如何 20 分鐘部署靜態網站,並使用 OAuth 2.0 授權、CloudFront CDN 加速?

(圖說:Grilled Halloumi Cheese 下午茶,是的,故意圖文不符 XDD。圖片來源:Ernest 攝於倫敦。)

0. 起因

這個月 Pahud 邀約了一群 AWS Hero 與 AWS Builders 一起用 AWS CDK 來玩 CloudFront Extensions (CloudFront Lambda@Edge)。我從自己長長的代辦清單中找了個一直很想做、但擇期不如撞日的題目 OAuth 2.0,來嘗試看看如何實作成一個 CloudFront Extension 方案,並且採用 AWS CDK (Cloud Development Kit) 搭配 .env 環境變數設定,讓大家可以先輕鬆設定自己喜歡的 IdP (Identity Provider),然後將 IdP 產生的參數設定進 .env 檔案,即可使用這套 CloudFront Extension CDK 方案完成部署,熟練的狀況下應該可在 20 分鐘內完成。

這篇文章是以未來作為教育訓練為架構來安排順序,分成三個部分:

  1. 開頭會先介紹使用情境架構OAuth 2.0 原理Grant Type: Authorization Code 流程
  2. 接著挑選一個 IdP 進行設定(這部分在未來預計會陸續擴充與更新)
  3. 最後「CloudFront Extension OAuth2 Getting Started」帶大家實際操作這個 CDK 主角


1. 使用情境

近年來使用 靜態網站產生器 (static site generators) 不論是架設部落格、或是文件網站,都逐漸成為主流工具。而產生的靜態檔案們,除了放上 GitHub Pages,另一個好去處就是 Amazon S3

在 Amazon S3 前面放一個 Amazon CloudFront 作為 CDN,也是種常見的選擇。

在部分使用情境中,會希望使用者登入後,才能觀看網站或文件內容,此時就可以請 CloudFront Lambda@Edge 來幫我們執行一個 Lambda 程式,與自己指定的 IdP (Identity Provider) 進行登入、OAuth 2.0 授權流程。

這次會使用 CloudFront Lambda@Edge 四個觸發點的其中一個「Viewer request」來觸發執行我們指定的 Lambda 程式與 IdP 進行通訊,如果完成 OAuth2.0 Authorization Code 授權成功後,則放行使用者觀看 Origin 上頭的網站或文件內容。

接著來看兩個 demo 影片 (分別對應不同的 IdP,但都採用標準的 OAuth 2.0 與 OpenID),以快速理解本篇文章想實作的使用情境:

  • 下圖使用 Auth0 (IdP) + 本文實作的 OAuth2 Authentication (CloudFront Extension)
  • 下圖使用自建 KeyCloak (IdP) + 本文實作的 OAuth2 Authentication (CloudFront Extension)
    • 也是使用 CDK 快速部署 KeyCloak

2. Amazon CloudFront Extensions

Amazon CloudFront Extensions 是一組使用 Lambda@Edge 擴充 CloudFront 實作各種豐富功能的組合。原本使用 AWS CloudFormation 實作,但這次 Pahud 邀請我們改用 AWS CDK 方式來實做看看。

我們這次實作的 git repo 放在 Pahud 家:https://github.com/pahud/cdk-cloudfront-plus

我這次實作的擴充套件名稱為「OAuth2 Authentication」,方案編號「SO8131」,目錄名稱「cf-authentication-by-oauth2」。

實作目標是希望能創造一個通用型的套件 (Lambda@Edge),藉由環境變數設定以對應 IdP (Identity Provider) 提供的各種參數,例如 client id, client secret, callback URL 等等。並且希望能減少套件相依性,以及全程使用 TypeScript。

3. OAuth 2.0 Grant Type: Authorization Code

RFC6749 定義了 The OAuth 2.0 Authorization FrameworkOpenID 實作了 OAuth 2.0 並整合了帳號資訊。先簡單理解成 OAuth 2.0 是理論,OpenID 是基於該理論的其中一種實作方式。

OAuth 2.0 定義了四種角色六個步驟三次來回。參閱以下 Protocol Flow:

  • Client: Authorization Request –> Resource Owner: Authorization Grant
  • Client: Authorization Grant –> Authorization Server: Access Token
  • Client: Access Token –> Resource Server: Protected Resource

快速總結:簡單記法三步驟:先換 code –> 拿 code 換 (access) token –> 拿 token 換資源


OAuth 2.0 定義了四種 Authorization Grant Types:

  • Authorization Code
    • Authorization Code
    • Authorization Code with PKCE
  • Implicit
  • Resource Owner Password Credentials
  • Client Credentials

快速總結:本文使用情境適合使用 Authorization Code


確立流程架構,縮小範圍使用 Authorization Code 之後,接著設定自己自由選定的 IdP,在 IdP 環境中建立 client,並取得該 IdP 與該 client 的各項環境變數。

過程中 會遭遇各種 IdP 的毛,喔不是,我是說 IdP 為了加強驗證程序,會有各家不同的細節處理需要留意。例如

  • Auth0 文件沒寫但要指定 authorize 參數組合必須要是 response_type=code 搭配 scope=openid,才能拿到 JWT token。(參考來源)
  • KeyCloak 規定 Client 一開始從哪個 URL 過來,後續指定的 redirect_uri 也要完全一模一樣的 URL 才放行,否則 KeyCloak 會一直噴 400 Bad Request 附帶神秘未知的 error log [0m11:57:34,772 WARN [org.keycloak.events] (default task-97) type=CODE_TO_TOKEN_ERROR, realmId=RealmDemoCDK, clientId=demo-cloudfront-plus, userId=2ae58a4c-9b3e-4f0e-b58d-b9462e73105a, ipAddress=10.0.9.157, error=invalid_code, grant_type=authorization_code, code_id=fbb08df8-1af1-4960-a9ac-ab80e0b3d377, client_auth_method=client-secret。(參考來源)

4.1 IdP Example Configuration: Auth0

最後的 .env

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CLIENT_DOMAIN="your-domain-here.example.auth0.com"
CLIENT_ID="yourClientId"
CLIENT_SECRET="yourClientSecret"
CLIENT_PUBLIC_KEY="-----BEGIN CERTIFICATE-----\nPlaceYourKeyHere\n-----END CERTIFICATE-----"

AUTHORIZE_URL="https://your-domain.here/authorize"
AUTHORIZE_PARAMS="?response_type=code&scope=openid"
AUTHORIZE_REDIRECTURI_SHOULD_MATCH=false

CALLBACK_PATH="/callback"

JWT_ARGORITHM="RS256"
JWT_TOKEN_PATH="/oauth/token"

DEBUG_ENABLE=false

4.2 IdP Example Configuration: KeyCloak

實作完成 Auth0 的範例之後,正打算再多串接幾個 IdP 驗證一下各家的 OAuth 2.0 Authorization Code 流程是否一致,正巧與 Pahud 聊到他的 cdk-keycloak construct,就這麼巧,所以就開始了 Keycloak 的整合之旅 (aka 踩坑之旅)

首先由於已經有了 cdk-keycloak construct 的助攻,我只要組裝一個 CDK app 將之執行起來即可,所以我做了個 cdk-keycloak-demo,詳細安裝過程請先參閱 repo README.md,未來有機會再整理成筆記文章。在此先專注在如何設定,使之得以搭配 CloudFront Extension OAuth2 一起運作。

建立 Realm

建議名稱中不要包含有空白字元,最好是連續的英數字元,大小寫沒差別。

接下來 Realm name 會變成 URL 的一部分。在此舉例稱為 RealmDemoCDK

建立 Key

建立一組 Key 給 RS256 生成 JWT token 之用。在此舉例稱為 demo-cdk-rsa-key-pair。右側按鈕「Public Key」的內容請放進 CloudFront Extension OAuth2 .env 檔案的 CLIENT_PUBLIC_KEY 欄位中。

範例:

CLIENT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQABAQUAA4GNADDCiQKBgQ8EpDw0PDfYYqAwWFnzVFkfC7+diZTkVJob3WwtaeIsY9ygFCjGhOSWgpJkB8pqGFu0kYT8Nz8ZBOetvMwFUAGBBPrOVa6TSocss8fwMmmkQvFxpC2UmzRZY1Oj+0eMGI0KUPJ6I7wTuPdnC0mip+RJdvKxJw7UhOld4yAp1JSQewIAAQAB\n-----END PUBLIC KEY-----"

建立 Client

在此舉例稱為 demo-cloudfront-plus。內容請放進 CloudFront Extension OAuth2 .env 檔案的 CLIENT_ID 欄位中。

設定 Client

請對照下圖,選擇對應的開開關關,以及將 CloudFront Extension OAuth2 demo 部署完成的靜態網站 endpoint URL 設定給 Root URL、Valid Redirect URLs、Admin URL 以及 Web Origins,最後設定 * 給 Web Origins。

取得 Client Secret

在設定 Client 的「Credentials」分頁,可以找到 Client Secret。內容請放進 CloudFront Extension OAuth2 .env 檔案的 CLIENT_SECRET 欄位中。

建立 Client Role

在此舉例稱為 User。若沒建立 Client Role 以及後續幾個關聯動作,會造成 401 Unauthorized 錯誤。

建立 User

這個步驟就很彈性了,可以使用自己喜歡的 Username、Email、Last Name 與 First Name。

設定 User

為了測試方便,我將 User Enabled 和 Email Verified 開啟。

設定 User Credentials

為了測試方便,直接指定一組密碼給這位新的 User,並關閉 Temporary,然後按下 Reset Password。下圖是已經設定過密碼之後的畫面。

建立 Group

在此舉例稱為 ClientUsers

設定 Group Role Mapping

編輯新建立的 Group,使用 Role Mappings 分頁,設定 Client Roles,選擇我們所建立的 Client demo-cloudfront-plus,並選擇剛才建立的 Client Role User

設定 User Group

回到 User 編輯畫面,使用 Groups 分頁,將這位使用者歸類到剛才建立的 Group ClientUsers

最後的 .env

為了使 CloudFront Extension OAuth2 能正確與 KeyCloack 串接互動,請設定 .env 裡頭的 AUTHORIZE_REDIRECTURI_SHOULD_MATCH=true

CloudFront Extension OAuth2 .env 檔案範例格式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CLIENT_DOMAIN="your-demo-keycloak-elb-endpoint.example.com"
CLIENT_ID="demo-cloudfront-plus"
CLIENT_SECRET="12341234-1234-1234-1234-1234567890ab"
CLIENT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQABAQUAA4GNADDCiQKBgQ8EpDw0PDfYYqAwWFnzVFkfC7+diZTkVJob3WwtaeIsY9ygFCjGhOSWgpJkB8pqGFu0kYT8Nz8ZBOetvMwFUAGBBPrOVa6TSocss8fwMmmkQvFxpC2UmzRZY1Oj+0eMGI0KUPJ6I7wTuPdnC0mip+RJdvKxJw7UhOld4yAp1JSQewIAAQAB\n-----END PUBLIC KEY-----"

AUTHORIZE_URL="https://your-demo-keycloak-elb-endpoint.example.com/auth/realms/RealmDemoCDK/protocol/openid-connect/auth"
AUTHORIZE_PARAMS="?response_type=code&scope=openid"
AUTHORIZE_REDIRECTURI_SHOULD_MATCH=true

CALLBACK_PATH="/callback"

JWT_ARGORITHM="RS256"
JWT_TOKEN_PATH="/auth/realms/RealmDemoCDK/protocol/openid-connect/token"

DEBUG_ENABLE=false

5. CloudFront Extension OAuth2 Getting Started

總結一下,我們有了 Lambda@Edge、OAuth 2.0 Authorization Code grant type 的流程概念,以及設定了自己選定的一家 IdP 之後,接下來來將 CloudFront Extension OAuth2 demo 跑起來試試看,這個 demo 順利部署之後,將產生一個 CloudFront distribution endpoint URL 可以設定回去到 IdP Client 裡頭,讓 IdP 認識這組 URL。

CloudFront Extension OAuth2 demo 放在 https://github.com/pahud/cdk-cloudfront-plus/tree/main/src/demo/cf-authentication-by-oauth2,大家可以 clone repo,然後直接在專案根目錄操作即可。記得先設定好你的 AWS CLI 環境、AWS profile 以及 CDK 環境。

設定 .env

複製 dotenv/cf-authentication-by-oauth2/.env-exampledotenv/cf-authentication-by-oauth2/.env,然後依照你的 IdP 環境進行 .env 設定。

cp dotenv/cf-authentication-by-oauth2/.env-example dotenv/cf-authentication-by-oauth2/.env

yarn

先在第一個指令列視窗,執行以下指令:

yarn install

yarn watch

cdk deploy

接著在第二個指令列視窗,執行以下指令:

AWS_REGION=us-east-1 cdk --app lib/demo/cf-authentication-by-oauth2/index.js bootstrap

AWS_REGION=us-east-1 cdk --app lib/demo/cf-authentication-by-oauth2/index.js diff

AWS_REGION=us-east-1 cdk --app lib/demo/cf-authentication-by-oauth2/index.js deploy

Output

cdk deploy 完成後會顯示一組 CloudFront Distribution endpoint URL,請使用這組 URL 結合 .env 指定的 callback path,回到你的 IdP Application 或 IdP Client 的設定畫面,設定對應的 callback URL。

Login

完成 IdP Application/Client 設定後,迫不及待可以來測試看看,有沒有正確轉向給 IdP,以及輸入帳號密碼後,有沒有正確轉向回 callback URL。

https://<CLOUDFRONT_DOMAIN>

若過程中發生錯誤,可以到 AWS Management Console 使用 CloudWatch Logs 功能追查。記得選用所發生的 AWS Region 才找得到對應的 Lambda@Edge 執行結果,並記得開啟 .env 裡頭的 DEBUG_ENABLE=true

Origin S3 (Private content)

若一切順利,你將看到如下內容 :) 未來再替換成你自己的網頁內容或文件即可。

Hello CloudFront Extension with CDK!!!
You have logged in. Enjoy your private content.

6. 參考資料

7. 總結

本文開場介紹使用情境、架構、OAuth 2.0 原理、Grant Type: Authorization Code 流程,接續著帶大家將自選的 IdP Client/Application 進行設定,最後透過「CloudFront Extension OAuth2 Getting Started」帶大家實際操作這個 CDK 快速部署。

我找了幾位朋友實測,在沒有被 IdP 設定卡關的情況下,都可以在 20 分鐘內完成 CloudFront Extension OAuth2 CDK 的部署。

期待這個小工具能幫大家節省時間,讓大家更專注在產品設計與內容產出上,大家可以動手試試看 :)

若喜歡這類完整實作的文章整理,歡迎用下方表格訂閱我的電子報。

Peace out!

Loading comments…