繼續未完的 Amazon Cognito 設定
在設定 react-native-facebook-login
與 react-native-google-signin
的過程中,我們一併把 Facebook & Google 應用程式設定好了,現在我們回過頭來設定之前未設定完畢的部份。
- 先來看 OpenID 的部份,切到 OpenID 頁籤會發現什麼也沒有:
是說 OpenID Provider 必須在 Amazon IAM 中進行設定。
- IAM 是 Identity and Access Management 的縮寫,進到服務首頁後點擊 開始使用 Amazon IAM。
- 在左方選項中點選 Identity providers,再點選 Create Provider。
- Provider Type 選擇
OpenID Connect
,Provider URL 填入https://accounts.google.com
,最後 Audience 的部份,請打開 Google Developers Console,切換至相應的應用程式,並在 憑證 頁下方找到 OAuth 2.0 用戶端 ID,這邊請先加任何一組,之後修改設定請通通加到Audience
。 - 儲存之後,回到 Cognito 設定頁面重整,可以發現 OpenID 頁籤有
acctouns.google.com
了,勾選它。 - 切到 Facebook 頁籤,填入 Facebook App ID:
- 點
Create pool
進到下一步,保持預設值即可,點Allow
。 - Cognito 設定完成,不過 SDK 我們先前已經設定完畢,這邊就不用再做一次了。
- 回到 IAM 編輯剛剛的
accounts.google.com
,把所有audience
加入。
前端程式
這邊的前端程式是指 React Native 的程式碼,本文同時會用它進行 iOS 與 Android 的實作。
主要流程可參考 https://github.com/awslabs/aws-sdk-react-native/tree/master/Core/example 這邊的範例程式碼。
以下分段介紹:
- 先準備好基本 layout 吧,Android & iOS 都一樣,大概就是放個標題,擺 Facebook 與 Google 登入的按鈕。
app.js
:import React, { PropTypes, Component } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; const styles = StyleSheet.create({ containerStyle: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20, }, headingText: { fontWeight: '500', fontSize: 18, color: 'rgb(38, 38, 38)', marginTop: 20, marginBottom: 12, }, facebookLoginButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', width: 260, height: 42, backgroundColor: 'rgb(59, 90, 150)', borderRadius: 4, paddingVertical: 15, paddingHorizontal: 32, }, facebookLoginButtonText: { fontWeight: 'normal', fontSize: 17, color: 'rgb(255, 255, 255)', }, googleLoginButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', width: 260, height: 42, backgroundColor: 'rgb(234, 67, 53)', borderRadius: 4, marginTop: 10, paddingVertical: 15, paddingHorizontal: 32, }, googleLoginButtonText: { fontWeight: 'normal', fontSize: 17, color: 'rgb(255, 255, 255)', marginLeft: 10, }, logoutButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', width: 260, height: 42, backgroundColor: 'rgb(234, 67, 53)', borderRadius: 4, marginTop: 10, paddingVertical: 15, paddingHorizontal: 32, }, logoutButtonText: { fontWeight: 'normal', fontSize: 17, color: 'rgb(255, 255, 255)', marginLeft: 10, }, }); class App extends Component { handleLogin = async (type) => { } handleLogout = () => { } render() { const { isLoading, loggedIn, profile } = this.state; return ( <View style={styles.containerStyle}> <Text style={styles.headingText}>React-Native Cognito Login Example</Text> <View> <TouchableOpacity onPress={() => this.handleLogin('facebook')} style={styles.facebookLoginButton}> <Text style={styles.facebookLoginButtonText}>Login with Facebook</Text> </TouchableOpacity> <TouchableOpacity onPress={() => this.handleLogin('google')} style={styles.googleLoginButton}> <Text style={styles.googleLoginButtonText}>Login with Google</Text> </TouchableOpacity> </View> </View> ); } } export default App;
註:本例中我們不打算用套件提供的按鈕。
- 再來新開一個檔案
utils/auth.js
,裡面放跟登入驗證有關的東西,先import
幾個東西:import { AWSCognitoCredentials } from 'aws-sdk-react-native-core'; import { FBLoginManager } from 'react-native-facebook-login'; import { GoogleSignin } from 'react-native-google-signin';
- 接著宣告幾個常數,比較好的作法是將它們放在環境變數中,或是透過 react-native-config 管理。
const IDENTITY_POOL_ID = 'ap-northeast-1:4e86b831-da7f-47d5-8382-3d800cd28a25'; const REGION = 'ap-northeast-1'; const GOOGLE_SIGNIN_IOS_CLIENT_ID = '469382905985-0ho9t3lc0g6ig7l69du9971vijdgv6fn.apps.googleusercontent.com'; const GOOGLE_SIGNIN_WEBCLIENT_ID = '469382905985-sdi5a3gqtk1ikce7cb2f0u6h9ashculq.apps.googleusercontent.com'; const providers = { 'graph.facebook.com': 'FacebookProvider', 'accounts.google.com': 'GoogleProvider', };
其中
IDENTITY_POOL_ID
是在 Amazon Cognito 建立Federated Identities
時的Identity Pool
會給的 pool id。忘記的話可以到Cognito > Federated Identities > Identity pool 名稱 > 右上角的 Edit identity pool
中找到。REGION
就是當初建立時選用的 region。GOOGLE_SIGNIN_IOS_CLIENT_ID
與GOOGLE_SIGNIN_WEBCLIENT_ID
可以從 Google Developers Console 中找到,在憑證
那一頁。由於 Android 上面用的是 Web Client 的方式,所以請不要複製錯了哦。providers
是我們這次會用到的 providers,這邊先定義好對應的名稱,之後會用到。 - 宣告其他變數
let identityId = null; let currentLoginMethod; // 目前是用哪個 provider let globalSupplyLogin = false; // 若已經有 id token / access token 就會登入的旗標 let globalAccessToken = ''; let globalOpenIdToken = ''; let openIdResolve; let openIdTokenPromise = new Promise(resolve => { openIdResolve = resolve; });
- 接著我們要定義幾個主要的 function:
getCredentials
、fbGetCredentials
、googleGetCredentials
、getIdentityId
、onLoginInvoked
、getOpenIdToken
。登入流程中,會先依據設定(例如 app ID、scopes 等)初始化第三方 providers 並嘗試取得 Access Token、接著取得 Identity ID(每個身分都有一個 ID,而這些身分都存放在 Identity Pool 裡面,可以去 Amazon Cognito 的 Federated Identities 下的 Identity browser 中看到登入過的 Identity ID 哦)、再拿 Access Token 去換 OpenID Token。getCredentials
:const getCredentials = () => Promise.race([fbGetCredential(), googleGetCredential()]);
fbGetCredentials
與googleGetCredentials
:const fbGetCredential = () => { return new Promise(resolve => { FBLoginManager.getCredentials((err, data) => { if ( data && typeof data.credentials === 'object' && typeof data.credentials.token === 'string' && data.credentials.token.length > 0 ) { currentLoginMethod = 'graph.facebok.com'; resolve({ accessToken: data.credentials.token, }); } else { console.log('fbGetCredential fail', data, err); } }); }); }; const googleGetCredential = () => { return new Promise(resolve => { GoogleSignin.hasPlayServices({ autoResolve: true }) .then(() => GoogleSignin.configure({ iosClientId: GOOGLE_SIGNIN_IOS_CLIENT_ID, webClientId: GOOGLE_SIGNIN_WEBCLIENT_ID, }) ) .then(() => GoogleSignin.currentUserAsync()) .then(user => { console.log('user', user); if (user && user.idToken && user.accessToken) { currentLoginMethod = 'accounts.google.com'; resolve({ idToken: user.idToken, accessToken: user.accessToken, }); } else { console.log('user error'); } }) .catch(err => { console.log(err); }); }); };
getIdentityId
:async function getIdentityId() { try { await AWSCognitoCredentials.getCredentialsAsync(); const identity = await AWSCognitoCredentials.getIdentityIDAsync(); // https://github.com/awslabs/aws-sdk-react-native/search?utf8=%E2%9C%93&q=identityid // aws-sdk-react-native return `identity.identityId` for ios, `identity.identityid` for android const _identityId = identity.identityId || identity.identityid; return _identityId; } catch (e) { console.log('Error: ', e); } }
onLoginInvoked
:async function onLoginInvoked(isLoggingIn, accessToken, idToken) { if (isLoggingIn) { globalSupplyLogin = true; globalAccessToken = accessToken; globalOpenIdToken = idToken; const map = {}; map[providers[currentLoginMethod]] = idToken || accessToken; AWSCognitoCredentials.setLogins(map); // ignored for iOS identityId = await getIdentityId(); const token = await getOpenIdToken(idToken || accessToken); return { accessToken: globalAccessToken, openIdToken: token, }; } else { globalSupplyLogin = false; globalAccessToken = ''; globalOpenIdToken = ''; } }
getOpenIdToken
:async function getOpenIdToken(token) { const payload = { IdentityId: identityId, Logins: { [currentLoginMethod]: token, }, }; try { const rsp = await fetch('https://cognito-identity.ap-northeast-1.amazonaws.com/', { method: 'POST', headers: new Headers({ 'X-Amz-Target': 'AWSCognitoIdentityService.GetOpenIdToken', 'Content-Type': 'application/x-amz-json-1.1', random: new Date().valueOf(), 'cache-control': 'no-cache', }), body: JSON.stringify(payload), }); if (!rsp.ok) { // rsp.status === 400, ok: false // message: "Invalid login token. Token is expired." logout(); } else { const json = await rsp.json(); openIdResolve(json.Token); return json.Token; } } catch (e) { console.log('Error of getOpenIdToken: ', e); } }
- 再來定義
init
function:function init() { AWSCognitoCredentials.getLogins = () => { if (globalSupplyLogin) { const map = {}; map[providers[currentLoginMethod]] = globalOpenIdToken || globalAccessToken; return map; } return ''; }; AWSCognitoCredentials.initWithOptions({ region: REGION, identity_pool_id: IDENTITY_POOL_ID, }); getCredentials().then(tokens => { onLoginInvoked(true, tokens.accessToken, tokens.idToken); }); }
其中必須自行實作
AWSCognitoCredentials.getLogins
的 function。然後傳入 region 與 identity_pool_id 以初始化 Amazon Cognito 的 credentials init function。
最後就是執行上面定義好的
getCredentials
function 了。 - 再來分別實作
loginFB
與loginGoogle
兩個 functions:function loginFB() { return new Promise((resolve, reject) => { FBLoginManager.loginWithPermissions( ['email', 'user_birthday'], async (error, data) => { if (!error) { const token = data.credentials.token; currentLoginMethod = 'graph.facebook.com'; const result = await onLoginInvoked(true, token); resolve(result); } else { currentLoginMethod = null; reject(error); } }); }); } async function loginGoogle() { const user = await GoogleSignin.signIn().catch(error => { console.log('WRONG SIGNIN', err); currentLoginMethod = null; }); if (user) { currentLoginMethod = 'accounts.google.com'; return await onLoginInvoked(true, user.accessToken, user.idToken); } return; }
- 再來是登出相關的 functions:
function cleanLoginStatus() { AWSCognitoCredentials.clearCredentials(); AWSCognitoCredentials.clear(); // clear keychain identityId = null; globalSupplyLogin = false; globalAccessToken = ''; openIdTokenPromise = new Promise(resolve => { openIdResolve = resolve; }); } function logout() { FBLoginManager.logout(error => { if (!error) { cleanLoginStatus(); } }); GoogleSignin.signOut().then(() => { cleanLoginStatus(); }); }
- 最後將這些 functions export 出去,以便呼叫:
export { init, loginFB, loginGoogle, logout, };
- 好了,回到我們的主程式,加上以下部分:將剛剛寫好的程式 import 進來:
import * as AuthUtils from './utils/auth';
在
componentWillMount
時,呼叫初始化init
function:componentWillMount() { AuthUtils.init(); }
補上初始化的 state:
state = { loggedIn: false, isLoading: false, };
實作
handleLogin
function:handleLogin = async (type) => { let result = {}; this.setState({ isLoading: true, }); try { if (type === 'facebook') { result = AuthUtils.loginFB().catch(() => { this.setState({ isLoading: false, }); }); } else if (type === 'google') { result = await AuthUtils.loginGoogle(); } } catch (err) { this.setState({ isLoading: false, }); } this.setState({ loggedIn: true, isLoading: false, }); }
還有
handleLogout
function:handleLogout = () => { AuthUtils.logout(); this.setState({ loggedIn: false, }); }
然後在按鈕上加上
onPress
事件,並設定只有在loggedIn
為 false 的時候才 render 按鈕:{!this.state.loggedIn && <View> <TouchableOpacity onPress={() => this.handleLogin('facebook')} style={styles.facebookLoginButton}> <Text style={styles.facebookLoginButtonText}>Login with Facebook</Text> </TouchableOpacity> <TouchableOpacity onPress={() => this.handleLogin('google')} style={styles.googleLoginButton}> <Text style={styles.googleLoginButtonText}>Login with Google</Text> </TouchableOpacity> </View>}
在
loggedIn
為 true 的時候顯示登出按鈕:{this.state.loggedIn && <View style={styles.welcome}> <Text style={styles.welcomeText}> You're logged in! </Text> <TouchableOpacity onPress={() => this.handleLogout()} style={styles.logoutButton}> <Text style={styles.logoutButtonText}>Logout</Text> </TouchableOpacity> </View>}
再加上登入中的提示:
{this.state.isLoading && <View> <Text>Logging In...</Text> </View>}
- 大概就是這樣哦,實際登入登出看看吧!
疑難排解
如果沒有辦法順利地達成功能,這邊蒐集了一些可能遇到的錯誤與解法。
在進行 Android 按下 Google 登入按鈕後,一直噴 12501 錯誤
這在 react-native-google-signin 的 FAQ 中也有提到,可前往參閱。
請確認兩個部分都有設對:
- 請確認是使用 WebClient 的 ID。
- 請確認 signing key 是對的,請參考此篇。
在 Cognito 登入的那段 trace 到 Invalid login token. Incorrect token audience
的錯誤訊息
通常是 Google 登入時發生,請在 Amazon IAM 設定 accounts.google.com
的 audiences
加上對應的 client ID,看看是不是少加了 Web Client ID 呢?