﻿# 使用 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](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/48c6029c14a8d1c25a52-235182150-bdb5dd95-e61e-498e-8af3-3473c06bb4dd.jpg)

### 建立範例專案

1.  鎖定 .NET SDK 版本到 `7.0.203` (含之後的版本)
    
    ```bash
    dotnet new globaljson --sdk-version 7.0.203 --roll-forward latestFeature
    ```
    
2.  建立 ASP.NET Core Web API 專案
    
    ```bash
    dotnet new webapi -n AspNetCoreUserJwtsDemo
    cd AspNetCoreUserJwtsDemo
    dotnet new gitignore
    git init
    git add .
    git commit -m "Initial commit"
    ```
    

### 初始化認證與授權

1.  安裝 [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) 套件
    
    ```bash
    dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
    ```
    
2.  修改 `Program.cs` 註冊 JwtBearer 服務到 DI 容器
    
    ```csharp
    builder.Services.AddAuthentication("Bearer").AddJwtBearer();
    builder.Services.AddAuthorization();
    ```
    
3.  修改 `Program.cs` 設定將所有 Controller 都要需要走 Bearer Token 認證與授權
    
    ```csharp
    app.MapControllers().RequireAuthorization();
    ```
    
4.  理論上所有的 API 都無法存取才對
    
    先調整 Properties/launchSettings.json 檔案
    
    ```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"
          }
        }
      }
    }
    ```
    
    啟動網站
    
    ```bash
    dotnet run
    ```
    
    透過 cURL 發出要求
    
    ```bash
    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
    
    ```bash
    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` 環境喔)
    
    ```bash
    dotnet run
    ```
    
    透過 cURL 發出要求
    
    ```bash
    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` 設定
    
    ```json
    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "Authentication": {
        "Schemes": {
          "Bearer": {
            "ValidAudiences": [
              "http://localhost:5000"
            ],
            "ValidIssuer": "dotnet-user-jwts"
          }
        }
      }
    }
    ```
    
2.  自動在 `AspNetCoreUserJwtsDemo.csproj` 註冊一組 Secret Manager 所需的 `<UserSecretsId>` 秘密編號
    
    ```xml
    <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](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets))
    
    若為 Linux/macOS 會是在這裡 `~/.microsoft/usersecrets/<secrets_GUID>/secrets.json`
    
    這裡包含了開發時期所需的 JWT 簽章金鑰(SigningKeys)！（只有在 `Development` 環境會用到）
    
    ```json
    {
        "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，範例如下：
    
    ```json
    {
        "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`)
    
    ```bash
    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`)
    
    ```bash
    dotnet user-jwts print 3165f978 --show-all
    ```
    
3.  刪除已簽發的 JWT Token (`dotnet user-jwts remove`)
    
    ```bash
    dotnet user-jwts remove 3165f978
    ```
    
4.  取得目前 JWT Token 的簽發金鑰 (`dotnet user-jwts key`)
    
    ```bash
    dotnet user-jwts key
    ```
    
    ```
    Signing Key: '3oovYRPOGgclHgCL78QLhk0gYFoQro+4UQZripf02ec='
    ```
    
5.  重置目前使用中的 JWT Token 簽發金鑰
    
    ```bash
    dotnet user-jwts key --reset --force
    ```
    
    ```
    New signing key created: 'p3Nr5hi9yJtuH5/FAzKnoMrw09RYgE5ERLaM5WvgKok='
    ```
    
6.  重置目前使用中的 JWT Token 簽發金鑰並使用自訂的 Issuer 名稱
    
    ```bash
    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`)
    
    ```bash
    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`)
    
    ```bash
    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.  簽發包含 `admin` 與 `user` 角色的 JWT Token
    
    調整 API Controller 加入 `[Authorize(Roles = "admin, manager")]` 屬性
    
    > 設定 `admin` 或 `manager` 都可以授權存取此 API
    
    ```csharp
    using Microsoft.AspNetCore.Authorization;
    
    [Authorize(Roles = "admin, manager")]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    { ... }
    ```
    
    建立 JWT Token 並在 Token 寫入 `role` 的 Claims
    
    ```bash
    dotnet user-jwts create --role admin --role user
    ```
    
    重新啟動網站 (一定要啟動在 `Development` 環境喔)
    
    ```bash
    dotnet run
    ```
    
    透過 cURL 發出要求
    
    ```bash
    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` 角色）
    
    ```bash
    dotnet user-jwts create --role user
    ```
    
    重新啟動網站 (一定要啟動在 `Development` 環境喔)
    
    ```bash
    dotnet run
    ```
    
    透過 cURL 發出要求
    
    ```bash
    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`)
    
    ```csharp
    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` 範圍）
    
    ```bash
    dotnet user-jwts create --scope "myapi:secrets" --role "admin"
    ```
    
    調整 API Controller 加入 `[Authorize(Roles = "admin, manager", Policy = "MyAPIOnly")]` 屬性
    
    ```csharp
    using Microsoft.AspNetCore.Authorization;
    
    [Authorize(Roles = "admin, manager", Policy = "MyAPIOnly")]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    { ... }
    ```
    
    > 注意: 上述語法需要這樣理解，角色為 `admin` 或 `manager`，而且同時要符合 `MyAPIOnly` 政策！
    

### 相關連結

-   本文範例原始碼: [https://github.com/doggy8088/AspNetCoreUserJwtsDemo/commits/main](https://github.com/doggy8088/AspNetCoreUserJwtsDemo/commits/main)
-   [Generate tokens with dotnet user-jwts | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt-authn?view=aspnetcore-7.0&tabs=linux&WT.mc_id=DT-MVP-4015686)
-   [aspnetcore/src/Tools/dotnet-user-jwts at main · dotnet/aspnetcore · GitHub](https://github.com/dotnet/aspnetcore/tree/main/src/Tools/dotnet-user-jwts) (原始碼)
-   [Create development-time JWTs using dotnet user-jwts](https://craftbakery.dev/create-jwt-using-dotnet-user-jwts/)
