使用 Authing + Lambda 替代 AWS Cognito

leinue發表於2019-04-29

Amazon Web Services(AWS) 雖然作為市場份額全球第一的雲端計算廠商,其產品也不是完美無缺的,Cognito (AWS 的身份認證解決方案)及其附帶的中文文件就是一個反面教材,其難用程度令人髮指。當然,除了不易用之外,還有訪問速度緩慢,不適用於中國市場等問題存在。

而國產的 Authing 可以解決使用 Cognito 的諸多問題,先看一下 Authing 的介紹:

Authing 是一個身份認證服務商,其提供了企業級身份認證和管理解決方案,客戶分佈教育、IoT、網際網路和電商等多個行業。

Lambda 是一個由 AWS 提供的 Function-as-a-Service (FaaS) 平臺 。Lambda 和 AWS 生態結合的非常緊密,接入 Lambda 後,開發者可以使用 AWS 生態內的所有資源。比如,我們可以建立一個 Lambda 函式,讓使用者通過 Cognito 登入(當然這篇文章是讓使用者使用 Authing 登入),然後再呼叫另外一個可以上傳檔案到 S3(AWS 的儲存服務) 的 Lambda 函式。

這類平臺(現在多被稱為 Serverless,無伺服器架構)的一個好處是可以讓開發者無需擔心基礎設施,專心業務研發。

FaaS 或者說 Serverless 平臺正在逐漸獲得市場關注,因為這種型別的平臺可以讓開發者不用再關注基礎設施。"What is serverless" 這篇文章詳細的講解了什麼是「無伺服器計算」和「無伺服器計算」的好處,推薦讀一下。

這篇文章的主要目的是介紹如何使用 Authing + Lambda 替代 AWS Cogito,點選這裡體驗最終 demo。

使用 Authing + Lambda 替代 AWS Cognito

此外,Authing 遵循 OIDC 規範,所以本篇文章將使用 OIDC 來做認證,如果你還不瞭解什麼是 OIDC,請檢視這篇文章

首先確認下使用者的操作流程

  1. 開啟頁面:sample.authing.cn/aws/
  2. 點選 Login 進行登入,此時跳轉到 Authing 的登入頁面(應用的二級域名);
  3. 輸入賬號密碼進行登入,若沒有賬號密碼則先進行註冊;
  4. 登入成功後返回第一步開啟的頁面,並顯示登入使用者的頭像;
  5. 此時使用者可以看到從 AWS Lambda 請求回來的 Private 資訊;

最終效果如下圖所示:

使用 Authing + Lambda 替代 AWS Cognito

建立一個 Authing 應用

如果你還沒有註冊 Authing,那麼請點選這裡進行註冊,註冊完成後,按以下步驟建立一個 Authing 應用。

使用 Authing + Lambda 替代 AWS Cognito
1. 建立應用
使用 Authing + Lambda 替代 AWS Cognito
2. 填寫基本資訊,應用型別選擇 Web 應用
使用 Authing + Lambda 替代 AWS Cognito
3. 建立完成後會進入到應用主頁(空空如也)

建立 OIDC 應用

建立完應用後相當於你有了一個使用者池,接下來你可以建立 OIDC 應用來授權其他程式(你自己寫的或其他第三方程式)訪問你的使用者池。

如果你還不清楚什麼是 OIDC,請參考這篇文章

使用 Authing + Lambda 替代 AWS Cognito
4. 點選「第三方登入」開始建立 OIDC 應用
使用 Authing + Lambda 替代 AWS Cognito
5. 選擇「OIDC 應用」選項卡,並點選「建立 OIDC 應用」
使用 Authing + Lambda 替代 AWS Cognito
6. 填寫應用名和認證地址,並勾選 id_token token

這裡要說明一下,建立 OIDC 應用時的認證地址將由 Authing 生成一個二級域名(支援 HTTPS),且不能重複,回撥 URL 填寫你自己的回撥地址即可,在這裡我用的是 authing.cn,注意,OIDC 協議中不允許回撥 URL 為 localhost,請使用代理工具進行除錯。

使用 Authing + Lambda 替代 AWS Cognito
7. 點選確認,就可以看到我們有了第一個基於 OIDC 協議的授權應用

建立完成後你可以訪問 lambda.authing.cn ,此時會看到報了一個錯,別害怕,這是因為我們發起的授權連結不正確。

使用 Authing + Lambda 替代 AWS Cognito
8. 訪問 lambda.authing.cn 時報的錯

發起正確授權請求的方式請繼續往下看。

發起授權請求

和絕大多數的 OAuth 應用差不多,OIDC 的授權連結也需要拼接(如果你開發過微信應用,應該會很容易理解),Authing OIDC 應用的授權連結符合標準規範,具體格式為:

lambda.authing.cn/oauth/oidc/… profile&response_type=<OIDC 模式,分為好幾種>&state=<一個隨機字串,用來防範 CSRF 攻擊>

若需要檢視詳細的引數,請點選這裡檢視。

例如:

lambda.authing.cn/oauth/oidc/…

為了簡單起見,這裡我們的 response_type 設定為「id_token token」,這樣不需要使用「code」換取 token,token 會直接附帶到回撥地址中。

使用 Authing + Lambda 替代 AWS Cognito
9. 如果你的授權連結正確,應該可以看到上圖這樣的登入視窗

如果你的授權連結正確,應該可以看到上圖這樣的登入視窗,同時這個視窗也是你的終端使用者所使用的視窗,他們都將從這裡登入然後回撥到你配置好的回撥 URL 中。

你可以試著註冊一個賬號然後進行登入,登入完成後可以在控制檯中觀察到登入狀況。

使用 Authing + Lambda 替代 AWS Cognito
10. 註冊成功
使用 Authing + Lambda 替代 AWS Cognito
11. 登入之後的授權頁面
使用 Authing + Lambda 替代 AWS Cognito
12. 控制檯中觀察到的使用者資料

在你登入成功後應該會看到回撥到了你填寫 URL 中,並且附帶了很多引數,接下來我們會闡述如何使用這些引數。

獲取使用者資訊

回撥到在控制檯中配置的 redirect_uri 中後,將附帶以下資訊:

{
	"id_token": "JWT_TOKEN",
	"access_token": "JWT_TOKEN",
	"expires_in": "3600",
	"token_type": "Bearer",
	"state": "jacket",
	"session_state": "644d7b324ba61d517fdedd28b5b6e365d78f2a8178f2ee742474d5b57a99eb3f"
}
複製程式碼

可以看到其中包含了 access_token id_token,其中 access_token 可以幫助你從 Authing 後端獲取使用者資訊,而 id_token 中包含了基本的資訊,如果你要獲取使用者的頭像,那麼是需要通過 access_token 獲取的。

我們先看一個 id_token 的例子:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJzdWIiOiI1Y2MyYTg1MTFiYmFmMDRmOTNjZTQ4OWYiLCJub25jZSI6IjE4MzEyODkiLCJzaWQiOiI5MzkwZDA1ZC01ZTM3LTQ3ZWUtODJjNi1jNTQ1ZjA2ODhhMDAiLCJhdF9oYXNoIjoiNmxZMGRXajZYUTY0aExWdHAtR2tEdyIsInNfaGFzaCI6IlZVOU5QYV9JQ0VTSEdxRmxUZ3A2LUEiLCJhdWQiOiI1Y2MyYjU0OGQxNGM3NDJkYjg5M2JhNTUiLCJleHAiOjE1NTYzNjY0ODksImlhdCI6MTU1NjM2Mjg4OSwiaXNzIjoiaHR0cHM6Ly9vYXV0aC5hdXRoaW5nLmNuL29hdXRoL29pZGMifQ.Qc_OMqMf6_wwzW2SsEgEtiaGr3ZY1FWHnRrMU2M7LADGlNpq_pvPrFxAVsR2j-BFr1y48M-Trvq6yAu4_ZOUBHPtIIpoQ5W2bnABytUV693ZcwNlf9CCiLc-k0LG3o1U-BmiH3L6NAV7aKGsfVHS8toiNbVDuimPVdYJsRrF2C1jj1meM1K8FBVwqozXm6YtB--u3sqY4IszHnd5PMEWguLsOkpZJIh7xWeYPpVQ5WKfx0cA8rB_T2puSCbeaUVhgIwNADy06qBqXhUOiA4gdcNbHtx7tvGZMxzMC3rdjpXoZk89Duh3O5tHlMtaBlidJGYavUSjVl7potESecSlBg

使用 jwt.io 解析後將得到如下結果:

{
  "sub": "5cc2a8511bbaf04f93ce489f",
  "nonce": "1831289",
  "sid": "9390d05d-5e37-47ee-82c6-c545f0688a00",
  "at_hash": "6lY0dWj6XQ64hLVtp-GkDw",
  "s_hash": "VU9NPa_ICESHGqFlTgp6-A",
  "aud": "5cc2b548d14c742db893ba55",
  "exp": 1556366489,
  "iat": 1556362889,
  "iss": "https://oauth.authing.cn/oauth/oidc"
}
複製程式碼

其中包含了簽發時間(iat)、過期時間(exp)等欄位,可以用來判斷使用者有沒有被認證過,在 OIDC 的規範中,JWT 使用 OIDC 應用的 secret 簽發,需要開發者在後端驗證(這一步我們將會在 Lambda 中執行)後繼續執行開發者本身的業務流程。

再來看看 access_token 的例子:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJqdGkiOiJza0p-bTNaYmZsTjVxVGEzR2J2YlMiLCJzdWIiOiI1Y2MyYTg1MTFiYmFmMDRmOTNjZTQ4OWYiLCJpc3MiOiJodHRwczovL29hdXRoLmF1dGhpbmcuY24vb2F1dGgvb2lkYyIsImlhdCI6MTU1NjM2Mjg4OSwiZXhwIjoxNTU2MzY2NDg5LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIiwiYXVkIjoiNWNjMmI1NDhkMTRjNzQyZGI4OTNiYTU1In0.Uf3YK4D9HL-G71hkA4cWt5kitDo5rNgwVA9Vqlv4RjAILNDTylYWtkacKJpLcOSS81ivaNpDVNYYzBSoyN-eMH80VhArPUre74F9SHdonA-IVFVPT0DHRtOAJI9kqDW4tgTXhZeZMUm-MCjVjR-q8XrayXaqrC5Hu5W3D1N-K_jZOlwxzIBf51nuC4NMvSI_wPpYj2WPzGxFwpfTCEbnhj5RO0CcThRpC3EdmpbtcJqStd7AZQhkLyTb1TQLHJOel8DSxLnLnoIU0rZXsodK6EjE_oqRLagetNXF1cKfRmnGFaAKZKqgvHc527S_CVkgXIwcHBRmDeqo93CCId_hmQ

使用 jwt.io 解析後將得到如下結果:

{
  "jti": "skJ~m3ZbflN5qTa3GbvbS",
  "sub": "5cc2a8511bbaf04f93ce489f",
  "iss": "https://oauth.authing.cn/oauth/oidc",
  "iat": 1556362889,
  "exp": 1556366489,
  "scope": "openid profile",
  "aud": "5cc2b548d14c742db893ba55"
}
複製程式碼

可以看到 access_token 相比 id_token 是少了很多資訊的,這裡有一段英文的介紹,該介紹講解了 access_token 和 id_token 的區別:

ID Tokens vs Access Tokens. The ID Token is a security token granted by the OpenID Provider that contains information about an End-User. This information tells your client application that the user is authenticated, and can also give you information like their username or locale.You can pass an ID Token around different components of your client, and these components can use the ID Token to confirm that the user is authenticated and also to retrieve information about them.Access tokens, on the other hand, are not intended to carry information about the user. They simply allow access to certain defined server resources. More discussion about when to use access tokens can be found in Validating Access Tokens.

簡單來講,id_token 告訴你使用者被驗證過了,而 access_token 是一個你可以訪問資源伺服器(這裡就是 Authing) 的一個憑證。

同時也可以看到,idtoken 包含的資訊較少,如果想獲取更多資訊,需要使用 access_token 來獲取。獲取方式也非常簡單,只需要往以下連結傳送 GET 請求並且附帶 access_token 即可,如:

$ curl users.authing.cn/oauth/oidc/…

可以獲取到 id 等資訊,獲取到 id 之後你可以將 id 儲存到你自己的資料庫中以完成自己的實際業務。

{
    "sub":"5cc2a8511bbaf04f93ce489f",
    "nickname":"",
    "picture":"https://usercontents.authing.cn/authing-avatar.png"
}
複製程式碼

上面的 JSON 是一個使用 access_token 換取使用者資料後的返回結果。

好了,現在我們已經獲取到 Token 了,接下來我們需要在 Lambda 中驗證這個 Token 的合法性並在前端顯示不同的資訊。

編寫 Lambda 函式

編寫 Lambda 函式推薦使用 Serverless 這個 CLI,AWS 控制檯中的函式編寫堪稱讓人痛不欲生。

同時,你可以到這裡檢視完整程式碼。

Lambda 在這篇文章中主要用來做三件事:

  1. 對 id_token 進行認證,以獲取使用者是否被認證過;
  2. 提供一個 Public API,此 API 可以直接被訪問;
  3. 提供一個 Private API,此 API 需要經過認證後被訪問;

對 id_token 進行認證

認證 id_token 首先需要知道 OIDC 應用的 secret,此值可以在 Authing 控制檯檢視 OIDC 應用的詳情中找到:

使用 Authing + Lambda 替代 AWS Cognito
請務必保管好此值,避免向任何人洩漏

id_token 在簽發時的簽名是此 secret ,因此在 JavaScript 中可以直接使用 jsonwebtoken 這個庫來驗證 id_token 的合法性(詳情請參考:驗證 Token 合法性)。

在控制檯中安裝 jsonwebtoken:

$ npm install jsonwebtoken --save複製程式碼

P.S. 在 lambda 中引入包後會一起打包上傳到 AWS Lambda 執行時中。

const jwt = require('jsonwebtoken');

// Policy helper function 
// 這是 AWS 提供的模版程式碼,這裡不需要做修改
const generatePolicy = (principalId, effect, resource) => {
  const authResponse = {};
  authResponse.principalId = principalId;
  if (effect && resource) {
    const policyDocument = {};
    policyDocument.Version = '2012-10-17';
    policyDocument.Statement = [];
    const statementOne = {};
    statementOne.Action = 'execute-api:Invoke';
    statementOne.Effect = effect;
    statementOne.Resource = resource;
    policyDocument.Statement[0] = statementOne;
    authResponse.policyDocument = policyDocument;
  }
  return authResponse;
};

// Reusable Authorizer function, set on `authorizer` field in serverless.yml
module.exports.auth = async (event, context, cb) => {
  if (event.authorizationToken) {
    // remove "bearer " from token
    const token = event.authorizationToken.substring(7);

    try {
        let decoded = jwt.verify(token, 'YOUR_OIDC_APP_SECRET'),
          expired = (Date.parse(new Date()) / 1000) > decoded.exp;
        if (expired) {
          cb('Unauthorized, Login information has expired.');
        }else {
          cb(null, generatePolicy('user', 'Allow', event.methodArn));
        }
      } catch (error) {
        cb('Unauthorized');
      }
  } else {
    cb('Unauthorized');
  }
};
複製程式碼

公共 API

// Public API
module.exports.publicEndpoint = (event, context, cb) => {
  cb(null, { message: 'Welcome to our Public API!' });
};
複製程式碼

私有 API

// Private API
module.exports.privateEndpoint = (event, context, cb) => {
  cb(null, { message: 'Only logged in users can see this' });
};
複製程式碼

serverless.yml

service: serverless-authorizer

provider: 
 name: aws
 runtime: nodejs8.10

functions:
  auth:
    handler: handler.auth
  getUserInfo:
    handler: handler.getUserInfo
    events:
      - http:
          path: api/userInfo
          method: get
          integration: lambda
          cors: true    
  publicEndpoint:
    handler: handler.publicEndpoint
    events:
      - http:
          path: api/public
          method: get
          integration: lambda
          cors: true
  privateEndpoint:
    handler: handler.privateEndpoint
    events:
      - http:
          path: api/private
          method: get
          integration: lambda
          authorizer: auth # See custom authorizer docs here: http://bit.ly/2gXw9pO
          cors: true複製程式碼

此檔案可用來配置需要鑑權的路由,如上面程式碼中的 privateEndpoint,配置了 authorizer 為 auth 函式。

點選此處檢視完整程式碼。

測試 Lambda

寫完了程式碼之後我們需要進行測試。

Lambda 支援直接在本地測試,可以使用如下命令:

$ sls invoke local -f auth --data '{"authorizationToken": "Bearer <id_token>"}'複製程式碼

如果本地測試返回瞭如下資訊則表示驗證成功:

{
    "principalId": "user"
}複製程式碼

部署 Lambda

$ serverless deploy複製程式碼

部署完成後會得到三個連結,這三個連結分別是上述程式碼的三個函式。

使用 Authing + Lambda 替代 AWS Cognito
紅框中的路由是在 serverless.yml 中定義好的,可以直接對映到函式中

使用 curl 或 postman 將 OIDC 登入後的 id_token 攜帶到 header 的 Authorization 中即可檢視結果,如:

$ curl --header "Authorization: <id_token>" <endpoint>複製程式碼

上述三個路由的結果應該為:

curl <endpoint/dev/api/public> - Should work! Public!
curl <endpoint/dev/api/private> - Should not work
curl --header "Authorization: <id_token>" <endpoint/dev/api/private> - Should work! Authorized!複製程式碼

最後,在我們的前端補充上相關資訊,在點選登入後應該可以看到如下資訊:

使用 Authing + Lambda 替代 AWS Cognito

線上體驗地址:sample.authing.cn/aws/

Enjoy!

Authing.cn - 領先的身份認證雲

使用 Authing + Lambda 替代 AWS Cognito


相關文章