OIDC OpenID Connect ID Token

ID Token 如何應用

李蕙如 Mini Lee 2023/04/10 18:58:23
2629

End User身份驗證的時空背景

  單機Web網站發展至今我們可以在各種 framwork 上利用 Cookie 或 Session 之類的方法存放使用者資訊,然後在回到 Application 的時候再依對應的Key於資料庫上取得詳細資訊。但OIDC有提到它的 work flow 設計是將處理 OpenID Connect 流程身分驗證授權,三個不同目的分成不同的服務來處理,Application 則又是另外一個,這四個服務有可能會部署在不同的網域,因此是無法透過 Cookie 或 Session 取得的。這也是為什麼 OpenID Connect 或 OAuth 2.0 的流程是透過轉導來傳遞授權請求與回應。授權流程執行到最後,Application該如何取得使用者資訊呢?這些資訊其實是藏在 Token 裡面的。Token 有分成兩類,一個是不透明的 Token(Opaque Tokens),指的是 Token 是純粹無意義的亂數字串,必須從其他資料庫取得字串對應到資料庫裡的資訊;另一種是透明的 Token(Not Opaque Tokens),指的是 Token 有辦法透過演算法解析出內容並閱讀。剛好 ID Token 屬於透明的Token。

 

  本文主要介紹屬於透明 Token 的 ID Token,它本質上是一个 JWT Token,包含了該使用者資訊相關的 key/value pair,例如:

{

   "iss": "https://server.example.com", //核發者

   "sub": "24400320", // subject 的縮寫,為使用者資訊 ID

   "aud": "s6BhdRkqt3", // 接受者(受眾,不一定是一個單位)

   "nonce": "n-0S6_WzA2Mj", // 使用者在授權請求中包含一個隨機生成的 nonce 值,收到 id_token 時要再檢查一次,它通過防止重放攻擊來保護ID Token的完整性和可靠性

   "exp": 1311281970, // 效期

   "iat": 1311280970, // 核發時間

}

 

ID Token 本質上是一个 JWT Token 意味着:

1. 使用者資訊直接被編碼進了 id_token,你不需要額外請求其他的資源來獲取身份資訊;

2. id_token 可以驗證其沒有被篡改過,詳情請見如何驗證 id_token。

 

如何驗證終端使用者的身份 (ID Token Validation)

  驗證 Token 分為兩種模式:本地驗證與使用 Gateway 提供的 User Info API 即時訪問驗證(簡稱在線驗證)。我們建議在本地驗證 JWT Token,因為可以節省你的Server頻寬並加快驗證速度。你也可以選擇將 Token 發送到 Gateway IdP 的驗證 API 由 Gateway 進行驗證並返回結果,但這樣會造成網路延遲,而且在網路擁塞時可能會造成慢速請求,而慢速的原因僅僅是因為伺服器對外調用API。

 

以下是本地驗證和在線驗證的優劣對比:

 

驗證速度

程式撰寫難易度

可靠程度

在線驗證

簡單

單點失敗風險

本地驗證

使用Lib需要學習

分散式計算風險低

第三方驗證(jwt.io)

不用寫,會copy paste即可

用來學習與比較的

 

  第三方驗證 id_token,以 https://jwt.io 官網為例,將 jwt string 直接貼上去,它就會直接告知你驗證結果是否成功,如下圖它呈現的就是一個 id_token 失效的結果,不過它失效是因為它的效期過期了。

 

  在線驗證 id_token,這需要參考使用的idP所使用的 user_info API,一般我們可以由 well know URL 得知它的API,以下舉個例子:

https://accounts.google.com/.well-known/openid-configuration

 

https://access.line.me/.well-known/openid-configuration

 

  本地驗證 id_token,以Java為例這裡的範例使用昕力大學DevKit中提供的 Library,詳情請見 https://www.tpisoftware.com/tpu/devKit/details/2909

public class VerifyIdToken {

	public static void main(String[] args) {

		String token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFmYzRmYmE2NTk5ZmY1ZjYzYjcyZGM1MjI0MjgyNzg2ODJmM2E3ZjEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIyNTQ2MDU2MDA3NjctanVrZ3FiMGZlcXVwcnZ2NTNkaHV0MTI1N2ZicG9lNWYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyNTQ2MDU2MDA3NjctanVrZ3FiMGZlcXVwcnZ2NTNkaHV0MTI1N2ZicG9lNWYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDU3Njk1MjAyMTExMDQ4Njk5NDkiLCJoZCI6InRwaXNvZnR3YXJlLmNvbSIsImVtYWlsIjoiam9obi5jaGVuQHRwaXNvZnR3YXJlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiV2xjVVhCTk1PTTdvSXZUWkc5WTZFdyIsIm5hbWUiOiLpmbPnkZ7ms7AgSm9obiBDaGVuIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FFZEZUcDRHUW1pazM3NXZyUTRsTmhGZWZWcF9wMGtQT0R2VzVOdE1XMmdtX3c9czk2LWMiLCJnaXZlbl9uYW1lIjoi55Ge5rOwIEpvaG4gQ2hlbiIsImZhbWlseV9uYW1lIjoi6ZmzIiwibG9jYWxlIjoiemgtVFciLCJpYXQiOjE2NzQzNTI1OTYsImV4cCI6MTY3NDM1NjE5Nn0.AkWnSfIqB5oG_N8OSRQx4QSiqoMY9HpcU2TLWmPf_YFs6DsdMoR-ilASybEhWUJ6cXFT1X9Pi1_oF5mm8-QD72viFuwgxU6XCHLBWt9zt1kAg8AGmuIulhm6PGzHKLT1RB_2podhIsAkYA9H2Or4XuHZFiCmPk9rRx9GW9wUHaCVAhIERxJG4FdXkHuHvhFxlSLWsIzidQTIaDpwy_qvWTjuQzpXMBgt3_xma5VSB8Lh9TJI40Oh5b3MoEnIcyTlnBrL4UUVEHCg9Dv0ZovnFPCQVOS-yfn8IlnTVuyc-Kj0Y5KritVUt4PkMp4WDWriLkG9JcuwHXXygO1un_lPAQ";

		String jwks_uri = "https://www.googleapis.com/oauth2/v3/certs";

		String issuer = "https://accounts.google.com";

		JWKVerifyResult rs = JWKcodec.verifyJWStoken(token, jwks_uri, issuer); //進行簽章驗證, 驗不過會丟出 exception

		System.out.println("驗證結果:" + rs.verify);

		

		// 有了結果可以做後續處理

		if (rs.verify) {

			System.out.println("JWT Verified (O)");

			System.out.println("UserName = " + rs.tokenClaimsSet.get("name"));

		} else {

			System.out.println("JWT Verified (X)" );

			System.err.println(rs.errorMessg);

		}

	}

}

 

上述的範例在驗證時需要提供三個參數:(1)id_token (2)jwks_url (3)issuer_url 需要這三種參數是有理由的,第一個id_token它就是一個身份憑證,第二個因為憑證被發證者簽章過,所以需要發證者的公鑰來驗章,第三需要檢驗者自身明白你的憑證是誰發給你的,這就像客服電話必須自己去官方網站查詢,而不能相信突然收到的客服簡訊要求你回撥上面的客服電話,這樣才能避免被釣魚。

 

如何在 API 中獲取 ID Token

  上一節您已學會了驗證id_token,在這一小節將會以Java Controller 範例說明如何在應用程式端取得id_token,假設有一個API端點需要驗證使用者身份,並檢查使用者是否有權訪問受保護的資源。當使用者登錄並獲得有效的ID Token後,可以使用Bearer HTTP Header傳輸ID Token,以授權API請求。

 

例如,如果使用者獲得的ID Token為:

eyJhbGciOiJSUzI1NiIsImtpZCI6IjIzMTk3NzZkMmZkNzZlMjNlOWEwMDRkMTNjYjBhNmY0YjRhNjM5M2QiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJRRy1aU1p6UW4zU0lOVUc2Q2dPcWlSZEZSQTgifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

 

則 Gateway 一般會將Bearer HTTP Header (Authorization)添加到API請求中,如下所示:

GET /api/protected HTTP/1.1

Host: example.com

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjIzMTk3NzZkMmZkNzZlMjNlOWEwMDRkMTNjYjBhNmY0YjRhNjM5M2QiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJRRy1aU1p6UW4zU0lOVUc2Q2dPcWlSZEZSQTgifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

 

在這個例子中,API 端可以利用 getHeader(“Authorization”) 獲取 id_token JWT string,以下示範一個 SpringBoot @RestController的寫法:

import org.springframework.http.HttpHeaders;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RestController;



@RestController

public class MyController {



    @GetMapping("/myendpoint")

    public String myEndpoint(@RequestHeader HttpHeaders headers) {

        String idToken = headers.getFirst("Authorization");

        // 在這裡使用ID Token 或是本地驗證(當然驗證也可以不寫在每一支API而是寫在Filter)

        return "Hello, World!";

    }

}

 

在這個範例中,@RequestHeader HttpHeaders headers註解告訴Spring Boot將HTTP請求的標頭信息映射到headers參數中。然後,可以使用getFirst方法獲取Authorization標頭的值,該值應該是包含ID Token的Bearer標頭。

 

注意,為了使Spring Boot可以解析Authorization標頭的值,需要確保在發送HTTP請求時使用Bearer標頭和ID Token。以下是發送HTTP請求時應該包含的Authorization標頭:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjIzMTk3NzZkMmZkNzZlMjNlOWEwMDRkMTNjYjBhNmY0YjRhNjM5M2QiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJRRy1aU1p6UW4zU0lOVUc2Q2dPcWlSZEZSQTgifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

這樣,當客戶端向myendpoint端點發送HTTP請求時,Spring Boot將從請求標頭中提取Authorization標頭的值,並將其作為ID Token提供給myEndpoint方法。

 

使用 digiRunner Express 實作 Application API

  本小節將會帶你在本機上完成整個操作流程,包括安裝 API Gateway,由Gateway 取得 Access_Token & Id_Token,最後利用 post-man帶上2種 Token 調用API,準備的材料如下:

1. digiRunner Express安裝包下載

2. Spring Boot 

3. 昕力大學 DevKit 提供的 TPU_VerifyIdToken.zip

4. digiRunner 設定 Client(Application) & 授權可使用的 API

5. Postman 以 Authorization code 方式調用 API

6. 本機自我測試API不經過 Gateway 以Postman 進行調用

 

  digiRunner Express 安裝包解壓縮後,啟動 digiRunner,啟動完成即可看到下方的 Logo 圖示。

 

  Spring Boot可以利用 Eclipse 建置專案,本文在此不教花里胡哨的framework,就連 Spring Security 也不教甚至不會使用它,單純就是最小的SpringBoot套件即可,因為那會牽涉到社群idP的特性,而本文的重點在驗證id_token,利用它來取代傳統單主機 Cookie 或 Session 的登入機制。

這是一支完整的程式,它的SpringBoot也沒有套用SpringSecurity,使用最純粹的Controller,沒有任何花招,由此證明使用 id_token來驗證 End User 身份不需要什麼高大上的技術,程式小白也可以輕鬆上手,就企業而言也就不需要再開發所謂的登入機制,而是直接選寫商業邏輯即可,省時又省成本。

 

  digiRunner 設定 Client(Application) & 授權可使用的 API,這裡我們截錄 digiRunner 的 client 及群組設定,如果您有慣用的API Gateway則請洽他們的官方網站,若是使用digiRunner Express有問題也可來信詢問,digiRunner相關設定截圖如下:

此設定將來會填寫在Postman的client & secret 欄位

 

此為API Gateway的授權設定,它說明 googleclient 可以使用的 scope 有哪些 API

 

請把您的Application API 註冊上來

 

可以看到 buxtaxi2 將會轉導到您的 Application API,到這裡digiRunner Express 就設定完成了。

 

  Postman 以 Authorization code 方式調用 API,在Postman介面中它整合了呼叫API時需要預先取得 Token的預取機制,由於OAuth2.0授權機制較為複雜,故Postman在 Authorization這個頁籤中選 ‘Oauth2.0’ 然後把右方的欄位依序填寫完畢,按下send 它就會先跑token授權流程直到取到 token。

 

按下 send 它會進行 idP 跳轉

由於本範例演示google 成為 digiRunner 代理的idP ,故您會發現是 google 在驗證 End User

 

表示正在收取 digiRunner 核發的 2種 Token

 

這裡我們可以檢視到 digiRunner 回傳二種 token 回來,接著按 ‘use token’


我們看到Postman已成功取回API結果,並且顯示出終端使用者身份。

 

要注意一點 Postman 打 digiRunner Gateway 必須一次挾帶 2 種 token,若您要測直接打API也可以在 Postman上選擇以id_token 代入  Authorization,並且加入 Bearer 檔頭。

 

  本機自我測試API不經過 Gateway 以 Postman 進行調用,這個主題最真實貼近後端 API 工程師的開發習慣,試想在開發時只要預先取得 id_token 的字串,且把它的效期設長一點,例如一週,那麼每次調用 API 時就可以不取 token,以下我們利用 Postman 示範完整的流程供各位參考:

取到 token 後,記得來這個頁面把 ‘Use Token Type’,改為 ‘ID token’ ,這樣就可以直接打您的 API,不需經過 API gateway,與上一動相比,您會發現和 API Gateway代理回傳的內容一模一樣。

 

這是 Postman 自動帶入的 ID token。

 

使用 id_token 取代傳統的 cookies 或 session 機制好處多多,以下總結一下它的好處:

 

1. 無需在服務器上儲存任何狀態:使用ID Token的方式可以使服務器不必儲存任何使用者的狀態,從而降低了服務器的負擔。相反,使用者的狀態由使用者代理(例如瀏覽器或移動應用程序)管理。

 

2. 可以跨不同的應用程序使用:使用ID Token可以令使用者驗證狀態在不同的應用程序之間共享,從而實現單點登錄(SSO)。這使得使用者可以在一次身份驗證後訪問多個應用程序,而無需多次輸入其憑證。

 

3. 可以提高安全性:使用ID Token的方式可以使應用程序更容易實現跨站請求偽造(CSRF)防護,因為ID Token包含一些用於驗證用戶身份的元數據。同時,ID Token還可以防止在使用者端上注入假的使用者狀態。

 

4. 可以提高可擴展性:使用ID Token的方式可以使應用程序更容易實現跨不同平台的身份驗證,從而實現更好的可擴展性和可移植性。

 

總之,使用ID Token可以使應用程序更安全、更可擴展、更易於維護,並且可以提供更好的使用者體驗。

 

 

李蕙如 Mini Lee