最近開始接觸 React Native 的專案了,能用前端技術跟知識同時開發 iOS/Android App 滿新奇的!雖然缺乏原生 App 的知識讓我踩了不少坑,其中不少還是非常低能的坑 XD!

這次實作的任務是要在 App 加上 Google/Facebook 登入的功能,而會員系統是建構在 Amazon Cognito 上面的,再加上 React Native 的相關套件 – react-native-facebook-login 及 react-native-google-signin 來實作 App 中的登入功能。其中有些設定上的眉角讓我花了不少時間嘗試,所以整理成本篇的筆記備忘囉~


本文大綱

本篇文章滿長的,所以先整理大綱在此,了解一下會介紹到的部份:

  • 本文的套件版本
  • AWS SDK React Native 與 Amazon Cognito
  • react-native-facebook-login
  • react-native-google-signin
  • 繼續未完的 Amazon Cognito 設定
  • 前端程式
  • 疑難排解
  • 用 Amazon Lambda 來做 Serverless API
  • Amazon API Gateway
  • 在 App 端補上註冊(register)的 function
  • Source Code
  • 參考資料

本文的套件版本

由於這是個升版可能就變天的世代,所以開始之前先將相關版本列於下方:

AWS SDK React Native 與 Amazon Cognito

首先,要建構會員系統的話,依過去的經驗,有很多部分要處理,像是寫會員註冊、忘記密碼、認證、更新資料等後端程式,還要建會員資料庫、思考怎麼跟第三方社群登入(像是本文主要目的的 Google、Facebook 登入)串接、怎麼讓桌機與手機都能 work… 等一拖拉庫的事情。雖然要一個個自己來也是可以,但像前端工程師如我,可能就要花上不少時間了,也有時間成本的考量,而 Amazon Cognito 提供了一套不錯的跨裝置會員系統解決方案。

Amazon Cognito 讓您輕鬆新增使用者註冊與登入功能到行動應用程式與 Web 應用程式。使用 Amazon Cognito,您也可以選擇透過 Facebook、Twitter 或 Amazon 等社交身分供應商、利用 SAML 身分解決方案,或使用自己的身分系統來驗證使用者。除此之外,Amazon Cognito 可讓您將資料儲存在使用者的本機裝置,這允許應用程式即使在裝置離線時也能運作。然後,您可以同步使用者不同裝置之間的資料,無論他們使用哪部裝置,都能獲得一致的應用程式體驗。

而此次的任務就是在 React Native 的架構下要導入以 Cognito 為基礎的社群會員(Google 與 Facebook)登入流程,當然也可以選用非第三方的身分認證,像是手機、E-mail 認證等方式,不過就不在本文介紹範圍中了。首先,假設我們已經設定好一個基本的 React Native 的專案了,接下來就要安裝相關的套件 – aws-sdk-react-native。

註:其實在本文撰寫過程中,官方就更新 aws-sdk-react-native 的說明了,是說官方比較推薦去使用 AWS JavaScript SDK with React Native support,不過本文還是維持原本使用的 aws-sdk-react-native 這套進行實作,因為這套實在太多坑啦!就算看了說明檔還是看不懂怎麼使用!😱


以下流程主要是參考 @chunghe 在該 repo Issues 貼出的 detail step-by-step guide 進行步驟:

打包

  1. 首先要做的是自己打包(pack)一份 aws-sdk-react-native,請先下載 AWS Mobile iOS SDK 2.4.16 版(官網下載頁目前放上的 iOS 2.5+ 版跟本文流程不相容),然後解壓縮

  2. 然後把 aws-sdk-react-native 專案下載下來,可以用 git clone https://github.com/awslabs/aws-sdk-react-native.git

  3. 把步驟 1 中解壓縮的資料夾打開,進入 frameworks 資料夾,找到 AWSCore.framework,將它複製到 aws-sdk-react-native 資料夾中的 Core/src/ios/Frameworks/ 資料夾底下

  4. 然後 aws-sdk-react-native/Core/src/AWSCognitoCredentials.js 有個 JavaScript bug 😫 打包前要自己手動修正。請將第 48 行的:

    listener.addListener("LoginsRequestedEvent", async {callbackId} => {

    改為

    listener.addListener("LoginsRequestedEvent", async ({callbackId}) => {
  5. 接著進入 aws-sdk-react-native/Core/,鍵入以下指令進行打包(pack):

    npm pack
  6. 好了,打包完就有個名為 aws-sdk-react-native.tgz 的打包檔了,沒有操作錯誤的話,它大概有 8.3MB。接下來把它 copy 到專案目錄下並改個名字吧,我是放在 專案目錄/aws-sdk-react-native/ 底下,並更名為 aws-sdk-react-native-core-0.0.2-with-awscore-framework.tgz。

安裝到專案中

  1. 透過 yarn 安裝到專案中:

    yarn add ./aws-sdk-react-native/aws-sdk-react-native-core-0.0.2-with-awscore-framework.tgz
  2. 然後 link 它:

    react-native link aws-sdk-react-native-core
  3. 打開專案的 Xcode 檔案(在 ios/ 底下可能是 xxx.news.xcodeproj,有用 Pods 的話就是 xxx.xcworkspace),確認 Targets 是目前的專案名稱:image

    然後切換到 General 的頁籤,往下捲動直到找到 Embedded Binaries,按下下方的 + 按鈕。

  4. 在新跳出的視窗中,點擊左下角的 Add Other...,尋找專案目錄底下 node_modules/aws-sdk-native-core/ios/Frameworks/ 中的 AWSCore.framework 檔案,選定之後按下 Open。

  5. 在下一個跳出的視窗中,請確認 不要勾選 Destination: Copy items if needed 的選項,因為這個檔案已經在專案目錄的 node_modules/aws-sdk-native-core/ios/Frameworks/ 目錄底下囉。好了之後請按下 Finish。

    2017-05-24 10 23 09

  6. 操作無誤的話會看到下圖這樣:

    image

  7. 接下來請切換到 Build Phases 頁籤,並點擊左上角的 + 按鈕,選擇 New Run Script Phase。

    2017-05-24 10 24 23

  8. 請確認新增的 Run Script 區塊是在 Embed Frameworks 區塊之下的,不是的話請拖曳改變順序。然後展開 Run Script,確認 Shell 是 /bin/sh,接著在下方文字框輸入:

    bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSCore.framework/strip-frameworks.sh"

    接著要確認下面四個部分 Show environment variables in build log 已勾選、Run script only when installing 未勾選、Input Files 與 Output Files 都是空的。如圖:

    2017-05-23 8 41 04

  9. 最後,請切換至 Build Settings 頁籤,找到 Search Paths 底下的 Framework Search Paths(如果覺得選項太多不好找,可利用右上角的搜尋框),加入 $(SRCROOT)/../node_modules/aws-sdk-react-native-core/ios/Frameworks。

    2017-05-23 8 10 40

    好了,這樣套件就安裝完成了,可以試著 Build 看看,若沒有遇到 Fail 的話應該就是沒問題的。

Amazon Cognito 設定

假設已經申請好 AWS 的帳號了,請連到 Amazon Cognito 開始接下來的步驟:

  1. 到首頁請選擇「開始使用 Amazon Cognito」,連過去之後又有兩顆按鈕,因為這次的任務是要用第三方社群身分驗證進行登入,所以選擇右邊的 Manage Federated Identities。
  2. 點擊 Create new identity pool 建立新的身分池。
  3. 給這個 pool 一個名字吧,例如 My App。然後展開 Authentication providers,可以看到各式各樣的 providers,本例要設定的是 Facebook 與 Google,但是請 注意!若你有多個平台需要使用同一組 Google 身分,在 Google 的部份請透過 OpenId 進行設定喔!否則就會在不同平台登入的同一組 Google 帳號(例如 Web 跟 iOS 裝置登入的 Google 帳號)會被視為不同的身分!
  4. 由於下面介紹 react-native-facebook-login 及 react-native-google-signin 時,會介紹 Facebook 與 Google 應用程式的設定步驟,因此這邊就先跳過,只要知道之後會回來這邊設定 Facebook 與 OpenID 的部份即可。先留著這個視窗,晚點回來繼續完成。

react-native-facebook-login

  1. 安裝 react-native-facebook-login

    yarn add react-native-facebook-login
  2. 打開套件目錄

    open node_modules/react-native-facebook-login

iOS

  1. 在 Xcode 中,把步驟 2 中的 RCTFBLogin.xcodeproj 檔案拖曳到專案中的 Libraries 目錄。2017-05-24 10 35 21

  2. 確定 Target 是專案本身、而非 react-native-facebook-login 套件,接著切換到 Build Phases 頁籤,展開 Link Binary With Libraries,點擊下方的 + 按鈕,鍵入 libRCTFBLogin.a 找到它,加入。

    2017-05-24 10 36 11

  3. 接下來要加入 Facebook SDK 到 Xcode 專案中。在專案目錄下透過 open node_modules/react-native-facebook-login/FacebookSDK 指令打開 FacebookSDK  資料夾。

  4. 確定在 Project Navigator 項目(如圖)中:

    2017-05-29 12 05 54

    然後在專案名稱上點右鍵,選擇 New Group,並輸入 Frameworks(若已經有此資料夾的話,此步驟可省略)。

  5. 在步驟 5 中打開的資料夾中,找到 FBSDKCoreKit.framework、FBSDKLoginKit.framework 與 FBSDKShareKit.framework 三個檔案,將他們拖曳到步驟 6 中建立的 Frameworks 目錄中。

    2017-05-23 9 10 39

  6. 切換到 Build Settings 頁籤,捲動尋找 Search Paths 中的 Framework Search Paths,加入 $(SRCROOT)/../node_modules/react-native-facebook-login/FacebookSDK

    2017-05-23 9 11 35

  7. 接著編輯 ios/專案名稱/AppDelegate.m,在 #import "AppDelegate.h" 之下,加入:

    #import <FBSDKCoreKit/FBSDKCoreKit.h>
    #import <FBSDKLoginKit/FBSDKLoginKit.h>
  8. 往下找到 didFinishLaunchingWithOptions 這個 function,將最後

    return YES;

    的部份改為

    return [[FBSDKApplicationDelegate sharedInstance] application:application
                                      didFinishLaunchingWithOptions:launchOptions];
  9. 然後在 didFinishLaunchingWithOptions 這個 function 結束後,加上:

    // Facebook SDK
    - (void)applicationDidBecomeActive:(UIApplication *)application {
        [FBSDKAppEvents activateApp];
    }
    
    - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:   (id)annotation {
        return [[FBSDKApplicationDelegate sharedInstance] application:application
                                                              openURL:url
                                                    sourceApplication:sourceApplication
                                                           annotation:annotation];
    }
  10. 以上 FacebookSDK iOS 的安裝完畢,還剩一些設定 iOS 的部份就完成囉。接下來要進行 Facebook 應用程式相關的設定,先去 Facebook Apps – Quick Start for iOS,建立或是選擇既有的應用程式。在 Facebook 應用程式設定過程中,需要填入 bundle ID,可在 General 中找到:

    2017-05-23 9 45 03

  11. 接著以該應用程式的資訊,請編輯 ios/專案名稱/Info.plist,在第一層 <dict></dict> 之中,適當的區塊加上:

    <key>CFBundleURLTypes</key>
    <array>
      <dict>
        <key>CFBundleURLSchemes</key>
        <array>
          <string>fb{your-app-id}</string>
        </array>
      </dict>
    </array>
    <key>FacebookAppID</key>
    <string>{your-app-id}</string>
    <key>FacebookDisplayName</key>
    <string>{your-app-name}</string>
    <key>LSApplicationQueriesSchemes</key>
    <array>
      <string>fbapi</string>
      <string>fb-messenger-api</string>
      <string>fbauth2</string>
      <string>fbshareextension</string>
    </array>

    其中 {your-app-id} 是 Facebook 應用程式 ID、{your-app-name} 則是 Facebook 應用程式名稱。請依據實際情況填入。

好了,接下來是 Android 的部份。

Android

  1. 請至 Facebook Apps – Quickstart for Android 完成設定。過程中需填入 package name,可在 AndroidManifest.xml 中找到:

    image

    Key Hashes 則可透過步驟中的提示,使用 keytool 指令找到。

  2. 確定已經透過 yarn 或 npm 安裝了 react-native-facebook-login。

  3. 編輯 android/settings.gradle,加入以下內容:

    include ':react-native-facebook-login'
    project(':react-native-facebook-login').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-facebook-login/android')
  4. 編輯 android/app/build.gradle,在 dependencies { } 區塊中加入以下內容:

    compile project(':react-native-facebook-login')
  5. 編輯 android/app/src/main/java/com/專案名稱/MainApplication.java,在上方 import 處加上:

    import com.magus.fblogin.FacebookLoginPackage;

    然後在 new MainReactPackage(), 之後,加入

    new FacebookLoginPackage()
  6. 編輯 android/app/src/main/res/values/strings.xml(字串變數檔,若想要用 react-native-config 也是可以的),加入以下內容:

    <resources>
        <string name="app_name">{Your_App_Name}</string>
        <string name="fb_app_id">{FB_APP_ID}</string>
        <string name="fb_login_protocol_scheme">fb{FB_APP_ID}</string>
    </resources>

    其中 {Your_App_Name}、{FB_APP_ID}、fb{FB_APP_ID} 請依據實際情況填入。

  7. 編輯 android/app/src/main/AndroidManifest.xml,在 <application></application> 區塊中,加入以下內容:

    <!--add FacebookActivity-->
    <activity tools:replace="android:theme"
        android:name="com.facebook.FacebookActivity"
        android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
    
    <!--add CustomTabActivity-->
    <activity
        android:name="com.facebook.CustomTabActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="@string/fb_login_protocol_scheme" />
        </intent-filter>
    </activity>
    
    <!--reference your fb_app_id-->
    <meta-data
        android:name="com.facebook.sdk.ApplicationId"
        android:value="@string/fb_app_id"/>

    這邊的 @string/變數 會去讀在步驟 6 中設定的字串。

終於 Android 的部份也設定完成囉!

緊接著我們來設定 react-native-google-signin!

react-native-google-signin

  1. 安裝

    yarn add react-native-google-signin
  2. Link

    react-native link react-native-google-signin

iOS

  1. 打開 node_modules/react-native-google-signin/,找到 RNGoogleSignin.xcodeproj 檔案,將它複製到 Xcode 專案的 Libraries 目錄底下。2017-05-29 9 48 13

  2. 切換至 Build Phases 頁籤,找到 Link Binary With Libraries 展開它,點擊 + 並加入 libRNGoogleSignin.a、AddressBook.framework、SafariServices.framework、SystemConfiguration.framework 及 libz.tbd。

  3. 一樣在 node_modules/react-native-google-signin/ 目錄下找到 ios/GoogleSdk/,將它複製到 Xcode 專案下。(在跳出的視窗中請勾選 Copy items if needed,以及確定 Added folders 為 Create groups)

    2017-05-23 10 45 29

    2017-05-24 11 13 16

  4. 接著在 Link Binary With Libraries,一樣點擊 + 加入 GoogleSdk 的檔案(若沒有自動被加入的話),完成如圖:

    image

  5. 連至 Add Google Services | Google Developers,往下捲動找到 Get a configuration file 按鈕,點擊它。

  6. 設定 Google 應用程式。

    2017-05-23 11 16 15

  7. 啟用 Google Sign-in 功能,請依據實際情況填入 build ID。

    2017-05-23 11 17 13

  8. 最後就可以下載 GoogleService-Info.plist

    2017-05-23 11 17 53

  9. 打開剛下載的 GoogleService-Info.plist,找到 REVERSED_CLIENT_ID 與 bundle id 的值,在 Info 頁籤最下方的 URL Types 新增兩組分別填入:

    2017-05-23 11 23 43

  10. 接著編輯 ios/專案名稱/AppDelegate.m,在 #import "AppDelegate.h" 之後加入(如果發現已經在 link 階段加入過的話,請略過此步驟):

    #import <RNGoogleSignin/RNGoogleSignin.h>
  11. 然後在 react-native-facebook-signin 的步驟中曾經在下方加入過 openURL 的 function,請將最後 return 的那行改為:

    return [[FBSDKApplicationDelegate sharedInstance] application:application
                                                          openURL:url
                                                sourceApplication:sourceApplication
                                                       annotation:annotation
           ]
           || [RNGoogleSignin application:application
                                  openURL:url
                        sourceApplication:sourceApplication
                               annotation:annotation
              ];

再來換 Android 的設定。

Android

  1. 請確定 Android SDK 已經安裝這些套件:image

  2. 打開 Start Integrating Google Sign-In into Your Android App,往下捲動找到 Get a configuration file,點擊前往 Google 應用程式設定。

  3. 類似 iOS 的步驟,請依據實際情況選擇應用程式名稱、填入 package name。

    2017-05-23 11 28 42

  4. 填入 Android 簽名憑證的 SHA-1:

    2017-05-23 11 31 57

    若不知道怎麼找到這串碼的話,請參考 Authenticating Your Client。其中簽名金鑰的位置請依據實際情況填入。

  5. 最後就可以下載 google-services.json 了,請將它放到專案目錄底下的 android/app/ 目錄中。

    2017-05-23 11 32 21

  6. 編輯 android/settings.gradle,檢查是否有下方的 code,若無請加入。

    include ':react-native-google-signin', ':app'
    project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android')
  7. 編輯 android/build.gradle,在 dependencies 區塊中加入:

    classpath 'com.google.gms:google-services:3.0.0'
  8. 編輯 android/app/build.gradle,請確認是否有下方的 code:在 dependencies 區塊中:

    compile(project(":react-native-google-signin")){   // 若已有此行,但沒有 exclude group 請自行加入
        exclude group: "com.google.android.gms"  // 務必加入
    }
    compile 'com.google.android.gms:play-services-auth:10.0.1' // 至少需要 9.0.0 版本

    在 dependencies 區塊後,整個檔案最後一行加入:

    apply plugin: 'com.google.gms.google-services'
  9. 編輯 MainApplication.java,檢查是否有以下 code:

    import co.apptailor.googlesignin.RNGoogleSigninPackage;

    然後在 new MainReactPackage(), 下方是否有:

    new RNGoogleSigninPackage()
  10. 確認 android/gradle/wrapper/gradle-wrapper.properties 中是否有此行:

    distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip

這部份的設定大概就到這邊結束了。

繼續未完的 Amazon Cognito 設定

在設定 react-native-facebook-login 與 react-native-google-signin 的過程中,我們一併把 Facebook & Google 應用程式設定好了,現在我們回過頭來設定之前未設定完畢的部份。

  1. 先來看 OpenID 的部份,切到 OpenID 頁籤會發現什麼也沒有:2017-05-24 12 06 20

    是說 OpenID Provider 必須在 Amazon IAM 中進行設定。

  2. IAM 是 Identity and Access Management 的縮寫,進到服務首頁後點擊 開始使用 Amazon IAM。

  3. 在左方選項中點選 Identity providers,再點選 Create Provider。

    2017-05-24 12 03 49

  4. Provider Type 選擇 OpenID Connect,Provider URL 填入 https://accounts.google.com,最後 Audience 的部份,請打開 Google Developers Console,切換至相應的應用程式,並在 憑證 頁下方找到 OAuth 2.0 用戶端 ID,這邊請先加任何一組,之後修改設定請通通加到 Audience。

    2017-05-24 12 08 01

    2017-05-24 12 08 01

  5. 儲存之後,回到 Cognito 設定頁面重整,可以發現 OpenID 頁籤有 acctouns.google.com 了,勾選它。

  6. 切到 Facebook 頁籤,填入 Facebook App ID:

    2017-05-24 12 02 12

  7. 點 Create pool 進到下一步,保持預設值即可,點 Allow。

  8. Cognito 設定完成,不過 SDK 我們先前已經設定完畢,這邊就不用再做一次了。

    image

  9. 回到 IAM 編輯剛剛的 accounts.google.com,把所有 audience 加入。


前端程式

這邊的前端程式是指 React Native 的程式碼,本文同時會用它進行 iOS 與 Android 的實作。

主要流程可參考 https://github.com/awslabs/aws-sdk-react-native/tree/master/Core/example 這邊的範例程式碼。

以下分段介紹:

  1. 先準備好基本 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;

    註:本例中我們不打算用套件提供的按鈕。

  2. 再來新開一個檔案 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';
  3. 接著宣告幾個常數,比較好的作法是將它們放在環境變數中,或是透過 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,這邊先定義好對應的名稱,之後會用到。

  4. 宣告其他變數

    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;
    });
  5. 接著我們要定義幾個主要的 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);
      }
    }
  6. 再來定義 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 了。

  7. 再來分別實作 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;
    }
  8. 再來是登出相關的 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();
      });
    }
  9. 最後將這些 functions export 出去,以便呼叫:

    export {
      init,
      loginFB,
      loginGoogle,
      logout,
    };
  10. 好了,回到我們的主程式,加上以下部分:將剛剛寫好的程式 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>}
  11. 大概就是這樣哦,實際登入登出看看吧!

疑難排解

如果沒有辦法順利地達成功能,這邊蒐集了一些可能遇到的錯誤與解法。

在進行 Android 按下 Google 登入按鈕後,一直噴 12501 錯誤

這在 react-native-google-signin 的 FAQ 中也有提到,可前往參閱。

請確認兩個部分都有設對:

  1. 請確認是使用 WebClient 的 ID。
  2. 請確認 signing key 是對的,請參考此篇。

在 Cognito 登入的那段 trace 到 Invalid login token. Incorrect token audience 的錯誤訊息

通常是 Google 登入時發生,請在 Amazon IAM 設定 accounts.google.com 的 audiences 加上對應的 client ID,看看是不是少加了 Web Client ID 呢?

用 Amazon Lambda 來做 Serverless API

以上已經成功實作了登入的部份,那以下繼續來介紹註冊\存放會員資料的 API 部份。

這次來用 Amazon Lambda 加上 API Gateway 來實作看看,嘗試看看 Serverless 的解決方案。

註:若有個人偏好與時間的話,也可以用自己慣用的語言\環境實作,至於此節跟下一節就可以跳過啦~

請開另一個 repository 進行開發。

  1. 為了方便,先安裝 lambda 的佈署工具 – claudia

    npm install -g claudia
  2. 另外,透過 npm/yarn 安裝一些會用到的套件:

    "aws-sdk": "^2.57.0",
    "axios": "^0.16.1",
    "jsonwebtoken": "^7.4.1",
    "jwk-to-pem": "^1.2.6"
  3. 這邊我們一樣用 JavaScript 撰寫,不過 Lambda 目前最新支援到 Node.js 6.10 ,很可惜的像是 async/await 之類的特性尚不支援。

export 出去的東西就會是 lambda 執行的 script 主體了,大概會像是這樣:

exports.handler = (event, context, callback) => {
  // your script
};
  1. 這次要做的 API 目的是註冊\取得會員資料的 API。那麼先理一下思路,我們的流程大概是這樣子的:a. 從 request body 中取得 accessToken 及 openIdToken(這 request 以本例而言就是在 App 點擊按鈕後發送過來的)
    b. 驗證並解開 openIdToken
    c. 自解開的 openIdToken 中得知 provider 類型,以及該 user 的 Identity ID
    d. 得知 provider 類型後,使用 accessToken 取回 user 的資料(name、email 等資訊)
    e. 存入\更新 Cognito 的 Identity pool 中,相應的 Identity ID 裡面的附加資料集(Datasets)中
    f. API 輸出成功與否,以及 user 的相關資料

  2. 好,就來實作吧!先建立 index.js:

    引入套件,並設置好相關的參數,以及初始化相關物件。

    const jwt = require('jsonwebtoken');
    const axios = require('axios');
    const jwkToPem = require('jwk-to-pem');
    const AWS = require('aws-sdk');
    
    const IDENTITY_POOLID = 'ap-northeast-1:4e86b831-da7f-47d5-8382-3d800cd28a25';
    const COGNITO_DATASET_NAME = 'userData';
    
    AWS.config.region = 'ap-northeast-1';
    
    const cognitoidentity = new AWS.CognitoIdentity();
    const cognitosync = new AWS.CognitoSync();
  3. 這次需實作下列 functions:

    • downloadKey – 下載 Cognito Identity JWT Public key,以便在後續解開 JWT 內容
    • validateToken – 驗證並解開 JWT 內容
    • retrieveProfile – 依據 provider,使用 access token 取回 user 的相關資料
    • listData – 列出目前該 Identity ID 存放的資料內容
    • addData – 更新該 Identity ID 的資料內容

    downloadKey:

    const downloadKey = () => {
      return new Promise((resolve, reject) => {
        const url = 'https://cognito-identity.amazonaws.com/.well-known/jwks_uri';
        // Download the JWKs and save it as PEM
        axios.get(url).then(response => {
          if (response.status === 200) {
        const pems = {};
    
        const keys = response.data.keys;
    
        for (let i = 0; i < keys.length; i++) {
          // Convert each key to PEM
          const key_id = keys[i].kid;
          const modulus = keys[i].n;
          const exponent = keys[i].e;
          const key_type = keys[i].kty;
          const jwk = {
            kty: key_type,
            n: modulus,
            e: exponent,
          };
          const pem = jwkToPem(jwk);
    
          pems[key_id] = pem;
        }
    
        resolve(pems);
          }
        }).catch(error => {
          console.log(error, 'error');
          reject(error);
        });
      });
    };

    validateToken:

    const validateToken = (pems, event, context) => {
      return new Promise((resolve, reject) => {
        const iss = 'https://cognito-identity.amazonaws.com';
        const token = JSON.parse(event.body).openIdToken;
    
        // Fail if the token is not jwt
        const decodedJwt = jwt.decode(token, { complete: true });
    
        console.log(decodedJwt, 'jwt');
    
        if (!decodedJwt) {
          reject('Not a valid JWT token');
    
          return;
        }
    
        // Fail if token is not from your User Pool
        if (decodedJwt.payload.iss !== iss) {
          reject('invalid issuer');
    
          return;
        }
    
        // Get the kid from the token and retrieve corresponding PEM
        const kid = decodedJwt.header.kid;
        const pem = pems[kid];
    
        if (!pem) {
          reject('Invalid access token');
    
          return;
        }
    
        // Verify the signature of the JWT token to ensure it's really coming from your User Pool
        jwt.verify(token, pem, { issuer: iss }, (err, payload) => {
          if (err) {
        reject('Unauthorized');
    
        return;
          } else {
        const principalId = payload.sub;
        const provider = payload.amr && payload.amr[1];
        const accessToken = JSON.parse(event.body).accessToken;
    
        console.log(payload, 'payload');
        console.log(accessToken, 'accessToken');
    
        if (!principalId || !provider || !accessToken) {
          reject('Wrong token or no access token.');
        } else {
          // Using access token to retrieve user profile and save it with identityId in aws cognito
          resolve({ principalId, provider, accessToken });
        }
    
        return;
          }
        });
      });
    };

    retrieveProfile:

    const retrieveProfile = (provider, accessToken) => {
      console.log(provider, accessToken);
    
      return new Promise((resolve, reject) => {
        if (provider === 'accounts.google.com') {
          axios.get(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${accessToken}`)
        .then(response => {
          if (response.status === 200) {
            const profile = response.data;
    
            resolve({
              name: profile.name,
              email: profile.email,
            });
          } else {
            reject(response.status);
          }
        })
        .catch(error => {
          reject(`Retrieve google user data failed! ${error}`);
        });
        } else if (provider === 'graph.facebook.com') {
          axios.get(`https://graph.facebook.com/v2.9/me?fields=id%2Cname%2Cbirthday%2Cgender&access_token=${accessToken}`)
        .then(response => {
          if (response.status === 200) {
            const profile = response.data;
    
            resolve({
              name: profile.name,
              facebookId: profile.id,
              birthday: profile.birthday,
              gender: profile.gender,
              email: profile.email,
            });
          } else {
            reject(response.status);
          }
        })
        .catch(error => {
          reject('error');
        });
        } else {
          reject('Unknown provider');
        }
      });
    };

    listData:

    const listData = ({ identityId, profile }) => {
      return new Promise(resolve => {
        if (!identityId) {
          resolve({
        success: false
          });
    
          return;
        }
    
        cognitosync.listRecords({
          DatasetName: COGNITO_DATASET_NAME,
          IdentityId: identityId,
          IdentityPoolId: IDENTITY_POOLID
        }, (err, res) => {
          if (err) {
        console.log('err', err);
        resolve({ success: false });
    
        return;
          }
    
          console.log('res', res);
          console.log('parsedData', parseData(res.Records));
    
          resolve({ data: res, identityId, profile });
        });
      });
    };

    addData:

    const addData = ({ data, identityId, profile }) => {
      return new Promise(resolve => {
        if (!identityId) {
          resolve({
        success: false,
          });
    
          return;
        }
    
        const params = {
          DatasetName: COGNITO_DATASET_NAME,
          IdentityId: identityId,
          IdentityPoolId: IDENTITY_POOLID,
          SyncSessionToken: data.SyncSessionToken,
          RecordPatches: [{
        Key: 'facebookId',
        Op: 'replace',
        SyncCount: data.DatasetSyncCount,
        Value: profile.id,
          }, {
        Key: 'name',
        Op: 'replace',
        SyncCount: data.DatasetSyncCount,
        Value: profile.name,
          }, {
        Key: 'gender',
        Op: 'replace',
        SyncCount: data.DatasetSyncCount,
        Value: profile.gender,
          }, {
        Key: 'email',
        Op: 'replace',
        SyncCount: data.DatasetSyncCount,
        Value: profile.email,
          }, {
        Key: 'birthday',
        Op: 'replace',
        SyncCount: data.DatasetSyncCount,
        Value: profile.birthday,
          }],
        };
    
        cognitosync.updateRecords(params, (err, data) => {
          if (err) {
        resolve({ success: false });
    
        return;
          }
    
          resolve({
        userData: parseData(data.Records),
        success: true,
        identityId,
          });
        });
      });
    }

    最後還有一個 utils function,幫助我們把個人資料物件轉成資料集的格式:

    parseData:

    const parseData = (rec) => {
      const obj = {};
    
      rec.forEach(r => {
        obj[r.Key] = r.Value;
      });
    
      return obj;
    };
  4. 至於程式進入點的判斷邏輯如下:

    exports.handler = (event, context) => {
      const query = event || {};
      const bodyData = JSON.parse(event.body);
    
      if (!bodyData.accessToken || !bodyData.openIdToken) {
        const result = {
          statusCode: 500,
          headers: {},
          body: JSON.stringify({ error: 'Please input accessToken & openIdToken completely.' }),
        };
    
        context.fail(result);
      }
    
      // Download Cognito's JWT first
      downloadKey().then(pems => {
        // Validate token
        validateToken(pems, event, context)
          .then(response => {
        const provider = response.provider;
        const accessToken = response.accessToken;
        const principalId = response.principalId;
    
        console.log(response, 'response');
    
        retrieveProfile(provider, accessToken)
          .then(profile => {
            console.log(profile);
    
            listData({
              identityId: principalId,
              profile,
            })
            .then(addData)
            .then(res => {
              const result = {
            statusCode: 200,
            headers: {},
            body: JSON.stringify(res),
              };
    
              context.succeed(result);
            });
          })
          .catch(error => {
            console.log('retrieve profile error', error);
    
            const result = {
              statusCode: 500,
              headers: {},
              body: JSON.stringify({ error }),
            };
    
            context.fail(result);
          });
          })
          .catch(error => {
        console.log('validate token error', error);
    
        const result = {
          statusCode: 401,
          headers: {},
          body: JSON.stringify({ error }),
        };
    
        context.fail(result);
          });
      }).catch(error => {
        console.log('download key error', error);
    
        const result = {
          statusCode: 500,
          headers: {},
          body: JSON.stringify({ error }),
        };
    
        context.fail(result);
      });
    };

這邊差不多就完成了程式主體,剩下就是 deploy 到 Amazon Lambda 上啦!

佈署前置作業:

  1. 先至 Amazon IAM,點選左邊的 Users,然後按藍色按鈕的 Add user 以新增一個 User。
  2. 輸入使用者名稱,並勾選 Programmatic access,按下 Next: Permissions。
  3. 三個項目中請選擇 Attach existing policies directly,然後在 policy 勾選 AWSLambdaFullAccess 與 IAMFullAccess,最後按下 Next: Review。
  4. Create User
  5. 建立完成後,點擊 Download .csv 按鈕以下載這個使用者的憑證資訊,因為之後無法再看到這個資訊,所以建議還是下載保存吧。
  6. 依據憑證資訊的 key id 與 secret access key,設定 AWS 的帳號憑證資訊:
    vim ~/.aws/credentials
    [default]
    aws_access_key_id=XXXXXXXXXXXXXXX
    aws_secret_access_key=XXXXXXXXXXXXXXXXXXXXXXX

前置作業完成後,開始佈署作業:

  1. 為求方便,定義幾個常用 scripts 在 packages.json 中:

    "scripts": {
      "lambda:create": "claudia create --name cognitoLoginExample --region ap-northeast-1 --handler index.handler",
      "lambda:update": "claudia update",
      "lambda:destroy": "claudia destroy"
    }

    其中 lambda:create 的部份就可以建立並佈署 code 到 lambda 上,其中的參數請參閱該 repo 的說明。

    更新的話透過 lambda:update 即可,至於刪除就是 lambda:destroy 囉。

  2. 執行 lambda:create 佈署完後,可以上到 Amazon Lambda,進入 Functions 頁面,剛剛建立的 function 就在右邊:

    image

  3. 點擊 function 名稱,並切換到 Configuration 頁籤,確認執行身分 (Existing role),待會會用到。以及 Advanced settings 中的 Timeout 秒數,建議可以調整預設的 3 秒,提升到 5 ~ 10 秒以免 API 太快就逾時。

  4. 回到 IAM 頁面,切換至 Roles 頁面,找到剛剛的執行身分,要新增一些權限給它。Managed Policies 的部份請新增 AWSLambdaFullAccess 與 AmazonCognitoDeveloperAuthenticatedIdentities。

  5. Inline Policies 的部份,請新增 cognito-identity:* 與 cognito-sync:*。

    {
        "Version": "2012-10-17",
        "Statement": [
        {
            "Sid": "Stmt1495873848000",
            "Effect": "Allow",
            "Action": [
            "cognito-sync:*",
            "cognito-identity:*"
            ],
            "Resource": [
            "*"
            ]
        }
        ]
    }
  6. 再切回 Lambda 的頁面,function 上方有 Test 的藍色按鈕,我們可以試著填入實際 accessToken 與 openIdToken 的值先來測試看看:因為是讀取 request body 的值,所以要放在 body 中,值也是經過 JSON.stringify 的。

    image

    {
        "body": "{\"accessToken\":\"eyXXXXXXXXXXXX.eyXXXXXXXXXXXXX.XXXXXXXXXXXX\",\"openIdToken\":\"eyXXXXXXXXXXXXXX\"}"
    }
  7. 無論成功或失敗,都可以到 CloudWatch 去看 log 檔。

  8. 覺得這樣切換視窗實測很煩人的話,推薦安裝 lambda-local,這樣就可以在本地端快速實測了。

Amazon API Gateway

是說 Lambda 設好之後,還是無法讓外界直接訪問,那麼就要透過 Amazon API Gateway 了。(註:也有透過 Cognito 直接觸發的方式,不過本文不討論這塊)

  1. 從主控台進入 API Gateway 之後,請選擇 New API,並起個名字。

  2. 建立完成後,可看到這樣的畫面:

    image

  3. 先建立 Resouce。Resouce 就當做是資料夾路徑吧,例如建立 /register 的路徑這樣。點擊 Actions > Create Resouce,然後輸入 Resouce 的名稱之後按 Create Resouce 藍色按鈕。

    image

  4. 接著建立 Method,在 Actions 中選擇 Create Method。這邊建立的是 POST。觸發的類型是 Lambda Function,並請記得勾選 Use Lambda Proxy integration,這樣才會把 request 的 body 資料轉給 Lambda 使用。接著選擇正確的 Lambda Region 及 Lambda Function。

    image

  5. 再來要佈署才能真的給外面使用。一樣在 Actions,選擇 Deploy API,必須先建立環境。

    image

  6. 建立好之後,會得到一組可對外訪問的網址

    image

    在這組網址之後接上 Resouce 的名稱,再透過設定好的 Method 方式訪問(例如 POST),就能打到先前設定的 Lambda function 了。

    如果需要偵錯,可把 CloudWatch 相關的項目都勾選起來,並賦予相關的角色權限。請參閱此篇教學。

  7. 必須注意的是,傳入的 request 必須帶上 header Content-Type:application/json,然後是透過 request.body 傳輸這些資料的。至於輸出的格式也有規定,必須包含 statusCode、headers 與 body,不符合格式的話會直接噴 502 錯誤。

在 App 端補上註冊(register)的 function

上面忙了這麼一大段設定,為的就是讓 App 用戶在點擊登入按鈕後,可以將取得的身分資料存入 Identity ID 的 Datasets 中,並能夠在登入後取回並顯示使用者名稱。

  1. 先在 utils/auth.js 補上 register function:

    async function register(passedInToken) {
      const openIdToken = passedInToken || (await openIdTokenPromise);
      const payload = {
        accessToken: globalAccessToken,
        openIdToken,
      };
    
      try {
        const rsp = await fetch(`${剛剛的 API Gateway 網址}/register`, {
          method: 'POST',
          headers: {
        'Content-Type': 'application/json',
          },
          body: JSON.stringify(payload),
        });
    
        const json = await rsp.json();
    
        if (json.statusCode === 401) {
          const newToken = await refreshToken(); // 這邊礙於篇幅略過不提,請直接見 repo source code
    
          return await register(newToken);
        }
    
        return json.userData;
      } catch (e) {
        console.log('Error: ', e);
      }
    }

    並一樣請加入到 export 中:

    export {
      init,
      loginFB,
      loginGoogle,
      logout,
      register, // here
    };
  2. 在 app.js 中:補上初始化的 profile state:

    state = {
      loggedIn: false,
      isLoading: false,
      profile: {},
    };

    render 的部份補上使用者登入後的資訊(name):

    {loggedIn && <View style={styles.welcome}>
        <Text style={styles.welcomeText}>
          Hello {profile.name}!{"\n"}
          You're logged in!
        </Text>
    
        <TouchableOpacity onPress={() => this.handleLogout()} style={styles.logoutButton}>
          <Text style={styles.logoutButtonText}>Logout</Text>
        </TouchableOpacity>
      </View>}

    修改 handleLogin,加上 register 與設定 profile 的部份:

    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,
        });
      }
    
      if (result) {
        const profile = await AuthUtils.register();
    
        this.setState({
          loggedIn: true,
          isLoading: false,
          profile,
        });
      } else {
        this.setState({
          isLoading: false,
          profile: {},
        });
      }
    }

至此就完成這個 prototype 啦!附個完成圖:

image

Source Code

完整的程式碼請見我的 GitHub,只是概念驗證,code 寫的髒髒請不要見怪 😅😅😅

參考資料