﻿# LINE Login 簽發的 ID Token 如何用 ES256 非對稱加密演算法的公開金鑰驗證

我前陣子在研究 LINE Login 使用的 `ES256` 非對稱加密演算法，當我透過 OpenId Connect 拿到 ID Token 後，我想拿 LINE Login 提供的 JWKs 金鑰組來驗證其有效性，這篇文章我打算分享如何用 C# 搭配 LINE Login 提供的 JWKs 金鑰組來驗證其有效性！

![Cyber Security](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/3355dbe7ec767a46583d-230780153-2ab44e34-f37b-4f17-bd4b-75f220b678ea.jpg)

### `ES256` 與 `HS256` 傻傻搞不清楚

這個需求我嘗試了好久都寫不出來，直到昨天才發現，原來是 LINE Login 預設並不會核發 `ES256` 簽發的 JWT Token，所以沒辦法拿 OpenId Connect 提供的 JWKs 來驗證 ID Token。由於 LINE Login 預設是用 `HS256` 對稱式加密演算法，需要 LINE 的私密金鑰才能驗證，否則就要呼叫 LINE Login 的 [Verify API](https://api.line.me/oauth2/v2.1/verify) 才能得知真偽，這比較不是我希望的解決方案。我希望我可以在伺服器端，直接透過下載過的 JWKs 金鑰驗證使用者發來的 ID Token，確保驗證 ID Token 的效能與可靠性。

至於要如何讓 LINE Login 發出 `ES256` 簽發的 ID Token 呢？你必須要認真看完我的 [如何將一個 ASP.NET Core 網站快速加入 LINE Login 功能 (OpenID Connect)](/post/2022/04/08/LINE-Login-with-OpenID-Connect-in-ASPNET-Core) 文章，確保從 Token Endpoint 取得 Access Token 與 ID Token 時，要額外送出一個 `id_token_key_type=JWK` 的參數才行，這真的超雷的！

除此之外，我還發現 `HS256` 對稱式加密演算法核發的 ID Token，其 JWT Token 的 Header 部分內容如下：

```json
{
  "typ": "JWT",
  "alg": "HS256"
}
```

而透過 `ES256` 非對稱式加密演算法核發的 ID Token，其 JWT Token 的 Header 部分內容如下，多了一個 `kid` (Key ID) 屬性：

```json
{
  "kid": "95e9119f653eae095bb3d871d6ba8ff46467aa314575a4543615f051d4b735a6",
  "typ": "JWT",
  "alg": "ES256"
}
```

### 如何驗證 ID Token 的有效性

驗證 ID Token 的有效性主要有三個步驟：

1.  透過 LINE Login 的 [OpenId Connect Discovery Document](https://access.line.me/.well-known/openid-configuration) 得知 `jwks_uri` 如下：
    
    [https://api.line.me/oauth2/v2.1/certs](https://api.line.me/oauth2/v2.1/certs)
    
    取得內容的程式碼如下，記得要先安裝 [IdentityModel](https://www.nuget.org/packages/IdentityModel) 與 [System.IdentityModel.Tokens.Jwt](https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt) 這兩個 NuGet 套件：
    
    ```csharp
    var http = new HttpClient();
    
    // dotnet add package IdentityModel
    var disco = await http.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
    {
        Address = "https://access.line.me", // Authority
        Policy = new() { ValidateEndpoints = false }
    });
    
    var certs = await http.GetStringAsync(disco.JwksUri);
    ```
    
    > 這裡的 `certs` 是一個字串，你可以將該字串**快取**(Cache)起來，不用一直到 LINE 網站下載憑證！
    
2.  取得 JWKs 所有的公開金鑰
    
    ```csharp
    var signingKeys = new JsonWebKeySet(certs).GetSigningKeys();
    ```
    
    如果要從 ID Token 的 JWT Header 的 `kid` 比對出金鑰，可以這樣寫：
    
    ```csharp
    var idToken = "{YOUR_ID_TOKEN}";
    
    var tokenHandler = new JwtSecurityTokenHandler();
    var decodedToken = tokenHandler.ReadJwtToken(idToken);
    //var header    = JsonSerializer.Serialize(decodedToken.Header);
    //var payload   = JsonSerializer.Serialize(decodedToken.Payload);
    //var signature = decodedToken.RawSignature;
    
    var key = signingKeys.FirstOrDefault(k => k.KeyId == decodedToken.Header.Kid);
    ```
    
3.  驗證 ID Token 的真偽
    
    這裡我就是希望不要透過呼叫 API 的方式驗證 ID Token 的有效性，直接在本機完成驗證動作，執行效能肯定是最好的！
    
    ```csharp
    var handler = new JsonWebTokenHandler();
    TokenValidationResult result = handler.ValidateToken(idToken, new TokenValidationParameters
    {
        ValidIssuer = "https://access.line.me",
        ValidAudience = "1566379427", // LINE Login 的 Channel ID
        IssuerSigningKey = key,
    });
    
    if (result.IsValid)
    {
        Console.WriteLine("The ID Token is VALID.");
    }
    else
    {
        // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/wiki/PII
        Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
    
        throw result.Exception;
    }
    ```
    

### 總結

這段程式碼我寫好幾個月了，但一直執行失敗，無法驗證，昨天才得知原來是 LINE Login 預設並不會用 `ES256` 簽發的 ID Token，而 `ES256` 與 `HS256` 我又不太熟悉，兩個名字又這麼像，實在很容易搞混！😒

完整實作我放在 [https://github.com/doggy8088/LINELoginIDTokenValidator](https://github.com/doggy8088/LINELoginIDTokenValidator)

### 相關連結

-   [JWT 簽名算法 HS256、RS256 及 ES256 及密鑰產生](https://www.cnblogs.com/kirito-c/p/12402066.html)
-   [\[OpenID\] 使用 RS256 與 JWKS 驗證 JWT token 有效性](https://fullstackladder.dev/blog/2023/01/28/openid-validate-token-with-rs256-and-jwks/)
-   [如何將一個 ASP.NET Core 網站快速加入 LINE Login 功能 (OpenID Connect)](/post/2022/04/08/LINE-Login-with-OpenID-Connect-in-ASPNET-Core)
