28 May

Аутентификация — CUSTOM SETUP / AWS Amplify + React Native

JavaScriptDevelopment of mobile applicationsAmazon Web ServicesReactJS

cognito


Одна из самых запрашиваемых тем, среди подписчиков моего канала Димка Реактнативный — это аутентификация и авторизация в приложении React Native. Поэтому я решил посветить этому вопросу отдельный лонгрид и перед тем как мы начнем кодить, необходимо разобраться с определением Аутентификация/Авторизация.


Аутентификация


это проверка соответствия субъекта и того, за кого он пытается себя выдать, с помощью некой уникальной информации (отпечатки пальцев, цвет радужки, голос и т д.), в простейшем случае — с помощью почты и пароля.


Авторизация


это проверка и определение полномочий на выполнение некоторых действий в соответствии с ранее выполненной аутентификацией


В конце этой статьи, мы с вами сделаем это мобильное приложение:


cognito


Аутентификация являются неотъемлемой частью практически любого приложения. Знание того, кто пользователь, уникальный идентификатор пользователя, какие разрешения имеет пользователь, и вошли ли они в систему, позволяет вашему приложению отображать правильные представления и возвращать правильные данные для текущего вошедшего в систему пользователя.


Большинству приложений требуются механизмы для регистрации пользователей, их входа в систему, обработки шифрования и обновления паролей, а также множества других задач, связанных с управлением идентификацией. Современные приложения часто требуют таких вещей, как OAUTH (открытая аутентификация), MFA (многофакторная аутентификация) и TOTP (основанные на времени пароли времени).


В прошлом разработчикам приходилось вручную раскручивать все эти функции аутентификации с нуля. Одна только эта задача может занять у команды разработчиков недели или даже месяцы, чтобы сделать все правильно и сделать это безопасно. К счастью, сегодня есть полностью управляемые сервисы аутентификации, такие как Auth0, Okta и Amazon Cognito, которые обрабатывают все это для нас.


В этой статье вы узнаете, как правильно и безопасно внедрить аутентификацию в приложении React Native с использованием Amazon Cognito с AWS Amplify.


Amazon Cognito


это полностью управляемый сервис идентификации от AWS. Cognito обеспечивает простую и безопасную регистрацию пользователей, вход в систему, контроль доступа, обновление токенов и управление идентификацией пользователей. Cognito масштабируется до миллионов пользователей, а также поддерживает вход в систему с поставщиками социальных сетей, такими как Facebook, Google и Amazon.


Cognito состоит из двух основных частей: пулов пользователей и пулов идентификации.


User Pools


пулы пользователей предоставляют защищенный каталог пользователей, который хранит всех ваших пользователей и масштабируется до сотен миллионов пользователей. Это полностью управляемый сервис. Как бессерверная технология, пользовательские пулы легко настраиваются, не беспокоясь о том, чтобы поддерживать любую инфраструктуру. Пулы пользователей — это то, что управляет всеми пользователями, которые регистрируются и входят в учетную запись, и является основной частью, на которой мы сосредоточимся в этой статье.


Identity pools


пулы удостоверений позволяют вам авторизовать пользователей, вошедших в ваше приложение, для доступа к различным другим сервисам AWS. Допустим, вы хотите предоставить пользователю доступ к лямбда-функции, чтобы он мог получать данные из другого API. Вы можете указать это при создании пула удостоверений. В пулы пользователей входит то, что источником этих идентификаторов может быть пул пользователей Cognito или даже Facebook или Google.


Сценарий, когда пул пользователей Amazon Cognito и пул удостоверений используются вместе.


Смотрите схему для общего сценария Amazon Cognito. Здесь цель состоит в том, чтобы аутентифицировать вашего пользователя, а затем предоставить ему доступ к другому сервису AWS.


cognito


  1. На первом этапе пользователь вашего приложения входит в систему через пул пользователей и получает токены пула пользователей после успешной аутентификации.


  2. Затем ваше приложение обменивает токены пула пользователей на учетные данные AWS через пул удостоверений.


  3. Наконец, пользователь вашего приложения может затем использовать эти учетные данные AWS для доступа к другим сервисам AWS, таким как Amazon S3 или DynamoDB.



Cognito User Pools позволяет вашему приложению вызывать различные методы для службы для управления всеми аспектами идентификации пользователя, включая такие вещи, как:


  • Регистрация пользователя
  • Вход в систему пользователя
  • Выход пользователя
  • Смена пароля пользователя
  • Сброс пароля пользователя
  • Подтверждение кода MFA
  • Интеграция Amazon Cognito с AWS Amplify

AWS Amplify поддерживает Amazon Cognito различными способами. Прежде всего вы можете создавать и настраивать сервисы Amazon Cognito непосредственно из интерфейса командной строки AWS Amplify. Создав службу аутентификации через CLI, вы можете вызывать различные методы (например, signUp, signIn и signOut) из приложения JavaScript с помощью клиентской библиотеки Amplify JavaScript.


Amplify также имеет предварительно настроенные компоненты пользовательского интерфейса, которые позволяют выстраивать целые потоки аутентификации всего за пару строк кода для таких сред, как React, React Native, Vue и Angular.


Вы спросите и сколько же это все стоит?


Платите только за то, чем пользуетесь. Никаких минимальных платежей.


Используя Amazon Cognito Identity для создания пула пользователей, вы платите только за количество активных пользователей в месяц (MAU). MAU — это пользователи, которые в течение календарного месяца выполнили хотя бы одну операцию идентификации: регистрацию, авторизацию, обновление токена или изменение пароля. Последующие сессии активных пользователей и неактивные пользователи в этом календарном месяце не оплачиваются.


cognito


CODING TIME ‍‍


Чат поддержки AWS Amplify


Часть I


В этoй части мы настроим UI компонент аутентификации от AWS Amplify, а в следующей мы создадим его с нуля.



Весь код для этой части можно найти на GitHub.
Cognito


Step01


Создаем новый проект ️


react-native init auth

Запускаем проект


iOS


cd auth && react-native run-ios

Android


cd auth && react-native run-android

Step02


Подключаем иконки


Так как иконки используются фреймворком AWS Amplify, поэтому подключаем их согласно этой инструкции.
Проверяем наличие ошибок.


Добавляем в App.js


import Icon from 'react-native-vector-icons/FontAwesome5'

const App = () => {
  return (
    <>
      <Icon name="comments" size={30} color="#900" />
    </>
  )
}

Step03


Регистрируем свой AWS account


Регестрируемся согласно этой инструкции и по видеоучебнику чекаем все 5 шагов.


Внимание!!!


Потребуется банковская карта, где должно быть более 1$


Там же смотрим и ставим Amplify Command Line Interface (CLI)


Step04


Инициализация AWS Amplify в проект React Native


В корневой директории проекта React Native инициализируем наш AWS Amplify проект


amplify init

Отвечаем на вопросы:


amplify init


Проект инициализацировался


Step05


Подключаем плагин аутентификации


Теперь, когда приложение находится в облаке, вы можете добавить некоторые функции, такие как предоставление пользователям возможности зарегистрироваться в нашем приложении и войти в систему.


Командой


amplify add auth

подключаем функцию аутентификации. Выбираем конфигурацию по умолчанию. Это добавляет конфигурации ресурсов auth локально в ваш каталог ampify/backend/auth


Выбираем профиль, который мы хотим использовать. default. Enter и как пользователи будут входить в систему. Email(За SMS списывают деньги).


amplify init


Отправляем изменения в облако


amplify push

All resources are updated in the cloud


Step06


Подключаем AWS Amplify в проект React Native ️


Подробности в этой инструкции, а коротко и по прямой так:


yarn add aws-amplify @aws-amplify/core aws-amplify-react-native amazon-cognito-identity-js @react-native-community/netinfo

После установки обязательно заходим в папку ios и ставим поды


cd ios && pod install && cd ..

Step07


Редактируем структуру проекта


Создаем директорию /src и переносим туда файл App.js, переименовывая его в index.js


Правим импорт в /auth/index.js и скрываем будущие предупреждения.


import { AppRegistry, YellowBox } from 'react-native'
import App from './src'
import { name as appName } from './app.json'

YellowBox.ignoreWarnings([
  'Warning: AsyncStorage',  
  'Warning: componentWillReceiveProps',
  'RCTRootView cancelTouches',
  'not authenticated',
  'Sending `onAnimatedValueUpdate`'
])

//window.LOG_LEVEL = 'DEBUG'

AppRegistry.registerComponent(appName, () => App)

Step08


Минимальная конфигурация проекта и модуль Authenticator


Amplify.configure — конфигурация проекта


Authenticator — Модуль AWS Amplify Authentication предоставляет API-интерфейсы аутентификации и стандартные блоки для разработчиков, которые хотят создавать возможности аутентификации пользователей.


import React from 'react'
import {StatusBar} from 'react-native'
import Amplify from '@aws-amplify/core'
import {Authenticator} from 'aws-amplify-react-native'
import awsconfig from '../aws-exports'

Amplify.configure({
  ...awsconfig,
  Analytics: {
    disabled: true,
  },
})

const App = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <Authenticator usernameAttributes="email" />
    </>
  )
}

export default App

Запускаем симулятор, где нас встречает UI компонент аутентификации:


Cognito


Step09


Правим инпуты в App.js


Для этого добавляем signUpConfig


const signUpConfig = {
  hideAllDefaults: true,
  signUpFields: [
    {
      label: 'Email',
      key: 'email',
      required: true,
      displayOrder: 1,
      type: 'string',
    },
    {
      label: 'Password',
      key: 'password',
      required: true,
      displayOrder: 2,
      type: 'password',
    },
  ],
}

<Authenticator
   usernameAttributes="email"
   signUpConfig={signUpConfig}
/>

Step10


Меняем тему UI


Создаем точку экспорта наших будущих компонентов /src/components/index.js с содержанием


export * from './AmplifyTheme'

и соответствено создаем сам файл /src/components/AmplifyTheme/index.js темы с содержанием


import { StyleSheet } from 'react-native'

export const deepSquidInk = '#152939'
export const linkUnderlayColor = '#FFF'
export const errorIconColor = '#30d0fe'

const AmplifyTheme = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'space-around',
    paddingTop: 20,
    width: '100%',
    backgroundColor: '#FFF'
  },
  section: {
    flex: 1,
    width: '100%',
    padding: 30
  },
  sectionHeader: {
    width: '100%',
    marginBottom: 32
  },
  sectionHeaderText: {
    color: deepSquidInk,
    fontSize: 20,
    fontWeight: '500'
  },
  sectionFooter: {
    width: '100%',
    padding: 10,
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginTop: 15,
    marginBottom: 20
  },
  sectionFooterLink: {
    fontSize: 14,
    color: '#30d0fe',
    alignItems: 'baseline',
    textAlign: 'center'
  },
  navBar: {
    marginTop: 35,
    padding: 15,
    flexDirection: 'row',
    justifyContent: 'flex-end',
    alignItems: 'center'
  },
  navButton: {
    marginLeft: 12,
    borderRadius: 4
  },
  cell: {
    flex: 1,
    width: '50%'
  },
  errorRow: {
    flexDirection: 'row',
    justifyContent: 'center'
  },
  errorRowText: {
    marginLeft: 10
  },
  photo: {
    width: '100%'
  },
  album: {
    width: '100%'
  },
  button: {
    backgroundColor: '#30d0fe',
    alignItems: 'center',
    padding: 16
  },
  buttonDisabled: {
    backgroundColor: '#85E4FF',
    alignItems: 'center',
    padding: 16
  },
  buttonText: {
    color: '#fff',
    fontSize: 14,
    fontWeight: '600'
  },
  formField: {
    marginBottom: 22
  },
  input: {
    padding: 16,
    borderWidth: 1,
    borderRadius: 3,
    borderColor: '#C4C4C4'
  },
  inputLabel: {
    marginBottom: 8
  },
  phoneContainer: {
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center'
  },
  phoneInput: {
    flex: 2,
    padding: 16,
    borderWidth: 1,
    borderRadius: 3,
    borderColor: '#C4C4C4'
  },
  picker: {
    flex: 1,
    height: 44
  },
  pickerItem: {
    height: 44
  }
})

export { AmplifyTheme }

И подключаем тему в компонент Authenticator src/index.js


import {AmplifyTheme} from './components'

<Authenticator
  usernameAttributes="email"
  signUpConfig={signUpConfig}
  theme={AmplifyTheme}
/>

AmplifyTheme


Step11


Подключаем локализацию


В нашем случае русский язык


Добавляем экспорт в /src/components/index.js


export * from './Localei18n'

Cоздаем сам файл /src/components/Localei18n/index.js с содержанием


import { NativeModules, Platform } from 'react-native'
import { I18n } from '@aws-amplify/core'

let langRegionLocale = 'en_US'

// If we have an Android phone
if (Platform.OS === 'android') {
  langRegionLocale = NativeModules.I18nManager.localeIdentifier || ''
} else if (Platform.OS === 'ios') {
  langRegionLocale = NativeModules.SettingsManager.settings.AppleLocale || ''
}

const authScreenLabels = {
  en: {
    'Sign Up': 'Create new account',
    'Sign Up Account': 'Create a new account'
  },
  ru: {
    'Sign Up': 'Создать аккаунт',
    'Forgot Password': 'Забыли пароль?',
    'Sign In Account': 'Войдите в систему',
    'Enter your email': 'Введите email',
    'Enter your password': 'Введите пароль',
    Password: 'Пароль',
    'Sign In': 'Вход',
    'Please Sign In / Sign Up': 'Войти / Создать аккаунт',
    'Sign in to your account': 'Войдите в свой аккаунт',
    'Create a new account': 'Cоздайте свой аккаунт',
    'Confirm a Code': 'Подтвердите код',
    'Confirm Sign Up': 'Подтвердите регистрацию',
    'Resend code': 'Еще отправить код',
    'Back to Sign In': 'Вернуться к входу',
    Confirm: 'Подтвердить',
    'Confirmation Code': 'Код подтверждения',
    'Sign Out': 'Выход'
  }
}

// "en_US" -> "en", "es_CL" -> "es", etc
const languageLocale = langRegionLocale.substring(0, 2)
I18n.setLanguage(languageLocale)
I18n.putVocabularies(authScreenLabels)

const Localei18n = () => null

export { Localei18n }

И подключаем компонент Localei18n в src/index.js


import {
   AmplifyTheme,
   Localei18n
} from './components'

<Localei18n />
<Authenticator
  usernameAttributes="email"
  signUpConfig={signUpConfig}
  theme={AmplifyTheme}
/>

Запускаем проект, где видим, что локализация еще не применилась. Поэтому меняем в настройках своего симулятора язык на русский


Localei18n


Done


Часть II


Во-первых стандартный UI от Amplify далеко не всегда удовлетворяет UX приходящий со стороны заказчика


Во-вторых в официальной документации Amplify написано:


Data is stored unencrypted when using standard storage adapters (localStorage in the browser and AsyncStorage on React Native). Amplify gives you the option to use your own storage object to persist data. With this, you could write a thin wrapper around libraries like:
react-native-keychain
react-native-secure-storage
Expo’s secure store

Это значит, что данные аутентификации хранятся в не зашифрованном виде, а это риск информационной безопасности с возможными негативными последствиями, поэтому мы решим эти две задачи в этой части.


Весь код для этой части можно найти на GitHub.



Step01


UI Kit


Мы будем использовать наш UI Kit, но вы можете легко заменить его своим или любым другим.


Подключаем библиотеку компонентов согласно этой статьи.


Step02



Ставим навигацию react-navigation 5, также как написано здесь (на момент написание этой статьи):


yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view @react-navigation/stack

Добавляем поды под iOS


cd ios && pod install && cd ..

Рекомендую после каждой установки запускать приложение под iOS и Android, чтобы потом не искать библиотеку из-за которой приложение падает.


Step03


react-native-keychain


Ставим библиотеку react-native-keychain — это безопасное хранилище ключей react-native-keychain для React Native.


yarn add react-native-keychain

Добавляем поды под iOS


cd ios && pod install && cd ..

Согласно тому, что нам говорит официальная документация:


При использовании аутентификации с AWS Amplify вам не нужно обновлять токены Amazon Cognito вручную. Токены автоматически обновляются библиотекой при необходимости. Токены безопасности, такие как IdToken или AccessToken, хранятся в localStorage для браузера и в AsyncStorage для React Native. Если вы хотите хранить эти токены в более безопасном месте или используете Amplify на стороне сервера, вы можете предоставить свой собственный объект хранения для хранения этих токенов.

конфигурируем наш src/index.js


import React from 'react'
import Amplify from '@aws-amplify/core'
import * as Keychain from 'react-native-keychain'
import { ThemeProvider, DarkTheme, LightTheme } from 'react-native-unicorn-uikit'
import { useColorScheme } from 'react-native-appearance'
import AppNavigator from './AppNavigator'
import awsconfig from '../aws-exports'

const MEMORY_KEY_PREFIX = '@MyStorage:'
let dataMemory = {}

class MyStorage {
  static syncPromise = null

  static setItem(key, value) {
    Keychain.setGenericPassword(MEMORY_KEY_PREFIX + key, value)
    dataMemory[key] = value
    return dataMemory[key]
  }

  static getItem(key) {
    return Object.prototype.hasOwnProperty.call(dataMemory, key) ? dataMemory[key] : undefined
  }

  static removeItem(key) {
    Keychain.resetGenericPassword()
    return delete dataMemory[key]
  }

  static clear() {
    dataMemory = {}
    return dataMemory
  }
}

Amplify.configure({
  ...awsconfig,
  Analytics: {
    disabled: false
  },
  storage: MyStorage
})

const App = () => {
  const scheme = useColorScheme()
  return (
    <>
      <ThemeProvider theme={scheme === 'dark' ? DarkTheme : LightTheme}>
        <AppNavigator />
      </ThemeProvider>
    </>
  )
}

export default App

Step04


Константы


Чтобы не копипастить одни и те же значения, мы создаем файл с константами для общего использования в компонентах src/constants.js


import { Dimensions } from 'react-native'

export const BG = '#0B0B0B'
export const PINK = '#F20AF5'
export const PURPLE = '#7A1374'
export const BLUE = '#00FFFF'
export const GREEN = '#2E7767'
export const RED = '#FC2847'
export const LABEL_COLOR = BLUE
export const INPUT_COLOR = PINK
export const ERROR_COLOR = RED
export const HELP_COLOR = '#999999'
export const BORDER_COLOR = BLUE
export const DISABLED_COLOR = '#777777'
export const DISABLED_BACKGROUND_COLOR = '#eeeeee'

export const win = Dimensions.get('window')
export const W = win.width
export const H = win.height

export const Device = {
  // eslint-disable-next-line
  select(variants) {
    if (W >= 300 && W <= 314) return variants.mobile300 || {}
    if (W >= 315 && W <= 341) return variants.iphone5 || {}
    if (W >= 342 && W <= 359) return variants.mobile342 || {}
    if (W >= 360 && W <= 374) return variants.mi5 || {}
    if (W >= 375 && W <= 399) return variants.iphone678 || {}
    if (W >= 400 && W <= 409) return variants.mobile400 || {}
    if (W >= 410 && W <= 414) return variants.googlePixel || {}
    if (W >= 415 && W <= 434) return variants.mobile415 || {}
    if (W >= 435 && W <= 480) return variants.redmiNote5 || {}
  }
}

export const goBack = navigation => () => navigation.goBack()

export const onScreen = (screen, navigation, obj) => () => {
  navigation.navigate(screen, obj)
}

export const goHome = navigation => () => navigation.popToTop()()

Step05


AppNavigator


Создаем файл с конфигурацией навигации для нашей кастомной аутентификации src/AppNavigator.js


import * as React from 'react'
import { createStackNavigator } from '@react-navigation/stack'
import { Hello } from './screens/Authenticator'

const Stack = createStackNavigator()

const AppNavigator = () => {   
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false
      }}
      initialRouteName="HELLO"
    >
      <Stack.Screen name="HELLO" component={Hello} />
    </Stack.Navigator>
  )
}

export default AppNavigator

Step06


Hello screen


Создаем точку входа для нашых экранов аутентификации src/screens/Authenticator/index.js


Hello screen


Где для начала мы подключаем экран приветствия


export * from './Hello'

После создаем его src/screens/Authenticator/Hello/index.js


В хуке useEffect мы выполняем проверку на наличие токена пользователя, где в случае true мы отправляемся на экран User, а в случае false остаемся на этом экране.


import React, { useEffect, useState } from 'react'
import { Auth } from 'aws-amplify'
import * as Keychain from 'react-native-keychain'
import { AppContainer, Button, Space, H6 } from 'react-native-unicorn-uikit'
import { onScreen } from '../../../constants'

const Hello = ({ navigation }) => {
  const [loading, setLoading] = useState(false)
  useEffect(() => {
    setLoading(true)
    const key = async () => {
      try {
        const credentials = await Keychain.getInternetCredentials('auth')

        if (credentials) {
          const { username, password } = credentials
          const user = await Auth.signIn(username, password)
          setLoading(false)
          user && onScreen('USER', navigation)()
        } else {
          setLoading(false)
        }
      } catch (err) {
        console.log('error', err) // eslint-disable-line
        setLoading(false)
      }
    }
    key()
  }, []) // eslint-disable-line
  return (
    <AppContainer loading={loading}>
      <Space height={200} />
      <Button title="Sign In" onPress={onScreen('SIGN_IN', navigation)} />
      <Space height={10} />
      <H6 title="or" textStyle={{ alignSelf: 'center' }} />
      <Space height={15} />
      <Button title="Sign Up" onPress={onScreen('SIGN_UP', navigation)} />
    </AppContainer>
  )
}

export { Hello }

Собираем приложение и встречаем экран приветствия.



SignUp screen


Создаем экран регистрации SIGN_UP src/screens/Authenticator/SignUp/index.js, где для аутентификации мы используем метод Auth.signUp


SignUp


import React, { useState } from 'react'
import { Auth } from 'aws-amplify'
import * as Keychain from 'react-native-keychain'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { AppContainer, Space, Button, Input, TextError } from 'react-native-unicorn-uikit'
import { onScreen, goBack } from '../../../constants'

const SignUp = ({ navigation }) => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  const _onPress = async (values) => {
    const { email, password, passwordConfirmation } = values
    if (password !== passwordConfirmation) {
      setError('Passwords do not match!')
    } else {
      setLoading(true)
      setError('')
      try {
        const user = await Auth.signUp(email, password)
        await Keychain.setInternetCredentials('auth', email, password)
        user && onScreen('CONFIRM_SIGN_UP', navigation, { email, password })()
        setLoading(false)
      } catch (err) {
        setLoading(false)
        if (err.code === 'UserNotConfirmedException') {
          setError('Account not verified yet')
        } else if (err.code === 'PasswordResetRequiredException') {
          setError('Existing user found. Please reset your password')
        } else if (err.code === 'NotAuthorizedException') {
          setError('Forgot Password?')
        } else if (err.code === 'UserNotFoundException') {
          setError('User does not exist!')
        } else {
          setError(err.code)
        }
      }
    }
  }

  return (
    <>
      <AppContainer onPress={goBack(navigation)} title="Sign Up" loading={loading}>
        <Space height={80} />
        <Formik
          initialValues={{ email: '', password: '', passwordConfirmation: '' }}
          onSubmit={(values) => _onPress(values)}
          validationSchema={Yup.object().shape({
            email: Yup.string().email().required(),
            password: Yup.string().min(6).required(),
            passwordConfirmation: Yup.string().min(6).required()
          })}
        >
          {({ values, handleChange, errors, setFieldTouched, touched, isValid, handleSubmit }) => (
            <>
              <Input
                name="email"
                value={values.email}
                onChangeText={handleChange('email')}
                onBlur={() => setFieldTouched('email')}
                placeholder="E-mail"
                touched={touched}
                errors={errors}
                autoCapitalize="none"
              />
              <Input
                name="password"
                value={values.password}
                onChangeText={handleChange('password')}
                onBlur={() => setFieldTouched('password')}
                placeholder="Password"
                touched={touched}
                errors={errors}
                secureTextEntry
              />
              <Input
                name="passwordConfirmation"
                value={values.passwordConfirmation}
                onChangeText={handleChange('passwordConfirmation')}
                onBlur={() => setFieldTouched('passwordConfirmation')}
                placeholder="Password confirm"
                touched={touched}
                errors={errors}
                secureTextEntry
              />
              <Space height={30} />
              {error !== '' && <TextError title={error} textStyle={{ alignSelf: 'center' }} />}
              <Button title="Sign Up" disabled={!isValid} onPress={handleSubmit} formik />
            </>
          )}
        </Formik>
      </AppContainer>
    </>
  )
}

export { SignUp }

Step08


ConfirmSignUp screen


После успешного ответа с сервера, мы переходим на экран подтверждения и ввода кода, пришедшего нам на почту. Для этого создаем экран CONFIRM_SIGN_UP src/screens/Authenticator/ConfirmSignUp/index.js


ConfirmSignUp


import React, { useState } from 'react'
import { Auth } from 'aws-amplify'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { AppContainer, Button, Space, ButtonLink, TextError, Input } from 'react-native-unicorn-uikit'
import { onScreen, goBack } from '../../../constants'

const ConfirmSignUp = ({ route, navigation }) => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  const _onPress = async (values) => {
    setLoading(true)
    setError('')
    try {
      const { code } = values
      const { email, password } = route.params
      await Auth.confirmSignUp(email, code, { forceAliasCreation: true })
      const user = await Auth.signIn(email, password)
      user && onScreen('USER', navigation)()
      setLoading(false)
    } catch (err) {
      setLoading(false)
      setError(err.message)
      if (err.code === 'UserNotConfirmedException') {
        setError('Account not verified yet')
      } else if (err.code === 'PasswordResetRequiredException') {
        setError('Existing user found. Please reset your password')
      } else if (err.code === 'NotAuthorizedException') {
        setError('Forgot Password?')
      } else if (err.code === 'UserNotFoundException') {
        setError('User does not exist!')
      }
    }
  }

  const _onResend = async () => {
    try {
      const { email } = route.params
      await Auth.resendSignUp(email)
    } catch (err) {
      setError(err.message)
    }
  }

  return (
    <>
      <AppContainer title="Confirmation" onPress={goBack(navigation)} loading={loading}>
        <Formik
          initialValues={{ code: '' }}
          onSubmit={(values) => _onPress(values)}
          validationSchema={Yup.object().shape({
            code: Yup.string().min(6).required()
          })}
        >
          {({ values, handleChange, errors, setFieldTouched, touched, isValid, handleSubmit }) => (
            <>
              <Space height={180} />
              <Input
                name="code"
                value={values.code}
                onChangeText={handleChange('code')}
                onBlur={() => setFieldTouched('code')}
                placeholder="Insert code"
                touched={touched}
                errors={errors}
              />
              <ButtonLink title="Resend code?" onPress={_onResend} textStyle={{ alignSelf: 'center' }} />
              {error !== 'Forgot Password?' && <TextError title={error} />}
              <Button title="Confirm" disabled={!isValid} onPress={handleSubmit} formik />
              <Space height={50} />
            </>
          )}
        </Formik>
      </AppContainer>
    </>
  )
}

export { ConfirmSignUp }

ResendSignUp


Если код не пришел, то мы должны предоставить пользователю возможность отправить код повторно. Для этого на кнопку Resend code? мы вешаем метод Auth.resendSignUp(userInfo.email)
В случае успешного вызова метода


Auth.confirmSignUp(email, code, { forceAliasCreation: true })

мы должны вызывать метод


Auth.signIn(email, password)

Step09


User screen


В случае успеха переходим на экран USER, который мы создаем c кнопкой выхода из приложения и очисткой токенов src/screens/Authenticator/User/index.js


User screen


import React, { useState, useEffect } from 'react'
import { Auth } from 'aws-amplify'
import * as Keychain from 'react-native-keychain'
import { AppContainer, Button } from 'react-native-unicorn-uikit'
import { goHome } from '../../../constants'

const User = ({ navigation }) => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  useEffect(() => {
    const checkUser = async () => {
      await Auth.currentAuthenticatedUser()
    }
    checkUser()
  })

  const _onPress = async () => {
    setLoading(true)
    try {
      await Auth.signOut()
      await Keychain.resetInternetCredentials('auth')
      goHome(navigation)()
    } catch (err) {
      setError(err.message)
    }
  }

  return (
    <AppContainer message={error} loading={loading}>
      <Button title="Sign Out" onPress={_onPress} />
    </AppContainer>
  )
}

export { User }

Step10


SignIn screen


После того, как зарегистрировали пользователя, мы должны предоставить юзеру возможность войти в приложение через логин и пароль. Для этого мы создаем экран SIGN_IN src/screens/Authenticator/SignIn/index.js


SignIn screen


import React, { useState } from 'react'
import { Auth } from 'aws-amplify'
import * as Keychain from 'react-native-keychain'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { AppContainer, Button, Space, ButtonLink, TextError, Input } from 'react-native-unicorn-uikit'
import { onScreen, goBack } from '../../../constants'

const SignIn = ({ navigation }) => {
  const [userInfo, setUserInfo] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  const _onPress = async (values) => {
    setLoading(true)
    setError('')
    try {
      const { email, password } = values
      const user = await Auth.signIn(email, password)
      await Keychain.setInternetCredentials('auth', email, password)
      user && onScreen('USER', navigation)()
      setLoading(false)
    } catch (err) {
      setLoading(false)
      if (err.code === 'UserNotConfirmedException') {
        setError('Account not verified yet')
      } else if (err.code === 'PasswordResetRequiredException') {
        setError('Existing user found. Please reset your password')
      } else if (err.code === 'NotAuthorizedException') {
        setError('Forgot Password?')
      } else if (err.code === 'UserNotFoundException') {
        setError('User does not exist!')
      } else {
        setError(err.code)
      }
    }
  }

  return (
    <>
      <AppContainer onPress={goBack(navigation)} title="Sign In" loading={loading}>
        <Space height={140} />
        <Formik
          initialValues={{ email: '', password: '' }}
          onSubmit={(values) => _onPress(values) && setUserInfo(values.email)}
          validationSchema={Yup.object().shape({
            email: Yup.string().email().required(),
            password: Yup.string().min(6).required()
          })}
        >
          {({ values, handleChange, errors, setFieldTouched, touched, isValid, handleSubmit }) => (
            <>
              <Input
                name="email"
                value={values.email}
                onChangeText={handleChange('email')}
                onBlur={() => setFieldTouched('email')}
                placeholder="E-mail"
                touched={touched}
                errors={errors}
                autoCapitalize="none"
              />
              <Input
                name="password"
                value={values.password}
                onChangeText={handleChange('password')}
                onBlur={() => setFieldTouched('password')}
                placeholder="Password"
                touched={touched}
                errors={errors}
                secureTextEntry
              />
              {error !== 'Forgot Password?' && <TextError title={error} textStyle={{ alignSelf: 'center' }} />}
              {error === 'Forgot Password?' && (
                <ButtonLink
                  title={error}
                  onPress={onScreen('FORGOT', navigation, userInfo)}
                  textStyle={{ alignSelf: 'center' }}
                />
              )}
              <Space height={30} />
              <Button title="Sign In" disabled={!isValid} onPress={handleSubmit} formik />
            </>
          )}
        </Formik>
      </AppContainer>
    </>
  )
}

export { SignIn }

Step11


Forgot password screen


В случае успеха, мы отправляем юзера на экран USER, который мы уже ранее сделали, а если юзер забыл или не правильно ввел пароль, то мы показываем ошибку Forgot Password? и предлагаем сбросить пароль.


Forgot password


Для этого мы создаем экран FORGOT src/screens/Authenticator/Forgot/index.js


Forgot password


import React, { useState } from 'react'
import { Auth } from 'aws-amplify'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { AppContainer, Button, Input } from 'react-native-unicorn-uikit'
import { onScreen, goBack } from '../../../constants'

const Forgot = ({ route, navigation }) => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  const _onPress = async (values) => {
    setLoading(true)
    try {
      const { email } = values
      const user = await Auth.forgotPassword(email)
      user && onScreen('FORGOT_PASSWORD_SUBMIT', navigation, email)()
      setLoading(false)
    } catch (err) {
      setError(error)
    }
  }

  return (
    <>
      <AppContainer title="Forgot" onPress={goBack(navigation)} loading={loading}>
        <Formik
          initialValues={{ email: route.params }}
          onSubmit={(values) => _onPress(values)}
          validationSchema={Yup.object().shape({
            email: Yup.string().email().required()
          })}
        >
          {({ values, handleChange, errors, setFieldTouched, touched, isValid, handleSubmit }) => (
            <>
              <Input
                name="email"
                value={values.email}
                onChangeText={handleChange('email')}
                onBlur={() => setFieldTouched('email')}
                placeholder="E-mail"
                touched={touched}
                errors={errors}
                autoCapitalize="none"
              />
              <Button title="Confirm" disabled={!isValid} onPress={handleSubmit} formik />
            </>
          )}
        </Formik>
      </AppContainer>
    </>
  )
}

export { Forgot }

Step12


Forgot Password Submit


После подтверждения e-mail, мы вызываем метод Auth.forgotPassword(email) и в случае, если такой юзер есть, то отправляем пользователя на экран FORGOT_PASSWORD_SUBMIT src/screens/Authenticator/ForgotPassSubmit/index.js


ForgotPassSubmit


import React, { useState } from 'react'
import { Platform } from 'react-native'
import { Auth } from 'aws-amplify'
import * as Keychain from 'react-native-keychain'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { AppContainer, Button, Space, Input, TextError } from 'react-native-unicorn-uikit'
import { onScreen, goBack } from '../../../constants'

const ForgotPassSubmit = ({ route, navigation }) => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  const _onPress = async (values) => {
    setLoading(true)
    try {
      const { email, code, password } = values
      await Auth.forgotPasswordSubmit(email, code, password)
      await Keychain.setInternetCredentials('auth', email, password)
      onScreen('USER', navigation)()
      setLoading(false)
    } catch (err) {
      setLoading(false)
      setError(err.message)
    }
  }

  return (
    <>
      <AppContainer title="Confirmation" onPress={goBack(navigation)} loading={loading}>
        <Space height={Platform.OS === 'ios' ? 20 : 150} />
        <Formik
          initialValues={{ email: route.params, code: '', password: '', passwordConfirmation: '' }}
          onSubmit={(values) => _onPress(values)}
          validationSchema={Yup.object().shape({
            email: Yup.string().email().required(),
            code: Yup.string().min(6).required(),
            password: Yup.string().min(6).required(),
            passwordConfirmation: Yup.string().min(6).required()
          })}
        >
          {({ values, handleChange, errors, setFieldTouched, touched, isValid, handleSubmit }) => (
            <>
              <Input
                name="email"
                value={values.email}
                onChangeText={handleChange('email')}
                onBlur={() => setFieldTouched('email')}
                placeholder="E-mail"
                touched={touched}
                errors={errors}
                autoCapitalize="none"
              />
              <Input
                name="code"
                value={values.code}
                onChangeText={handleChange('code')}
                onBlur={() => setFieldTouched('code')}
                placeholder="Code"
                touched={touched}
                errors={errors}
              />
              <Input
                name="password"
                value={values.password}
                onChangeText={handleChange('password')}
                onBlur={() => setFieldTouched('password')}
                placeholder="Password"
                touched={touched}
                errors={errors}
                secureTextEntry
              />
              <Input
                name="passwordConfirmation"
                value={values.passwordConfirmation}
                onChangeText={handleChange('passwordConfirmation')}
                onBlur={() => setFieldTouched('passwordConfirmation')}
                placeholder="Password confirm"
                touched={touched}
                errors={errors}
                secureTextEntry
              />
              {error !== '' && <TextError title={error} textStyle={{ alignSelf: 'center' }} />}
              <Space height={30} />
              <Button title="Confirm" disabled={!isValid} onPress={handleSubmit} formik />
            </>
          )}
        </Formik>
      </AppContainer>
    </>
  )
}

export { ForgotPassSubmit }

где после ввода кода, отправленного на почту, нового пароля и его подтверждения, мы вызываем метод смены пароля


Auth.forgotPasswordSubmit(email, code, password)

успех которого отправляет юзера на экран USER.


Step13


Связывание экранов


Подключаем все созданые компоненты в src/screens/Authenticator/index.js


export * from './Hello'
export * from './User'
export * from './SignIn'
export * from './SignUp'
export * from './Forgot'
export * from './ForgotPassSubmit'
export * from './ConfirmSignUp'

Step14


Update AppNavigator


Обновляем файл конфигурации навигации:


import * as React from 'react'
import { createStackNavigator } from '@react-navigation/stack'
import { Hello, SignUp, SignIn, ConfirmSignUp, User, Forgot, ForgotPassSubmit } from './screens/Authenticator'

const Stack = createStackNavigator()

const AppNavigator = () => {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false
      }}
      initialRouteName="HELLO"
    >
      <Stack.Screen name="HELLO" component={Hello} />
      <Stack.Screen name="SIGN_UP" component={SignUp} />
      <Stack.Screen name="SIGN_IN" component={SignIn} />
      <Stack.Screen name="FORGOT" component={Forgot} />
      <Stack.Screen name="FORGOT_PASSWORD_SUBMIT" component={ForgotPassSubmit} />
      <Stack.Screen name="CONFIRM_SIGN_UP" component={ConfirmSignUp} />
      <Stack.Screen name="USER" component={User} />
    </Stack.Navigator>
  )
}

export default AppNavigator

Step15


Clean Up


Так как мы используем кастомную тему, то удаляем компоненты AmplifyTheme и Localei18n


Step16


Debug


Для того, чтобы понимать, что происходит с токенами в вашем приложении, добавьте в корневой /index.js


window.LOG_LEVEL = 'DEBUG'

Запускаем приложение и получаем кастомную аутентификацию.


Done


В продолжение темы смотреть статью DataStore — CRUD (Create Read Update Delete)


Become a Patron!

Tags:authenticationauthorizationAws amplifyawsreact nativefullstackserverlessamazonaутентификацияавторизация
Hubs: JavaScript Development of mobile applications Amazon Web Services ReactJS
+8
1.7k 23
Comments 5