OpenID Connect実装調査 (oidc-provider編)

OpenID Connectへの理解を深めるために、[Certified OpenID Connect Implementations](https://openid.net/developers/certified/)で紹介されているOSSを対象に、仕様の解釈やデータモデルなどの設計思想を調べている。今回はNode.jsでOpenID Providerを実装したライブラリである[oidc-provider](https://github.com/panva/node-oidc-provider)を調査した内容を紹介する。

### 調査対象バージョン

* oidc-provider v6.29.7
* （Node.js v12.19.0）※ ライブラリを組み込んだ調査用アプリの実行時に利用

### 調査用アプリとデバッグ環境の準備

ライブラリの設計を理解するには、実際に動かしてみてデバッグ実行で動作の確認ができると効率が良い。今回は調査用アプリ（oidc-provider-study）を作り、Node.jsのInspectorを有効化して起動して、Visual Studio Codeでデバッグ実行できるようにした。

* [調査用アプリ（index.js）](https://github.com/unhurried/oidc-provider-study/blob/master/index.js)
* [Visual Studio Code起動設定](https://github.com/unhurried/oidc-provider-study/blob/master/.vscode/launch.json)

アプリを起動すると、以下のURLで[OpenID Connect Discovery 1.0](http://openid.net/specs/openid-connect-discovery-1_0.html)で定義される各種エンドポイントや対応する機能に関する情報を取得できる。

```
http://localhost:3000/.well-known/openid-configuration
```

### Client設定項目

基本的にはOAuth 2.0もしくはOpenID ConnectでDynamic Client Registrationのメタデータとして定義される項目に対応している。

これに加えて以下の設定項目を独自に定義している。

* introspection_endpoint_auth_method
* introspection_endpoint_auth_signing_alg
* revocation_endpoint_auth_method
* revocation_endpoint_auth_signing_alg

これらはOAuth 2.0のRevocation Endpoint ([RFC 7009](https://tools.ietf.org/html/rfc7009)) とIntrospection Endpoint ([RFC7662](https://tools.ietf.org/html/rfc7662)) での認証方法を指定するためのパラメータであり、[OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html) で定義されるToken Endpointの認証方法の設定 (token_endpoint_auth_method, token_endpoint_auth_signing_alg) と同じ設定値を受け付ける。

その他細かいところでは、[RFC7591 - OAuth 2.0 Dynamic Client Registration Protocol](https://tools.ietf.org/html/rfc7591)に定義されるsoftware_idとsoftware_versionはデフォルトではサポートされていないが、独自のClientメタデータを追加する[extraClientMetadata](https://github.com/panva/node-oidc-provider/blob/v6.29.6/docs/README.md#extraclientmetadata)を設定することで対応できる。（[参考](https://github.com/panva/node-oidc-provider/issues/641)）

### ユーザー認証機能の分離

oidc-providerは名前の通りOpenID Providerの実装に特化したライブラリであるため、ユーザー認証などOpenID Connect仕様に含まれない機能は別に実装して組み込む仕組みになっている。これらの機能はoidc-providerと同じオリジンの別のパスに実装する (※) 必要があり、oidc-providerは必要なタイミングでこれらのパスにリダイレクトを行う。

※ この実装を追加しやすいように、oidc-providerはexpressやkoaなど様々フレームワークにマウントできるようになっている。（[参考](https://github.com/panva/node-oidc-provider/blob/v6.29.7/docs/README.md#mounting-oidc-provider)）

oidc-providerは、ユーザー認証やユーザーのリソースへのアクセス認可など、ユーザー操作が必要となるアクションをインタラクション (Interaction) というデータモデルで扱う。Authorization Requestを受け付けた際に一意な識別子（Authorization Request UID) を発行し、ユーザー操作が必要な場合にはこの識別子に紐づけて必要なユーザー操作などの情報を格納したInteractionデータを保存する。そして、ユーザー操作を担当するパス （[interactions.url](https://github.com/panva/node-oidc-provider/blob/v6.29.7/docs/README.md#interactionsurl)）にリダイレクトする際に短期間のCookieを発行してこの識別子を伝達することで、ユーザー操作処理にてInteractionデータを検索できるようにする。

大まかな流れは以下の通り：

* oidc-providerがAuthorization Requestを受けて、識別子（Authorization Request UID）を生成する。
* oidc-providerがAuthorization Requestを現在のログインセッションの状態と比較する。
* この結果、ユーザー操作（ユーザー認証やリソースの認可など）が必要であれば、Interactionデータを保存する。
  * Interactionデータの例（一部のプロパティのみを抜粋）※ 参考：[Interactionクラスの型定義](https://github.com/panva/node-oidc-provider/blob/v6.29.7/types/index.d.ts#L180)

    ```javascript
    {
      // Authorization Request UID
      uid: "kfXJ3BWh0UOaLexEE4jG4",
      // Authorization Requestパラメータ
      params: { client_id: "foo", response_type: "code", ... },
      // 必要なユーザー操作に関する情報
      prompt: { name: "login", reasons: ["no_session"], ... },
      // ユーザーインタラクション完了後のリダイレクト先
      returnTo: "http://localhost:3000/auth/kfXJ3BWh0UOaLexEE4jG4",
    }
    ```

* 短期間のCookieに識別子を埋め込んで、ユーザー操作を担当するパス （[interactions.url](https://github.com/panva/node-oidc-provider/blob/v6.29.7/docs/README.md#interactionsurl)）にリダイレクトする。
  * 発行するCookieの例

    ```
    Set-Cookie: _interaction=kfXJ3BWh0UOaLexEE4jG4; path=/interaction/kfXJ3BWh0UOaLexEE4jG4; expires=xxx; samesite=lax; httponly
    Set-Cookie: _interaction_resume=kfXJ3BWh0UOaLexEE4jG4; path=/auth/kfXJ3BWh0UOaLexEE4jG4; expires=xxx; samesite=lax; httponly
    ```

* ユーザー操作処理では、Cookieから取得した識別子からInteractionデータを取得する。
* ユーザー操作が完了した場合は、その結果をInteractionデータに更新して登録し、再度oidc-providerにリダイレクトする。
  * Interactionデータの例（一部のプロパティのみを抜粋）

    ```javascript
    {
      // Authorization Request UID
      uid: "kfXJ3BWh0UOaLexEE4jG4",
      // ユーザー操作の結果（下記はjohnsmithというユーザーでのログインした際のデータ）
      result: {
        login: {　account: "johnsmith" },
      },
    }
    ```

* oidc-providerがAuthorization Requestと再度比較を行い、ログインセッションを生成（更新）し、認可コードを発行（Authorization Code Flowの場合）する。

なお、実際にユーザー操作処理を実装する際にはInteractionデータを直接参照・更新する必要はなく、公式ドキュメントの[User Flow](https://github.com/panva/node-oidc-provider/blob/v6.29.7/docs/README.md#user-flows)に記載されるoidc-providerが提供するユーティリティメソッドを利用すればよい。

### セッションに関係するデータモデル

（ログイン）セッションを中心に関係するデータモデルをER図に整理した。（概要を理解しやすくするために一部の属性のみを抽出している。）

![データモデル](https://gist.githubusercontent.com/unhurried/9ae0db89ac8838a5333072fa88aafc0a/raw/6683b68afb703eb5e88c7d6faa85d978c67a48f8/oidc-provider_data-model.svg)

#### セッションと認可状態の関連

oidc-providerはユーザーがクライアントに対して認可（同意）した内容（scopeとclaim）を、セッションに従属する認可状態（[Client Authorization State](https://github.com/panva/node-oidc-provider/blob/v6.29.7/types/index.d.ts#L163)）として管理する。この認可状態はクライアント単位で集約され、同じセッション内で同じクライアントに対して異なるscopeやclaimに対する認可がされた場合は、これらがマージされて保存される。

認証状態がセッションに従属するため、ログアウト（セッション無効化）時には、この認可状態も削除される。OAuthでAPI公開しているサービスには、ユーザーのクライアントに対する認可履歴を記録して、以降の認可要求では同意確認をスキップできるものがあるが、oidc-providerでこれを実現するには、独自に認可履歴を管理する実装を作り込む必要がある。

#### セッションとトークンの関連

oidc-providerでは、セッションとトークン（本章ではAuthorization Code, Access Token, Refresh Tokenをまとめてトークンと呼ぶ）を紐づける（セッション無効化時に紐づくトークンを無効とする）条件を[expiresWithSession](https://github.com/panva/node-oidc-provider/tree/v6.29.7/docs#expireswithsession)設定で指定できる。デフォルトではトークンのscopeにoffline_accessが含まれない場合にセッションとの紐づけが設定される。

※ ドキュメント上は「Authorization CodeとImplicit Grantで発行されたAccess Tokenのみがセッションに紐づけた削除に対応している」との記載であるが、ドキュメントの更新が追い付いていないだけで6.x以降では全てのAccess Token, Refresh Tokenも対象となっている。（[参考](https://github.com/panva/node-oidc-provider/issues/459)）

実装上はトークンの正当性検証処理にて、セッションと紐づけがある（expiresWithSessionプロパティがtrueの）トークンの場合には、紐づくセッションの状態を確認する仕組みとなっている。（参考：[is_session_bound.js](https://github.com/panva/node-oidc-provider/blob/v6.29.7/lib/models/mixins/is_session_bound.js#L22)）

なお、OpenID ConnectのLogout Endpoint等でのセッション削除時にも、紐づくAuthorization Code, Access Token, Refresh Tokenの削除が行われる。ただし、認可状態がクライアントごとに集約されて管理されるデータモデルの都合上、削除対象のトークンが全て削除される訳ではない。

#### 参考

oidc-providerのデータモデルは以下から読み取ることができる。

* [adapter](https://github.com/panva/node-oidc-provider/blob/v6.29.7/docs/README.md#adapter)（データ永続化処理）の[スケルトンコード](https://github.com/panva/node-oidc-provider/blob/v6.29.7/example/my_adapter.js)
* [型定義ファイル](https://github.com/panva/node-oidc-provider/blob/v6.29.7/types/index.d.ts)