.NET 8.0でAPIがJWTの検証に失敗するようになった場合は、JsonWebTokenを使うように構成を変更してみる

## はじめに
.NET 6.0から.NET 8.0へバージョンアップ後、JWTで認証を行っているWebAPIが常に401で終了する事象に遭遇しました。.NET 8.0への移行は基本的にはライブラリを.NET8.0に対応した最新版へ置き換えるだけで問題なく作業できましたが、こいつだけ対応に調査が必要だったのでメモを残しておきます。

:::note warn
2024/07/17追記
「現象１：invalid_tokenと表示されJWTの検証が失敗する」の対応方法に問題がありました。以前の実装ではJWTの署名検証が行われなくなるので注意してください。
:::

原因は[破壊的変更 セキュリティ トークン イベントが JsonWebToken を返す](https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events)にある通り.NET8.0の破壊的変更が原因で、JWTを検証する内部の実装がJwtSecurityTokenからJsonWebTokensに置き換えられたためでした。

https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events

.NET6.0から8.0の破壊的変更は次のページにまとまっています。

https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/8.0

## 現象１：invalid_tokenと表示されJWTの検証が失敗する

正しい署名がされたJWTであってもJWTの検証が失敗し、401が返却されます。

```txt:request
curl -X 'GET' \
  'http://localhost:5100/api/v1/Hello' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjNBQTdFQ...略...
```

```txt:response
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Thu, 21 Mar 2024 00:29:22 GMT
Server: Kestrel
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
```

ログには下記のようにセキュリティーキーが見つからないために、署名の検証が失敗したというメッセージが表示されます。

```
[09:19:27 INF] Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10500: Signature validation failed. No security keys were provided to validate the signature.
   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateJWSAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
[09:19:27 INF] Bearer was not authenticated. Failure message: IDX10500: Signature validation failed. No security keys were provided to validate the signature.
```

ブレーキングチェンジには、[SecurityToken](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.identitymodel.tokens.securitytoken)の実装が[JwtSecurityToken](https://learn.microsoft.com/en-us/dotnet/api/system.identitymodel.tokens.jwt.jwtsecuritytoken)から[JsonWebToken](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.identitymodel.jsonwebtokens.jsonwebtoken)に変更されたため、コード内でJwtSecurityTokenをダウンキャストして使っていたり、SignatureValidatorを差し替えている場合に問題が発生するとあります。

:::note warn
2024/07/17追記
以前のJwonWebTokenを単純に返却する方法では、JWTの検証がスキップされJWTの偽装されても検知できません。以下の対応を検討してください。
:::

今回はどちらにも当てはまらない状況でエラーが表示されました。調べていくと、`Microsoft.IdentityModel`から始まるライブラリが推移的に解決され、依存するライブラリのバージョンが崩れてしまったことが問題でした。
StackOverflowにコメントがありました。

https://stackoverflow.com/a/78442187/26401461

`Microsoft.IdentityModel`から始まる複数のライブラリで異なるバージョンが選択されていたので、プロジェクトに明示的に参照したり、不要な参照を削除したりして同じバージョンを参照するように調整して対応しました。
また、`Microsoft.IdentityModel.Tokens`と`System.IdentityModel.Tokens.Jwt`の両方が参照されていたので、`System.IdentityModel.Tokens.Jwt`の参照を削除したところ正しく署名の検証がおこなわれつつ、JWTのエラーも発生しなくなりました。

## 現象２：JwtSecurityTokenHandlerでクレームのマッピングをクリアしている場合は、JsonWebTokenHandlerに置き換える

`subクレーム`を直に参照して値を確認しているコードで、`subクレーム`を参照できない問題が発生しました。
例えば、次のデコードされたJWTの場合、

```json
{
  "iss": "https://*********",
  "aud": [ "https://****/resources" ],
  "scope": [ "openid", "profile" ],
  "amr": [ "pwd" ],
  "client_id": "****",
  "sub": "testuser@example.com",
  "idp": "local",
  
  ... 以下略 ...
}
```

クレームを.NETでウォッチすると次のように表示されました。
`subクレーム`や、`armクレーム`、`idpクレーム`などがnameidentifierなどに変換されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/ad503260-2c68-d9ac-7774-a1358a1f3aa6.png)

Program.cs内で`JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`を呼び出してマッピングを構成している部分があったので、`JwtSecurityTokenHandler`から`JsonWebTokenHandler`に変更しました。

```diff_c_sharp
-JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
+JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/2191563d-a132-6829-b4a6-4fe33f566cc8.png)

## おわりに

バージョンアップ時は破壊的変更をちゃんと読もう