用 AWS Cognito 實作經典的登入服務
AWS Cognito 主要提供帳號管理與授權的服務。舉例來說,使用它可以簡單做出供一般使用者註冊帳號,登入帳號的功能。而你所提供的系統的使用者,可以進一步取得事先設定好的 IAM Role,正常地使用 AWS 服務。當你正在設計的服務,需要提供使用者管理的功能實,用 AWS Cognito 的 User Pool 就能夠做到,特別是當你沒有很在乎「介面」設計時,它也有預設的 Hosted UI 可以用在簡單的情境。
那麼要怎麼利用 AWS Cognito 做出需要的登入介面,或是相對應的後端管理功能呢?查看了 Amazon Cognito Developer Guide 的教學,它寫的其實有點難懂,太專注在介紹自己的功能,即使要想各種例子,會被導流到二個地方。一是要推廣的新功能 AWS Amplify framework,另一個是被封存的舊 Github 專案,例如:amazon-cognito-identity-js 被封存後,併入了 amplify-js。其他的範例則是散放在 AWS Samples 之中。這些筆記則是由幾個簡單的使用情境與「特別好奇」的部分集結而成的記錄。
PS. 在手冊上也有列出些常見使用情境,也許可以先參考一下有沒有需要的資料,但它只有描述概念,不太好懂就是了。
起手式:建立 User Pool 與 App Client
在開始動手實作前,請先依著手冊上的 Getting started with Amazon Cognito 與 Tutorial: Creating a user pool 體驗一下怎麼建立 User Pool 與加入新的 Client。
然後,建立完後你就會被導流至 Amplify 去,開始想著?蛤!我還要再多學一個東西啊?其實,在你建立好 App Client 後,就可以開工了寫前端的登入頁面了。但要注意一件事,建立 Client 時,你可以選擇要不要產生 Client Secret。
例如:我選了圖中的 Public client
,它會自動幫我選擇 Don't generate a client secret
,但這並不是強制的,你可以「手動」選回 Generate a client secret
。請不要這麼做,因為 Client SDK 並沒有地方讓你填 Client Secret,並且它也不該被 Client 知道:
除了給純前端使用的 App Client 不該產生 Client secret 之外,還得選好認證流程。以 Public client 來看,它預設選了這些:
一般使用的情境,且還不需要進入認證流程客製化前,我會建議將 ALLOW_CUSTOM_AUTH 換成 ALLOW_USER_PASSWORD_AUTH 會對剛學習的人較為直覺,因為就如同一般的使用習慣,使用者輸入好帳密,接著由系統判斷登入有沒有成功:
以上前置作業都備妥後,就可以開始來建立前端的專案來實作它囉!
建立前端專案
建立前端專案的重點會在於,我們倒底該引用哪個函式庫,讓我們可以與 AWS Cognito 服務互動,以達到我們想做的功能?有 3 個官方的函式庫可用:
先不打算使用包好包滿的 AWS Amplify 的情況下,又不想要摸超低階的 AWS SDK,合理的選擇會是 amazon-cognito-identity-js。(詳細的技術選擇思路,可參考這篇 Blog)
在前端專案直接用 npm 安裝後即可使用,在 npm 頁面也有許多的使用範例:
npm i amazon-cognito-identity-js
函式庫初始化
安裝完需要的函式庫後,需要先將它初始化後才可以使用。下列是初期可能會用到的類別:
對操作 User Pool 來說,我們得先建立 CognitoUserPool 物件,它需要的 User Pool Id 與 Client Id 你都可以在 Cognito Console 中查得到:
amazon-cognito-identity-js
替我們封裝了低階的 AWS SDK,透過 CognitoUserPool
提供對應的方法進行操作。
使用者註冊流程
在官方文件有有提到,使用者註冊之候需要進行帳號確認,而一個使用者可能的狀態可由此文件提供的狀態圖得知:
當我使用 SignUp API 註冊使用者時,Cognito 的 User Pool 就會建立好使用者的資料,這個動作讓使用者 (CognitoUser) 的狀態切換至 Registered
初始狀態,在這個狀態使用者沒有任何功能,直到使用者狀態變更為 Confirmed
之後,它才可以開始作為正常的 Account 使用。如狀態圖表示的,有 3 種途徑可以將 CognitoUser
狀態轉移至 Confirmed
:
- 使用 Lambda Trigger:Pre Sign-up
- 使用 AdminConfirmSignUp API
- 使用 ConfirmSignUp API
由上述的資訊可以約略感受到,AWS Cognito 提供 Lambda Trigger 讓開發者可以客製化各種流程,而 API 分為給 Admin 使用的版本與 Client 端使用的版本。Admin 的版本要填的參數較少,畢竟那是讓你製作管理功能用的,你不用取得使用者才能知道的資料,就可以強制變更狀態,而 Client 則需使用者個人知道的參數才可以使用。
以 AdminConfirmSignUp 為例,它的 Request 參數如下:
而 ConfirmSignUp 的 Request 參數如下:
同樣執行 Confirm 的功能,以 User 的需要有 ConfirmationCode
才可以執行,而這個資訊會透過簡訊(SMS) 或 Email 寄送,只有使用者本來才能收到。
接下來的記錄,我們會先著重在前端如何整合 AWS Cognito 的 User Pool,提供使用者基本的註冊與登入功能。
使用者註冊 (SignUp)
我們由典型的使用者註冊流程開始,讓使用者填寫他的 Email 與 Password。如同上面的示意圖,我們準備了這樣的表單讓使用者輸入。Sign Up 其實就呼叫 SignUp API 進行註冊,聽起來相當單純!由文件可知有 3 組必填的值:
- ClientId
- Username
- Password
- UserAttributes (optional)
除了必填的部分,需要特別指出 UserAttributes 有可能也是必填的,得看 User Pool 的設定來決定。點開 Console 後選擇 Sign-up experience
,其中的 Required attributes
是當初建立 User Pool 勾選的必填屬性 (目前只有 email):
請在呼叫 SignUp API 時一起使用才不會造成 Error 的結果,並且它是大小寫敏感的屬性。下面為使用範例。在我們的設計中,直接將 email 當作是 username 使用,所以我們將 username 填入 email 屬性內:
Error Handling
當新的使用者,輸入了帳號與密碼按下註冊鈕後,它可能會種種問題而被拒絕:
這時,你可以回頭看一下 Console 內的 Sign-in experience 設定,其中有一段是 Password policy,可以考慮明確地將規則在註冊時展示出來:
在我們的範例中,簡單地處理它:遇到 error 時,將 error message 顯示在 Sign In 表單的下方:
確認使用者
如同先前提到的,使用者建立後會是 Registered
的狀態,它必需要成為 Confirmed
的狀態才可以使用,在 signUp 之後我們必需接著執行確認使用者的流程:
確認使用者的流程,有 2 種常用的 API:
如同字面上的意思,前者用來傳送 Confirm Code 用的,後著則是在 Confirm Code 遺失或過期時,要求新的 Confirm Code 使用的。而 Confirm Code 的傳送型式預設有 2 種可以選擇,即為常見的 SMS 簡訊或 Email 寄發。Email 寄發不需要額外設定,我在建立 User Pool 時,只有勾選這個選項。
可以在 Email 中收到這樣的信件,它的 Template 可以在 Console 中修改,但並沒有支援 i18n 的功能。若需要支援 i18n 的情境,可以搭配 Custom message Lambda trigger 針對不同的 locale 發送不同語系的信件:
ConfirmSignup 的使用方式相當容易,對至 amazon-cognito-identity-js 就是 CognitoUser 的 confirmRegistration 方法:
當發生問題時,我們可以提供讓使用者發送新的 Confirm Code 的選項:
在 amazon-cognito-identity-js 就是呼叫 CognitoUser 的 resendConfirmationCode 方法:
使用者登入 (SignIn)
使用者登入要填的資料跟註冊的必需資料是大同小異的,但要注意的是使用 amazon-cognito-identity-js
的情況。若你事先查過 API 手冊,會發現並沒有 SignIn 為名的相關 API,而是:
- InitiateAuth
- AdminInitiateAuth (後端製作管理功能用的)
基於先前的討論,我們並不會直接讓前端使用 Admin*
系列的 API,所以很直覺地會在 amazon-cognito-identity-js
找到 CognitoUser 有個 initiateAuth
方法,但可惜他並不是我們所想的直接與 Cognito API 對應的 InitiateAuth。要實作非客製化登入流程的情況,都該這麼寫:
原因是 initiateAuth 中的 flow type 固定是 CUSTOM_AUTH,沒有任何地方可以修改它,看起來最初在包裝這個方法的人,就是只想讓它負責客製化登入流程的部分:
Error Handling
在登入的錯誤處理部分,我們就不特別去談論常見的帳號或密碼輸入錯誤的問題。這裡要特別指出的是,使用者可能註冊後忘了進行使用者確認,這種情況要特別提示使用者應該進行使用者確認。在登入的 Callback 中,我們可以利用 onFailure 的 UserNotConfirmedException
來判斷,也可以用 onSuccess 的 userConfirmationNecessary
判斷:
我懷疑其中的 userConfirmationNecessary
可能是 legacy 的設計,目前看起來未確認的使用者,並沒有機會登入成功,它也許是過去使用的判斷方法。
取得目前使用者
使用者登入成功後,由 API 回傳的資料會被 cache 至 Browser 的 local storage。我們能使用 User Pool 的方法取得目前的使用者:
不過,這樣只是單純拿出了 local storage 內的部分資料填上,實際上我們還必需去驗證這個 Session 是不是還可用,特別是若你想要跟後端互動時,使用 JwtToken 是最方便的,換言之,我們希望這樣取得 JwtToken:
user.getSignInUserSession()?.getIdToken().getJwtToken()
但是 getCurrentUser() 並不會去載入 Session 資料,得額外呼叫 getSession(…) 才會載入。因此,合理的取得當前使用者的方法應為:
重新包裝的 currentUser
就可以正常使用 Session 資料,後續就是將 JwtToken 在後續的 API 呼叫時,送給後端去驗證權限的事情。
由 USER_PASSWORD_AUTH 畢業
在最開頭我們使用了「典型」的註冊與登入,而選用了 USER_PASSWORD_AUTH
的認證流程,但實際上我們可以簡單地將它換成 USER_SRP_AUTH
,因為 amazon-cognito-identity-js
把該做的事都封裝進了 CognitoUser 內的 authenticateUser 方法:
這樣修改之後,程式依然可以正常運作。那它實際上有差別嗎?原先的 USER_PASSWORD_AUTH 會將你填寫的密碼送至 API 讓它進行驗證,它在 InitiateAuth 的 HTTP POST 的 Body 結構如下:
而 USER_SRP_AUTH
則不會傳送你的密碼,它會呼叫 2 次 API:
第 1 次是 InitiateAuth,它的 HTTP POST 的 Body 內容如下:
得到回應:
過程中,透過 SRP_A 與 SRP_B 二個數字來驗證密碼是否正確,但不用真的將密碼本人傳送出去。收到了 InitiateAuth 結果後,必需使用 RespondToAuthChallenge 回應:
若 RespondToAuthChallenge 的結果被認可,那就可以取得使用者的認證結果囉!
結語
最近有著需要實作註冊功能的需求而開始研究 AWS Cognito,但跟著簡單的教學做完了之後,發現 Hosted UI 醜到慘不忍睹。連前端苦手的我,都有意識到它實在不可能在 Production Level 中使用,於是開始研究著怎麼去整合 AWS Cognito,但又不想要直接使用 AWS Amplify Framework 讓自己掉入另一種不好客製化的窘境。
靠著網路上的討論,看著前人留下的分享,有的是後端整合,有的是客製化各種流程,整體來說知識的材料相當碎片化。於是,決定了替自己濃縮一份純前端整合的版本。這篇焦點放在實作經典的帳密與密碼的認證功能,即使它是已經過時的實作但卻是多數人可以理解的情況。後續有時間,會再希望能整理一下目前潮流的 Passwordless 認證流程。
另外,在研究 AWS Cognito 的過程中,關於它支援的第三方登入方式不夠多,例如 GitHub 登入並不能直接支援。在我的觀點來看,雖然他不能直接支援,也有網路上的 Open Source 專案將 GitHub Application 轉成符合 OIDC 規格讓 AWS Cognito 串接。其實,這就只是不去自己寫後端的意思一樣,若可以實作簡單的後端,加上 Admin*
的 API 可以使用,直接把資料透過 OAuth Callback 登進 Cognito User Pool 就行了,沒有什麼真的無法做到的事。
相關的範例可至 GitHub 上查看,並依著 README.md 中的說明設定專案。文章中提到的範例碼集中在 SignForm.tsx。
廣告插播:JCConf Taiwan 2022 志工招募中
報名表單:https://forms.gle/oak2PVzihqsEe9946
支援日期
行前大會:2022/10/02 (日) 下午 — 暫定
場地佈置:2022/10/06 (四) 晚上
大會日期:2022/10/07 (五)