iT邦幫忙

2022 iThome 鐵人賽

DAY 13
2
Software Development

30 天與九頭蛇先生做好朋友系列 第 13

應用程式處理身分驗證回應

  • 分享至 

  • xImage
  •  

OpenID Connect 最一開始是由應用程式發出身分驗證請求(Authentication Request),接著使用者會在授權伺服器上( Hydra、Login Provider、Consent Provider)上完成身分驗證,並同意應用程式請求的授權範圍後,接下來就輪到應用程式處理身分驗證回應(Authentication Response)了。

先回顧一下,在整個登入流程裡,最一開始的授權請求有這一行程式碼

'response_type' => 'code',

它會在轉導到 Hydra 的 Query 上加上 response_type=code 這個參數。這個參數代表的意思是,請求在授權回應的時候要給應用程式 code

而實際執行完的結果是到下面這個網址:

http://127.0.0.1:8000/callback?code=9BRBZZPq....&scope=openid&state=1a2b3c4d

上面的 Query 參數內容就是授權回應,OpenID Connect 並沒有重新定義欄位,而是沿用 OAuth 2.0 - 4.1.2. Authorization Response 的定義。成功的身分驗證回應如下:

欄位 必填 說明
code REQUIRED 授權碼,應用程式可以拿這個參數來向授權伺服器交換 Access Token。
state REQUIRED,當身分驗證請求有傳這個欄位,則授權回應必須要有這個欄位 當初身分驗證請求填了什麼內容,這裡就會出現一樣的內容。這是為了避免 CSRF 攻擊
scope 這個欄位並不是 OAuth 2.0 標準定義,而是 Hydra 額外回傳的。雖然原始碼看到有做開關可以控制,但實際上 Hydra 並沒有設定在處理這個開關。 從程式與測試結果可以看得出,這裡會放使用者已同意的授權範圍。

身分驗證或授權的過程也有可能失敗,這時應用程式會收到失敗的身分驗證回應,OpenID Connect Core 1.0 - 3.1.2.6. Authentication Error Response 有定義相關的欄位如下:

欄位 必填 說明
error REQUIRED 錯誤代碼,要給程式判斷使用
error_description OPTIONAL 人看得懂的訊息,這是要給開始者參考用的。
error_uri OPTIONAL 額外的網址,讓開發者可以在上面查到更多資訊。但 Hydra 並沒有實作這個參數,可以參考原始碼
state REQUIRED,與成功的身分驗證回應相同。 與成功的身分驗證回應相同。

不管成功或失敗,都會有一個參數是 state。應用程式第一步應該是要確認 state 正確性後,再開始處理身分驗證回應。處理 state 的要點在於,要確認當初發出身分驗證請求的瀏覽器或裝置,跟現在拿到身分驗證回應的的瀏覽器或裝置是一致的,也就是要避免 CSRF 攻擊。以瀏覽器的情境來說,通常會使用 Cookie 或 Session 的機制來儲存 state 的內容,接著在轉導回應用程式的時候再拿出來確認內容一致。

確認完成後,應用程式才能知道這個身分驗證回應是合法應該要處理的。以下再接著說明成功與失敗的時候該怎麼處理。

處理成功的身分驗證回應

之前在快速體驗的示範裡,使用回應類型設定是 response_type=token ,因此前端可以直接拿到 Access Token,然後授權流程就結束了。

這次回應類型設定是 response_type=code,身分驗證回應裡,只有拿到 Code 而沒有 Access Token,而 Code 的用途是要拿來跟 Hydra 交換 Access Token 用的。OpenID Connect Core 1.0 - 3.1.3.1 Token Request 有提到該如何對授權伺服器的 token 端點發送 Token 請求,這裡也是參考 OAuth 2.0 - 4.1.3. Access Token Request,欄位定義如下:

欄位 必填 說明
grant_type REQUIRED 固定為 authorization_code
code REQUIRED 原文為 Authorization Code,中文直譯即「授權碼」。
redirect_uri REQUIRED,當一開始的身分驗證請求中有這個欄位時,就是必填。 要帶入一開始身分驗證請求裡的 redirect_uri,而且要完全一致。
client_id REQUIRED,當應用程式一開始沒註冊驗證方法時,則此欄位必填。(以這個例子來說,是不需要給這個欄位的) 此為應用程式的唯一識別碼。

Hydra 的 token 端點,可以從 SDK 的 Public API 裡可以找到對應的方法,接著可以在 callback 裡寫如下的程式,裡面有很多欄位或值都是上面表格找得到的:

public function __invoke(Request $request, PublicApi $hydra)
{
    $redirectUri = 'http://127.0.0.1:8000/callback';

    $tokenResponse = $hydra->oauth2Token(
        grantType: 'authorization_code',
        code: $request->input('code'),
        redirectUri: $redirectUri
    );

    dump(json_decode((string)$tokenResponse, true));

    return response('拿到身分驗證回應了');
}

Hydra SDK 提供的 oauth2Token() 方法,會呼叫 Hydra Public API 所提供的 Token 端點來取得 Token 資訊。但 API 也不是隨隨便便就讓別人能呼叫的,要先證明你是應用程式的持有人才行,因此這時要提供應用程式的憑證才能正常呼叫。

Hydra 的 SDK 是透過 Config 物件來設定憑證,這裡是物件初始化過程的程式碼:

$this->app->singleton(PublicApi::class, function () {
    return tap(new PublicApi(), function (PublicApi $instance) {
        $instance->getConfig()
            // 這裡要注意,是用 Public API 的 port
            ->setHost('http://127.0.0.1:4444')
            ->setUsername('my-rp')
            ->setPassword('my-secret')
            // 下面這行應該是 Hydra SDK 的 bug,必須要強制設定為 null 才能正常執行
            ->setAccessToken(null);
    });
});

成功驗證就能夠拿到像下面這樣的 Token 回應:

{
  "access_token": "gljqNs7XFogfPJURtAW6rygFm-KzRWphuv_EqD5LUMQ.XdXx9iDX3lJaQVQHhNnCl77IWSPf87UhaOYrlkGt9_8",
  "expires_in": 3599,
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpoeWRyYS5vcGVuaWQuaWQtdG9rZW4iLCJ0eXAiOiJKV1QifQ.eyJhdF9oYXNoIjoiVTMxN2xqVE5zMTBBV3Z3bmgwUW9IZyIsImF1ZCI6WyJteS1ycCJdLCJhdXRoX3RpbWUiOjE2NjQzNTE0MzcsImV4cCI6MTY2NDM1NTAzOSwiaWF0IjoxNjY0MzUxNDM5LCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjQ0NDQvIiwianRpIjoiMjEwZWRhNDMtZjhjNy00NWEzLWEyNWItYTJiZDkyMjcyOTcxIiwicmF0IjoxNjY0MzUxNDMxLCJzaWQiOiI3MTViNjNjNC0wZjJhLTRmZTUtOGQyOC1lODMwZGQzNjEzNDkiLCJzdWIiOiIxIn0.sM3A5wLTUBE1DlDAqfcF8pSUNU74AaP2GDaJ_gLLEV_uh-tG6b6B8e58fxH1vRmjtTE6aJG-chG98di12paDD8tV6JXAcuyk3p5Tw_OZnCcFVEvAYoP_m8BYhwGK7DfmiWCZVMTHLX2AFd3J93XR7tems-xV1ponXQQaPDIVKjEVe3k9aoPkgKFj-8CabMwq-aVzKmq0Zr940tynWUTbu_ylSdnnvw68q74zmL6mwhGaDQ0DKTza-fDy_dp_Ht1QujIa-aSrLLkWm9MP0zzPCiZ1YYv-cpx3xb-tpoJm3bNu1h6XMGCjsoPtBOgQzESqojea0H2Cc7DsnDlezIVKM2OphF0_REy5ljBIA3zmbN0arbVVozN2p68YHX8YVp121yWpLycw1DA9eXdu2xp4I8N2Vmng9sez7KT8vKT9vCzFJgqTNG5Jhrv2VH7F4IXzjmbLD3bQW9HAtI_SGwSZ9FsQFUifxxgs1L9FFGb_pyc9arfoTqJnIaZgfsPMOQ6_4S4GlNwvX9YugOugecQfnfXX17ddNHtU7_YOkAAccZVaG5xQZEDtnggrtOVhBKebt5oHFLNDk44eK6GzD1yV9RtO0g0IK6OTt_IFN6gcEPo5d-QTphLxpYecLSvcYciI5k_wx-RpwZdJM1IU-JHqMUdHHm-DpOPCHzTAuXu894w",
  "scope": "openid",
  "token_type": "bearer"
}

這裡為了方便下一次解說,先把 id_token 的值完全保留,明天再來說明詳細內容。

這個回應是 OpenID Connect Core 1.0 - 3.1.3.3. Successful Token Response 裡定義的,裡面提到這是基於 OAuth 2.0 - 4.1.4. Access Token Response(然後裡面又提到欄位定義要再參考 5.1. Successful Response)再加上新的 id_token 欄位而成的。Token 回應的欄位定義如下:

欄位 必填 說明
access_token REQUIRED 可以用來存取資源伺服器的憑證。
token_type REQUIRED Token 的類型。
expires_in RECOMMENDED Token 還有幾秒會過期。
scope OPTIONAL / REQUIRED 使用者同意授權範圍,如果跟應用程式要求的範圍相同時,可以省略;如果不同則必須要回傳。
id_token REQUIRED OpenID Connect 定義的新欄位,也可以說如果有要求授權 openid 範圍的時候,就會給予這個欄位。

到了這裡,因為 Token 都拿到了,所以原則上應用程式要求授權的流程基本上就結束了。再來就會是驗證 Token 是否正確,這部分留到明天繼續。

請求 Token 也有可能會失敗,回傳的訊息定義在 OAuth 2.0 - 5.2. Error Response 裡,格式跟身分驗證回應差不多,但 error 的錯誤碼,因為情境不同所以會有所不同。比方說 invalid_client 是指應用程式驗證失敗,像是密碼(secret)打錯,這是在身分驗證請求到身分驗證回應的過程中不會發生的,這部分就留給讀者閱讀。

處理失敗的身分驗證回應

先講失敗的授權回應。情境很好理解,參考 error 的內容做對應的業務邏輯處理即可。如:昨天實作 Consent Provider 的時候有提到被使用者拒絕授權的情境,應該要回 access_denied,就能讓應用程式知道現在發生什麼問題,以及該出什麼樣的錯的訊息給使用者參考。範例程式碼如下:

$error = $request->input('error');

if (null !== $error) {
    return match ($error) {
        'access_denied' => response('使用者拒絕授權'),
        default => response('未知的 error: ' . $error),
    };
}

其他在 Login Provider 或 Consent Provider 處理的過程中,只要身分驗證或授權流程無法進行下去的時候,都可以回錯誤碼給應用程式。但該回哪一個,就必須要參考協定說明來決定適合的錯誤碼。

範例程式碼有實作成功的授權回應,和 access_denied 的授權回應,請讀者可以參考 GitHub Commit


上一篇
實作 Consent Provider
下一篇
如何驗證 ID Token 的資訊
系列文
30 天與九頭蛇先生做好朋友23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
json_liang
iT邦研究生 4 級 ‧ 2022-09-28 17:12:27

感謝分享!callback 概念真的很重要

0
雷N
iT邦研究生 1 級 ‧ 2022-09-28 17:23:33

回應callback 真的是串接整合上重要的概念跟流程
感謝五哥分享

我要留言

立即登入留言