The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

使用 dotnet user-jwts 管理開發時期的 JWT Tokens 與 Signing key

從 ASP.NET Core 7.0 開始,.NET SDK 7 內建支援 dotnet user-jwts 命令,可以幫助你管理開發時期所需的金鑰與 JWT Tokens,我深入的把玩了一下,發現還真的好用,這篇文章我就來說說怎樣使用。

background-design-cyber-technology-security-network-protection

建立範例專案

  1. 鎖定 .NET SDK 版本到 7.0.203 (含之後的版本)

    dotnet new globaljson --sdk-version 7.0.203 --roll-forward latestFeature
    
  2. 建立 ASP.NET Core Web API 專案

    dotnet new webapi -n AspNetCoreUserJwtsDemo
    cd AspNetCoreUserJwtsDemo
    dotnet new gitignore
    git init
    git add .
    git commit -m "Initial commit"
    

初始化認證與授權

  1. 安裝 Microsoft.AspNetCore.Authentication.JwtBearer 套件

    dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
    
  2. 修改 Program.cs 註冊 JwtBearer 服務到 DI 容器

    builder.Services.AddAuthentication("Bearer").AddJwtBearer();
    builder.Services.AddAuthorization();
    
  3. 修改 Program.cs 設定將所有 Controller 都要需要走 Bearer Token 認證與授權

    app.MapControllers().RequireAuthorization();
    
  4. 理論上所有的 API 都無法存取才對

    先調整 Properties/launchSettings.json 檔案

    {
      "$schema": "https://json.schemastore.org/launchsettings.json",
      "profiles": {
        "http": {
          "commandName": "Project",
          "dotnetRunMessages": true,
          "launchBrowser": false,
          "launchUrl": "WeatherForecast",
          "applicationUrl": "http://localhost:5000",
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
      }
    }
    

    啟動網站

    dotnet run
    

    透過 cURL 發出要求

    curl -i http://localhost:5000/WeatherForecast
    

    執行結果,你將會得到 HTTP/1.1 401 Unauthorized 回應!

    HTTP/1.1 401 Unauthorized
    Content-Length: 0
    Date: Fri, 28 Apr 2023 13:15:33 GMT
    Server: Kestrel
    WWW-Authenticate: Bearer
    

使用 dotnet user-jwts create 建立 JWT Token

  1. 執行以下命令建立 JWT Token

    dotnet user-jwts create
    

    執行結果如下:

    New JWT saved with ID '3165f978'.
    Name: wakau
    
    Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiIzMTY1Zjk3OCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY4ODMxNiwiZXhwIjoxNjkwNTUwNzE2LCJpYXQiOjE2ODI2ODgzMTcsImlzcyI6ImRvdG5ldC11c2VyLWp3dHMifQ.WergzBiHz7UFAeBotkpaM9lpn8is0J5Fpm0D2yCfB-A
    

    預設 Issuer 為 dotnet-user-jwts

  2. 檢查是否可以用這個 Token 呼叫 API

    重新啟動網站 (一定要啟動在 Development 環境喔)

    dotnet run
    

    透過 cURL 發出要求

    curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiIzMTY1Zjk3OCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY4ODMxNiwiZXhwIjoxNjkwNTUwNzE2LCJpYXQiOjE2ODI2ODgzMTcsImlzcyI6ImRvdG5ldC11c2VyLWp3dHMifQ.WergzBiHz7UFAeBotkpaM9lpn8is0J5Fpm0D2yCfB-A" http://localhost:5000/WeatherForecast
    

    執行結果,你將會得到 HTTP/1.1 200 OK 回應!

    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8
    Date: Fri, 28 Apr 2023 13:20:48 GMT
    Server: Kestrel
    Transfer-Encoding: chunked
    
    [{"date":"2023-04-29","temperatureC":-18,"temperatureF":0,"summary":"Bracing"},{"date":"2023-04-30","temperatureC":54,"temperatureF":129,"summary":"Bracing"},{"date":"2023-05-01","temperatureC":51,"temperatureF":123,"summary":"Balmy"},{"date":"2023-05-02","temperatureC":35,"temperatureF":94,"summary":"Warm"},{"date":"2023-05-03","temperatureC":5,"temperatureF":40,"summary":"Chilly"}]
    

    你會不會覺得:「這麼簡單?這也太無腦了吧?XD」

關於 dotnet user-jwts create 背後偷偷做的事

事實上,這個 dotnet user-jwts create 命令不單單只有建立 Token 這麼簡單,背後還幫你做了幾件事:

  1. 自動在 appsettings.Development.json 組態設定檔中加入以下 Authentication:Schemes:Bearer 設定

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "Authentication": {
        "Schemes": {
          "Bearer": {
            "ValidAudiences": [
              "http://localhost:5000"
            ],
            "ValidIssuer": "dotnet-user-jwts"
          }
        }
      }
    }
    
  2. 自動在 AspNetCoreUserJwtsDemo.csproj 註冊一組 Secret Manager 所需的 <UserSecretsId> 秘密編號

    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <UserSecretsId>98ca3101-9491-4d1c-98d9-d44e2da12ea0</UserSecretsId>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.5" />
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
      </ItemGroup>
    
    </Project>
    
  3. 自動建立 %APPDATA%\Microsoft\UserSecrets\<secrets_GUID>\secrets.json 檔案 (Secret Manager)

    若為 Linux/macOS 會是在這裡 ~/.microsoft/usersecrets/<secrets_GUID>/secrets.json

    這裡包含了開發時期所需的 JWT 簽章金鑰(SigningKeys)!(只有在 Development 環境會用到)

    {
        "Authentication:Schemes:Bearer:SigningKeys": [
            {
                "Id": "751e5adb",
                "Issuer": "dotnet-user-jwts",
                "Value": "3oovYRPOGgclHgCL78QLhk0gYFoQro\u002B4UQZripf02ec=",
                "Length": 32
            }
        ]
    }
    
  4. 自動建立 %APPDATA%\Microsoft\UserSecrets\<secrets_GUID>\user-jwts.json 檔案

    若為 Linux/macOS 會是在這裡 ~/.microsoft/usersecrets/<secrets_GUID>/user-jwts.json

    這裡擁有所有透過 dotnet user-jwts create 建立的 JWT Tokens,範例如下:

    {
        "3165f978": {
            "Id": "3165f978",
            "Scheme": "Bearer",
            "Name": "wakau",
            "Audience": "http://localhost:5000",
            "NotBefore": "2023-04-28T13:25:16+00:00",
            "Expires": "2023-07-28T13:25:16+00:00",
            "Issued": "2023-04-28T13:25:17+00:00",
            "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiIzMTY1Zjk3OCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY4ODMxNiwiZXhwIjoxNjkwNTUwNzE2LCJpYXQiOjE2ODI2ODgzMTcsImlzcyI6ImRvdG5ldC11c2VyLWp3dHMifQ.WergzBiHz7UFAeBotkpaM9lpn8is0J5Fpm0D2yCfB-A",
            "Scopes": [],
            "Roles": [],
            "CustomClaims": {}
        }
    }
    

    預設會簽發為期 91 天的 JWT Tokens

常見的 dotnet user-jwts 命令用法

假設我們的 JWT ID 為 3165f978

  1. 列出所有已發出的 JWT Tokens (dotnet user-jwts list)

    dotnet user-jwts list
    
    Project: 'G:\Projects\AspNetCoreUserJwtsDemo\AspNetCoreUserJwtsDemo.csproj'
    User Secrets ID: '98ca3101-9491-4d1c-98d9-d44e2da12ea0'
    ---------------------------------------------------------------------------------------------------------------------------------------
    |       ID | Scheme |           Audience(s) |                                  Issued On |                                 Expires On |
    ---------------------------------------------------------------------------------------------------------------------------------------
    | 3165f978 | Bearer | http://localhost:5000 | 2023-04-28T13:25:17.0000000+00:00          | 2023-07-28T13:25:16.0000000+00:00          |
    ---------------------------------------------------------------------------------------------------------------------------------------
    
  2. 顯示 JWT Token 的完整資訊 (dotnet user-jwts print)

    dotnet user-jwts print 3165f978 --show-all
    
  3. 刪除已簽發的 JWT Token (dotnet user-jwts remove)

    dotnet user-jwts remove 3165f978
    
  4. 取得目前 JWT Token 的簽發金鑰 (dotnet user-jwts key)

    dotnet user-jwts key
    
    Signing Key: '3oovYRPOGgclHgCL78QLhk0gYFoQro+4UQZripf02ec='
    
  5. 重置目前使用中的 JWT Token 簽發金鑰

    dotnet user-jwts key --reset --force
    
    New signing key created: 'p3Nr5hi9yJtuH5/FAzKnoMrw09RYgE5ERLaM5WvgKok='
    
  6. 重置目前使用中的 JWT Token 簽發金鑰並使用自訂的 Issuer 名稱

    dotnet user-jwts key --reset --force --issuer AspNetCoreUserJwtsDemo
    
    New signing key created: 'p3Nr5hi9yJtuH5/FAzKnoMrw09RYgE5ERLaM5WvgKok='
    

    如果建立多組不同名的 Issuer 的話,他會在 secrets.json 建立多把簽章金鑰

  7. 建立 Issuer 為 AspNetCoreUserJwtsDemo 的 JWT Token (dotnet user-jwts create)

    dotnet user-jwts create --issuer AspNetCoreUserJwtsDemo
    
    New JWT saved with ID '75bf89c7'.
    Name: wakau
    Issuer: AspNetCoreUserJwtsDemo
    
    Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiI3NWJmODljNyIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY5MDQ2MCwiZXhwIjoxNjkwNTUyODYwLCJpYXQiOjE2ODI2OTA0NjEsImlzcyI6IkFzcE5ldENvcmVVc2VySnd0c0RlbW8ifQ.qGWR-uVUDaaBJfJAgNJGNcchLwPtmKI0W6W4OsX5KfA
    

    如果建立多組不同名的 Issuer 的話,他會在 secrets.json 建立多把簽章金鑰

  8. 將所有簽發的 JWT Token 全部清除 (dotnet user-jwts clear)

    dotnet user-jwts clear
    
    Are you sure you want to delete 2 JWT(s) for 'G:\Projects\AspNetCoreUserJwtsDemo\AspNetCoreUserJwtsDemo.csproj'?
    [Y]es / [N]o
    y
    Deleted 2 token(s) from 'G:\Projects\AspNetCoreUserJwtsDemo\AspNetCoreUserJwtsDemo.csproj' successfully.
    

更多應用技巧

  1. 簽發包含 adminuser 角色的 JWT Token

    調整 API Controller 加入 [Authorize(Roles = "admin, manager")] 屬性

    設定 adminmanager 都可以授權存取此 API

    using Microsoft.AspNetCore.Authorization;
    
    [Authorize(Roles = "admin, manager")]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    { ... }
    

    建立 JWT Token 並在 Token 寫入 role 的 Claims

    dotnet user-jwts create --role admin --role user
    

    重新啟動網站 (一定要啟動在 Development 環境喔)

    dotnet run
    

    透過 cURL 發出要求

    curl -i -H "Authorization: Bearer {token}" http://localhost:5000/WeatherForecast
    

    執行結果,你將會得到 HTTP/1.1 200 OK 回應!

    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8
    Date: Fri, 28 Apr 2023 13:20:48 GMT
    Server: Kestrel
    Transfer-Encoding: chunked
    
    [{"date":"2023-04-29","temperatureC":-18,"temperatureF":0,"summary":"Bracing"},{"date":"2023-04-30","temperatureC":54,"temperatureF":129,"summary":"Bracing"},{"date":"2023-05-01","temperatureC":51,"temperatureF":123,"summary":"Balmy"},{"date":"2023-05-02","temperatureC":35,"temperatureF":94,"summary":"Warm"},{"date":"2023-05-03","temperatureC":5,"temperatureF":40,"summary":"Chilly"}]
    

    建立 JWT Token 並在 Token 寫入 role 的 Claims(只有 user 角色)

    dotnet user-jwts create --role user
    

    重新啟動網站 (一定要啟動在 Development 環境喔)

    dotnet run
    

    透過 cURL 發出要求

    curl -i -H "Authorization: Bearer {token}" http://localhost:5000/WeatherForecast
    

    執行結果,你將會得到 HTTP/1.1 200 OK 回應!

    HTTP/1.1 403 Forbidden
    Content-Length: 0
    Date: Fri, 28 Apr 2023 14:18:58 GMT
    Server: Kestrel
    
  2. 簽發包含 myapi:secrets 範圍(Scope)的 JWT Token

    簽發 JWT Token 的時候,可以依據簽發的 scope claim 來限縮特定 JWT Token 的授權範圍,當然我們在程式中也需要定義相對應的 Policy 授權政策。

    先在 DI 容器中宣告政策:(Program.cs)

    builder.Services.AddAuthentication("Bearer").AddJwtBearer();
    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("MyAPIOnly", policy => policy.RequireClaim("scope","myapi:secrets"));
        options.AddPolicy("AdminOnly", policy => policy.RequireClaim(ClaimTypes.Role, "admin"));
        options.AddPolicy("UserOnly", policy => policy.RequireClaim(ClaimTypes.Role, "user"));
    });
    

    建立 JWT Token 並在 Token 寫入 scope 的 Claims(只有 myapi:secrets 範圍)

    dotnet user-jwts create --scope "myapi:secrets" --role "admin"
    

    調整 API Controller 加入 [Authorize(Roles = "admin, manager", Policy = "MyAPIOnly")] 屬性

    using Microsoft.AspNetCore.Authorization;
    
    [Authorize(Roles = "admin, manager", Policy = "MyAPIOnly")]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    { ... }
    

    注意: 上述語法需要這樣理解,角色為 adminmanager,而且同時要符合 MyAPIOnly 政策!

相關連結

留言評論