IdentityServer 4 v3.xからv4.xへの移行手順まとめ

# はじめに
.NETで標準的（？）に利用されているOIDCプロバイダーである[IdentityServer 4](https://github.com/IdentityServer/IdentityServer4)ですが、現在のバージョンの更新は2022/11までになっています。移行先は[Duende Software](https://duendesoftware.com/)が公開する[Duende IdentityServer](https://github.com/DuendeSoftware/IdentityServer)です。

IdentityServer 4 はデータベースにEntityFrameworkを使っているか、利用中のバージョンが4.x以前かどうかで移行方法が大きく変わってきます。それぞれの移行方法は[Duendeのドキュメント](https://docs.duendesoftware.com/identityserver/v5/upgrades/)にまとめられているので確認してみてください。この記事では、Duende IdentityServerへの前段階として3.x -> 4.x への移行でドキュメントには書かれていない部分を中心に説明します。

# IdentityServer 4とDuende IdentityServerの使用料的なはなし
今回はIdentityServer 4のv3.x -> v4.x のバージョンアップなので直接の話題ではありませんが、後継の[Duende IdentityServer](https://github.com/DuendeSoftware/IdentityServer)は独自の[DUENDE™ SOFTWARE LICENSE AGREEMENT](https://duendesoftware.com/license/identityserver.pdf)を利用しているため、今後[Duende IdentityServer](https://github.com/DuendeSoftware/IdentityServer)を商用で利用する場合は注意が必要です。

ライセンスの内容は各自確認していただくとして、お金の話だけだとざっくりこんな感じです。

- 個人が非商用で利用する場合は無料
- 事業体全体の収入が100万米ドル未満であれば無料
- 事業体全体の収入が100万米ドル以上であれば有償

→ [価格表](https://duendesoftware.com/products/identityserver#pricing)

# 移行手順
ざっくり下記の3つのステップを実施します。

1. ライブラリのバージョンアップ
1. 3.x->4.xでブレーキングチェンジとなった項目の修正
1. PersistedGrantDbContext及びConfigurationDbContextのマイグレーション適用（使っていれば）

うちの認証サーバーではPersistedGrantDbContextとConfigurationDbContextは利用していないので、この記事では触れませんがそれぞれのRDBMS毎のマイグレーション方法が[Migrating Your IdentityServer4 v3 Database to IdentityServer4 v4](https://www.identityserver.com/articles/migrating-your-identityserver4-v3-database-to-identityserver4-v4)に詳しくまとまっているので確認してみてください。

# 1. ライブラリのバージョンアップ

NuGetで置き換えてもよいですし、csprojファイルを直に編集してもよいです。
うちの環境では、PersistedGrantStoreに前述の通りデータベースではなくRedisを使っているので、関連する`IdentityServer4.Contrib.RedisStore`も同時にバージョンアップしています。

```diff
  <ItemGroup>
-   <PackageReference Include="IdentityServer4" Version="3.0.2" />
+   <PackageReference Include="IdentityServer4" Version="4.1.2" />
-   <PackageReference Include="IdentityServer4.Contrib.RedisStore" Version="3.1.2" />
+   <PackageReference Include="IdentityServer4.Contrib.RedisStore" Version="4.0.0" />
    ... 略 ...
  </ItemGroup>
```

# 2. 3.x->4.xでブレーキングチェンジとなった項目の修正

IdentityServerのこのIssueが参考になります。[Migration from v3 to V4](https://github.com/IdentityServer/IdentityServer4/issues/4592)対応が必要だった項目を見ていきましょう。

## ApiResourceから細分化されたApiScopeを追加する

IdentityServer 4 v3.xでは、次のようにApiResourceの中にそのApiリソースで利用するスコープを直に記載していました。

``` Config.cs
public class Config
{
    public IEnumerable<ApiResource> ApiResources =>
        new ApiResource[] { new ApiResource("api1", "api1 description") };

    // ... 略 ...
}
```

``` startup.cs
var builder = services.AddIdentityServer()
    .AddOperationalStore(options =>
    {
        options.RedisConnectionString = "xxxxxx";
        options.KeyPrefix = cfg.CachingStorePrefix;
    })
    .AddInMemoryApiResources(Config.ApiResources)
    .AddInMemoryIdentityResources(Config.IdentityResources)
    .AddInMemoryClients(Config.IdentityClients)
    .AddProfileService<CustomProfileService>();
```
IdentityServer 4 v4.xでは、ApiResouce が参照する ApiScope を汎用的に扱えるように別々に定義するように仕様変更が行われています。今回のような単純なAPI定義であれば、ApiResouce と同じ内容の ApiScope を定義して Startup 時に登録してあげればよいです。

``` Config.cs
public class Config
{
    public static IEnumerable<ApiScope> ApiScopes =>
        new ApiScope[] { new new ApiScope("api1", "api1") };

    public IEnumerable<ApiResource> ApiResources =>
        new ApiResource[] { new ApiResource("api1", "api1 description") };

    // ... 略 ...
}
```

``` startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ... 略 ...

    var builder = services.AddIdentityServer()
        .AddOperationalStore(options =>
        {
            options.RedisConnectionString = "xxxxxx";
            options.KeyPrefix = cfg.CachingStorePrefix;
        })
    　  .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryApiResources(Config.ApiResources)
        .AddInMemoryIdentityResources(Config.IdentityResources)
        .AddInMemoryClients(Config.IdentityClients)
        .AddProfileService<CustomProfileService>()
        ;

    // ... 略 ...
}
```


詳細については、Defining Resourcesドキュメントに[V4の変更点とマイグレーションステップ](https://identityserver4.readthedocs.io/en/latest/topics/resources.html?highlight=v4#migration-steps-to-v4)を参照してください。
このドキュメントの v3.x 版と v4.x 版を見比べてもどのような変更があったかを確認できると思います。
https://identityserver4.readthedocs.io/en/3.1.0/topics/resources.html#defining-api-resources
https://identityserver4.readthedocs.io/en/latest/topics/resources.html#api-resources

## クレームにaudクレームが含まれない場合は、EmitStaticAudienceClaim を有効にする

トークンのリクエスト方法によっては、audクレームが含まれない場合があります。
その場合はEmitStaticAudienceClaimプロパティーをtrueにしてあげると含まれるようになります。

https://identityserver4.readthedocs.io/en/latest/topics/resources.html#authorization-based-on-scopes

``` startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ... 略 ...
    var builder = services.AddIdentityServer(options => {
        options.EmitStaticAudienceClaim = false;
    })
    // ... 略 ...
}
```

## 削除されたPublicOriginの代わりにミドルウェアを作成する

v3.x では、リバースプロキシの後方に IdentityServer を配置した場合に、ホスト名を正しく解決するためにPublicOriginというプロパティーが用意されていました。v4.x ではこのプロパティーが削除されているため、startup 時に新たにミドルウェアを登録して解決してあげます。
https://github.com/IdentityServer/IdentityServer4/issues/4535

``` startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... 略 ...
    app.Use(async (ctx, next) =>
    {
        ctx.SetIdentityServerOrigin("https://foo.com");
        await next();
    });
    // ... 略 ...
}
```

## token エンドポイントのリクエスト時にコンテンツタイプ multipart/form-data が拒否される

もともと[OpenId Connectの仕様書](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.3.1)では token エンドポイントへのリクエストには、application/x-www-form-urlencoded で行うように記載されていましたが、v4.x 以前まではコンテンツタイプが multipart/form-data であっても処理を受け付けていました。
v4.x では、[このIssue](https://github.com/IdentityServer/IdentityServer4/issues/4340)の対応時にコンテンツタイプが application/x-www-form-urlencoded 出ない場合はエラーを返すように修正されています。

token エンドポイントリクエスト時にコンテンツタイプを multipart/form-data で送ってきているクライアントには、リクエスト時のコンテンツタイプを application/x-www-form-urlencoded に変更してもらう必要があります。

## QuickStart変更に伴う修正

IdentityServer をアプリに組み込む場合は、リファレンス実装となる QuickStart をベースに組み込みを行うことが多いです。
QuickStart も v4.x リリース時に大きく変更が入っているので、QuickStart をそのまま利用していたりする場合はこのあたりも合わせて確認する必要があるかもしれません。

- [ConsentResponse.Deniedがなくなったことによる修正](https://github.com/IdentityServer/IdentityServer4/issues/4592#issuecomment-656720575)
- [SignInAsyncがなくなったことによる修正](https://github.com/IdentityServer/IdentityServer4/issues/4592#issuecomment-656720575)
- [DefaultInboundClaimTypeMap.Clearの代替え](https://github.com/IdentityServer/IdentityServer4/issues/4592#issuecomment-701387343)

# おわりに
とりあえず、コミットログをたよりに修正した内容を列挙してみました。
さて、次は Duende IdentityServer への移行だけれど、これもちょっと腰が重いなぁ。
このレベルのミドルウェアはMSが保守してほしい気がしないでもないです...