這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408
Google 登入
https://docs.amplify.aws/lib/auth/social/q/platform/react-native
創建 OAuth 用戶端 ID
首先需要在 Google Console 新增 OAuth 用戶端 ID:
- 應用程式類型:網頁應用程式
- 已授權的 JavaScript 來源:cognito domain (在 user pool – app integration)
- 已授權的重新導向 URI: 來源後面加上
/oauth2/idpresponse
創建完會獲得 OAuth Client ID 和 OAuth Client Secret,這兩個等等會需要用到:
如果沒有 domain 的話就點擊右側的 Actions – Create Cognito domain 新建一個:
可以自定義 domain:
創建 Identity provider
為了能夠使用 Google 登入,需要要在 user pool 中新建 Identity provider
Amazon Cognito -> User pool -> Sign-in experience -> Add Identity provider
選擇 Google
設置 Google federation
- Client ID: 剛剛新增的 OAuth Client ID
- Client secret: 剛剛新增的 OAuth Client secret
- Authorized scopes:
profile email openid
全部都設置完成之後記得要在專案中將最新的設置拉下來:
amplify pull --appId <app_id> --envName <env_name>
完成 Google 登入
使用 Amplify 提供的第三方登入 API Auth.federatedSignIn
,provider 設為 CognitoHostedUIIdentityProvider.Google
:
import { CognitoHostedUIIdentityProvider } from '@aws-amplify/auth'
Auth.federatedSignIn({
provider: CognitoHostedUIIdentityProvider.Google
})}
完整寫法如下:
import React, { memo, useState, useEffect } from 'react'
import { Button } from 'react-native-paper'
import { Auth, Hub } from 'aws-amplify'
import { CognitoHostedUIIdentityProvider } from '@aws-amplify/auth'
import { UserData } from 'amazon-cognito-identity-js'
export const LoginScreen = ({ navigation }: LoginScreenProps) => {
const [user, setUser] = useState<UserData | null>(null)
useEffect(() => {
const unsubscribe = Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
getUser()
break
case 'signOut':
setUser(null)
break
case 'signIn_failure':
console.log('Sign in failure', data)
break
}
});
getUser()
return unsubscribe
}, [])
const getUser = async () => {
try {
const currentUser = await Auth.currentAuthenticatedUser()
setUser(currentUser)
} catch (error) {
console.error(error)
console.log("Not signed in")
}
}
return (
<>
<StyleText>User: {user ? JSON.stringify(user?.attributes) : 'None'}</StyleText>
<Button mode="contained" onPress={() => Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google })}>
Login with Google
</Button>
</>
)
}
登入後無法自動跳轉回應用
現在使用 Google 登入成功後依然無法跳轉回 App,這是因為還需要設置 Linking module
iOS
打開 xcodeproj – Build Settings – 搜索 Header Search Paths
雙擊點選新增:
$(PODS_ROOT)/../../node_modules/react-native/Libraries/LinkingIOS
新增完之後打開 AppDelegate.mm
加入以下內容
#import <React/RCTLinkingManager.h>
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
重新打開 APP 就可以了。
Android
在android/app/src/main/AndroidManifest.xml
添加下面這段,還有 android:launchMode
要設為 singleTask
:
<intent-filter android:label="filter_react_native">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
完整的身份驗證流程範例
可以使用 Context + Provider 的方式在應用中共享用戶資料、登入狀態以及登入、登出方法:
<em>// _types_/auth.ts</em>
import { UserData } from 'amazon-cognito-identity-js'
import { LoginSchemaType } from '@/helpers/validation'
export type AuthContextData = {
user: UserData | null
isAuthenticated: boolean
onLogin: (data: LoginSchemaType) => Promise<void>
onLogout: () => Promise<void>
onSocialLogin: (type: 'Google') => void
}
<em>// context/AuthContext.tsx</em>
import { createContext } from 'react'
import { AuthContextData } from '@/_types_'
export const AuthContext = createContext<AuthContextData>({} as AuthContextData)
<em>// provider/AuthProvider.tsx</em>
import { useEffect, useState } from 'react'
import { Auth, Hub } from 'aws-amplify'
import { CognitoHostedUIIdentityProvider } from '@aws-amplify/auth'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AuthContext } from '@/context'
import { UserData } from 'amazon-cognito-identity-js'
import { LoginSchemaType } from '@/helpers/validation'
interface AuthProviderProps {
children: React.ReactNode
}
export const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<UserData | null>(null)
<em>// 監聽身份驗證事件</em>
useEffect(() => {
const unsubscribe = Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
getUser()
break
case 'signOut':
setUser(null)
break
case 'signIn_failure':
console.log('Sign in failure', data)
break
}
})
getUser()
return unsubscribe
}, [])
<em>// 獲取當前用戶資料</em>
const getUser = async () => {
try {
const currentUser = await Auth.currentAuthenticatedUser()
setUser(currentUser)
} catch (error) {
console.error(error)
setUser(null)
}
}
<em>// 使用第三方登入</em>
const onSocialLogin = (type: 'Google') => {
Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider[type] })
}
<em>// 普通登入</em>
const onLogin = async (data: LoginSchemaType) => {
try {
const { email, password } = data
await Auth.signIn(email, password)
} catch (error: any) {
console.log(error)
}
}
<em>// 登出</em>
const onLogout = async () => {
await Auth.signOut()
}
const contextValue = {
user,
isAuthenticated: !!user,
onLogin,
onLogout,
onSocialLogin
}
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
)
}
新建一個 useAuth hook:
<em>// hooks/useAuth.tsx</em>
import { useContext } from 'react'
import { AuthContext } from '@/context'
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
接著將 Provider 包在應用路由的外層:
isAuthenticated
: 登入狀態,若為true
代表已登入,為false
則為未登入- 根據登入狀態判斷使用者能前往哪些路由
<em>// App.tsx</em>
import { Amplify } from 'aws-amplify'
import { createStackNavigator } from '@react-navigation/stack'
import { AuthStackNavigator } from '@/navigation/AuthStackNavigator'
import { LoginStackNavigator } from '@/navigation/LoginStackNavigator'
import { useAuth } from '@/hooks'
import { AuthProvider } from '@/provider'
import awsconfig from './aws-exports'
const Stack = createStackNavigator()
Amplify.configure(awsconfig)
const App = () => {
const { isAuthenticated } = useAuth()
return (
<AuthProvider>
<Stack.Navigator>
{isAuthenticated ? (
<Stack.Screen
name="AuthStack"
component={AuthStackNavigator}
options={{ headerShown: false }}
/>
) : (
<Stack.Screen
name="LoginStack"
component={LoginStackNavigator}
options={{ headerShown: false }}
/>
)}
</Stack.Navigator>
</AuthProvider>
)
}
export default App
完成以上工作之後就可以在登入、註冊等組件中使用 useAuth hook 來獲取與身份驗證相關的 data 和 function。比如我需要點按鈕登入,那麽就從 useAuth 中獲取 login function 調用:
import { useState } from 'react'
import { Button } from 'react-native'
import { useAuth } from '@/hooks'
<em>// ...</em>
export const LoginScreen = () => {
const { login } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
<em>// ...</em>
return (
<em>// ...</em>
<Button
style={styles.loginButton}
title={t('Login.SignIn')}
onPress={() => login({ email, password })}
/>
)
}
這只是一個簡單的身份驗證方法,還有很多細節待完善,僅供參考。
忘記密碼&重設密碼
amplify 有提供 Auth.forgotPassword(email)
的 API 可以寄出重設密碼的驗證碼,獲取驗證碼之後使用 Auth.forgotPasswordSubmit(email, code, newPassword)
即可重設密碼。
isSent
: 用於記錄是否已經寄出驗證信的狀態canResend
: 判斷是否可以送驗證信(送出時改為false
, 15秒後自動恢復成true
可再次發送)code
,newPassword
只有在驗證信已寄出之後才可以填寫
import { useState, useEffect } from 'react'
import { TextInput, Button } from 'react-native'
import { Auth } from 'aws-amplify'
<em>// ...</em>
export const ForgotPasswordScreen = ({ navigation }: ForgotPasswordScreenProps) => {
const { t } = useTranslation()
const [isSent, setIsSent] = useState(false)
const [canResend, setCanResend] = useState(true)
const [email, setEmail] = useState('')
const [code, setCode] = useState('')
const [newPassword, setNewPassword] = useState('')
<em>// 模擬15秒後可以再次寄出</em>
useEffect(() => {
if (canResend) return
setTimeout(() => {
setCanResend(true)
}, 15000)
}, [canResend])
const forgotPassword = async () => {
try {
await Auth.forgotPassword(email)
setIsSent(true)
setCanResend(false)
} catch (err) {
console.log(err)
setIsSent(false)
setCanResend(true)
}
}
const forgotPasswordSubmit = async () => {
try {
const res = await Auth.forgotPasswordSubmit(
email,
code,
newPassword
)
if (res === 'SUCCESS') {
navigation.navigate('Login')
}
} catch (err) {
console.log(err)
}
}
return (
<Column v="center" style={styles.container}>
<Column style={styles.form}>
<TextInput
value={email}
textContentType="emailAddress"
keyboardType="email-address"
placeholder={t('Login.Email')}
onChangeText={setEmail}
/>
<Row h="center" style={styles.sendCode}>
<Button
title={t(isSent ? 'Login.ResendCode' : 'Login.SendResetCode')}
disabled={!canResend}
onPress={forgotPassword}
/>
</Row>
<TextInput
value={code}
editable={isSent}
style={{ opacity: !isSent ? 0.5 : 1 }}
placeholder={t('Login.ConfirmCode')}
onChangeText={setCode}
/>
<TextInput
value={newPassword}
editable={isSent}
style={{ opacity: !isSent ? 0.5 : 1 }}
placeholder={t('Login.NewPassword')}
onChangeText={setNewPassword}
/>
</Column>
<Button
title={t('Login.ResetPassword')}
style={styles.button}
disabled={!isSent}
onPress={forgotPasswordSubmit}
/>
</Column>
)
}