Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

.NET 7で追加されたdotnet user-jwtsコマンドでAPIのローカル開発を楽にする

こんにちは、MLBお兄さんこと松村です。オルターブース Advent Calendar 2022 3日目のブログです。
前日はきよぴーのターンでした。もう5ヶ月が経つなんて早いですね。

aadojo.alterbooth.com


先日のブログでも紹介したとおり、.NET Conf 2022 Recap Event 福岡で登壇しました。
イベントについてはこちらをご覧ください。そのうちアーカイブ動画が公開されると思います。

msdevjp.connpass.com

このイベントでお話ししたのは ASP.NET Core Minimal API ですが、資料については公開していますので、こちらをご覧ください。

speakerdeck.com

github.com

さて、この登壇のなかで個人的な新しい発見として dotnet user-jwts というコマンドラインです。
このコマンドラインは、開発環境で使用するための JWT (JSON Web Tokens) を発行・管理するための機能です。

learn.microsoft.com

.csproj のディレクトリで dotent user-jwts create を実行すると JWT が発行されます。

$ dotnet user-jwts create
New JWT saved with ID 'f60f7427'.
Name: YutaMatsumura

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Ill1dGFNYXRzdW11cmEiLCJzdWIiOiJZdXRhTWF0c3VtdXJhIiwianRpIjoiZjYwZjc0MjciLCJhdWQiOlsiaHR0cDovL2xvY2FsaG9zdDo4OTkyIiwiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzYiLCJodHRwOi8vbG9jYWxob3N0OjUwNzQiLCJodHRwczovL2xvY2FsaG9zdDo3MDM1Il0sIm5iZiI6MTY2OTczMzg3NSwiZXhwIjoxNjc3NTk2Mjc1LCJpYXQiOjE2Njk3MzM4NzYsImlzcyI6ImRvdG5ldC11c2VyLWp3dHMifQ.MBcGYPznnZfx6ekDvbLFrocAaOqHml22hjb-dnajIyw

この JWT を https://jwt.ms/ で解析すると、このような内容になっていることがわかります。

{
  "alg": "HS256",
  "typ": "JWT"
}.{
  "unique_name": "YutaMatsumura",
  "sub": "YutaMatsumura",
  "jti": "f60f7427",
  "aud": [
    "http://localhost:8992",
    "https://localhost:44336",
    "http://localhost:5074",
    "https://localhost:7035"
  ],
  "nbf": 1669733875,
  "exp": 1677596275,
  "iat": 1669733876,
  "iss": "dotnet-user-jwts"
}.[Signature]

unique_namesub については 環境変数 USERNAME が使われていることがわかります。

$ echo $USERNAME
YutaMatsumura

aud については、対象のアプリケーションの URL が使われています。
アプリケーション URL は Properties/launchSettings.json に記載されています。

$ cat Properties/launchSettings.json
{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:8992",
      "sslPort": 44336
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5074",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:7035;http://localhost:5074",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

当然ですが、独自のクレームを JWT に含めることもできます。

$ dotnet user-jwts create --claim "team=Yankees"
New JWT saved with ID '3f1abc05'.
Name: YutaMatsumura
Custom Claims: [team=Yankees]

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Ill1dGFNYXRzdW11cmEiLCJzdWIiOiJZdXRhTWF0c3VtdXJhIiwianRpIjoiM2YxYWJjMDUiLCJ0ZWFtIjoiWWFua2VlcyIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0Ojg5OTIiLCJodHRwczovL2xvY2FsaG9zdDo0NDMzNiIsImh0dHA6Ly9sb2NhbGhvc3Q6NTA3NCIsImh0dHBzOi8vbG9jYWxob3N0OjcwMzUiXSwibmJmIjoxNjY5NzM0OTYwLCJleHAiOjE2Nzc1OTczNjAsImlhdCI6MTY2OTczNDk2MSwiaXNzIjoiZG90bmV0LXVzZXItand0cyJ9.jeaHMf-pRK_rYCjP1jraSJNxSM7q9ia0SWWerSrBNsQ

また JWT 発行に際して、.csproj に紐づくユーザーシークレットも登録されていました。

$ dotnet user-secrets list
Authentication:Schemes:Bearer:SigningKeys:0:Value = ji3voylTNLlQtkVLGOQQQbHfqVK3NOlmNEzRwoOpl2s=
Authentication:Schemes:Bearer:SigningKeys:0:Length = 32
Authentication:Schemes:Bearer:SigningKeys:0:Issuer = dotnet-user-jwts
Authentication:Schemes:Bearer:SigningKeys:0:Id = 44ff75ec

では、この JWT が実際に認証時のトークンとして識別してくれるのかを確認します。
ASP.NET Core Minimal API で、認証必須の API を準備します。

var builder = WebApplication.CreateBuilder(args);

// Swaggerの構成
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<SwaggerGeneratorOptions>(options =>
{
    options.InferSecuritySchemes = true;
});

// 認証の構成
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(options =>
    options.AddPolicy("AdminsOnly", b => b.RequireClaim("admin", "true")));

var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();

// admin=true というクレームをもつ JWT 認証が必要なAPI
//   ※EnableOpenApiWithAuthenticationは独自に用意した拡張メソッド(本記事には関係ない)
app.MapGet("/admin", () => "You are Administrator")
    .RequireAuthorization("AdminsOnly")
    .EnableOpenApiWithAuthentication();

EnableOpenApiWithAuthentication について知りたい方はこちらを参照してください。

JWT を指定しない状態で API を実行すると、HTTP 401 (Unauthorized) が返ります。

次に admin=true というクレームをもつ JWT を発行します。
dotnet user-jwts create --claim admin=true

その JWT を Authorization ヘッダーに指定した状態で API を実行すると、HTTP 200 (OK) となり、正しく結果を取得することができました。

なお、JWT は指定するものの クレームが無い(一致しない)場合は、HTTP 403 (Forbidden) となります。


ローカル環境での開発のためだけにわざわざ IdP を準備したり、デバッグのたびに JWT を発行する必要がなくなるのは、とても開発が楽になりますね。

www.alterbooth.com

cloudpointer.tech

www.alterbooth.com