OIDCのOPを実装したときに悩んだ箇所

## はじめに

この記事は[認証認可技術 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/identity)の14日目の記事です。

OpenID Connect の OP をスクラッチで実装したときにRFC上では表現されないような箇所に悩まされました。

- 各トークンの有効期限はどう設計すべきか
- 認可コードとトークンの関係性
- アクセストークンの有効期限＝同意画面を出す、出さないなのか？
- OIDC範囲外ですが、パスワードポリシーはどうするべきか

などなど、上記について記載していきたいと思います。

## システム概要

あまり本編とは関係ないですが、こんなシステムを作りました。

- OpenID Connect の IdPとして振る舞える
  - 一通りのフローをサポートする（Device Autorization grant含む。将来的にCIBAやFAPIにも対応したい）
  - クライアント管理やトークン管理など運用機能も必要
- 認証手段は、
  - 自前のDBへの問い合わせ（ID/PW）
  - 自前のDBへの問い合わせ（FIDO2、FIDO UAF1.1）
  - 外部IdPへの問い合わせ（Google,Facebook,etc）
  - 独自実装の外部システムへの問い合わせ（なんちゃってOIDCだったり、ROPCだったり）
- 多要素認証手段
  - SMS/TEL
  - FIDO U2F
  - TOTPベーストークン

## 各種トークンの有効期限の考え方

OIDCやOAuthでは、認可コード、アクセストークン、リフレッシュトークン、IDトークンといったクレデンシャル情報をIdP/OP側で管理する必要がありますが、RFCでは、これらの有効期限の値に関する具体的な記述は殆どありません。

調べた限りでは、 [OAuth2.0仕様](https://tools.ietf.org/html/rfc6749)にて、

> A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
> (認可コードの有効期限は最大でも10分を推奨)

という記載がある程度でした。

その他の記載としては、

[OIDCのSecurityConsiderations](https://openid.net/specs/openid-connect-core-1_0.html#TokenLifetime)にて、

> 16.18.  Lifetimes of Access Tokens and Refresh Tokens
> Access Tokens might not be revocable by the Authorization Server. Access Token lifetimes SHOULD therefore be kept to single use or very short lifetimes.
> (アクセストークンは、認可サーバ側でrevokeできない可能性があるので、アクセストークンの有効期限は、一回使う程度の時間か、非常に短い時間にすべき)
> (略)
> The Authorization Server SHOULD clearly identify long-term grants to the User during Authorization.
> (認可サーバは、ユーザが同意する際に、長期間の認可（リフレッシュトークンなど）を与えようとしていることを明示すべきである)

という程度です。「短い時間にせよ。」と言われても、どれぐらいが妥当なのかよくわかりません。
ユースケースによって有効期限は変わりそうです。

ここまで考えたときに、

**そもそもトークンの有効期限って固定の値でいいんだろうか？**

という疑問がわきました。

以下のようなケースもあるのではないかと考えました。

**クライアントによって、アクセストークンの有効期限を変えたい**

- publicクライアント向けに発行するアクセストークンの有効期限は短くしたい
- グループ内企業が作るクライアントはガバナンスが効くから長めに、サードパーティのクライアント向けには短めにしたい

**スコープによって、アクセストークンの有効期限を変えたい**

- write権限に対応するscopeに紐づくアクセストークンは短くしたい
- 大した権限の無いscopeのアクセストークンは長めでも良いのではないか

**フローによって、アクセストークンの有効期限を変えたい**

- Hybrid flowにおいて、認可エンドポイントから返却されたアクセストークンは短く、トークンエンドポイントから返却されたアクセストークンは長くしたい

[OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13)にも以下のような記述があるので、クライアントやスコープによってトークンの有効期限を変えるのはむしろ推奨なのかもしれません。

> Refresh tokens SHOULD expire if the client has been inactive for some time, i.e., the refresh token has not been used to obtain fresh access tokens for some time. The expiration time is at the discretion of the authorization server. It might be a global value or determined based on the client policy or the grant associated with the refresh token (and its sensitivity).
> (リフレッシュトークンの有効期間は認可サーバの裁量による。グローバル値でもいいし、クライアントのポリシーやトークンに関連付けられたgrantや機密性に基づいて決定される)

これらの検討を踏まえ、結局以下のような実装を行いました。

- クライアント単位、スコープ単位でトークンの有効期限の値を設定できるようにする
  - IDトークンの有効期限( `exp` クレーム)も念の為上記のように設定できるようにする
- 異なる有効期限のスコープが複数指定された場合（例：`scope=read+write`）、短い方の有効期限にする
- 認可コードの有効期限は統一する。（クライアントの性質やscopeに依存するものではないので）

*余談：自分が調べた限り、Auth0やCognito、Authleteでは、設定レベルで上記を満たすことは不可そうでした。
Authleteはトークンを弄るAPIを使って無理やり実現することは可能そうです。（IDトークンは署名解いて`exp`変えて再署名する必要があります。）
この粒度の制御が必要な要件を満たすとなると、製品導入するかスクラッチ実装レベルなのかなという気がします。*
(2019/12/14 13:30追記)Authleteは2.0からスコープ単位で有効期限設定可能。2.1からは クライアント単位で設定可能とのことです。

## 「認可した」という行為の考え方

続いて、各トークンの保持の仕方を設計する際にも悩みがありました。

普通に考えると、認可コード：アクセストークンは1:1で紐づけて設計すべきものかと思います。トークンエンドポイントに送信された認可コードに対してアクセストークンは一つのみ払い出されるからです。（Implicitフローは一旦省略（汗））以下は概念です。

|認可コード|アクセストークン|リフレッシュトークン|有効期限|
|:--|:--|:--|:--|
|03fbss35dsa|4b5b7e59-254b-4032-8919|25e4033f-a9d6-53414f1de624|30分|

一方で、ユーザーが「あるクライアントに対して、このスコープ内の権限を認可した」という情報はどのように管理すべきでしょうか。
認可コードとユーザー、スコープ、クライアントを紐付けて管理するのが良い気がします。認可コードは短期間の有効期限かつ、一度トークンリクエストを行うと無効化する必要があります。履歴管理の観点からも論理削除しないとまずいですね。

|認可コード|ユーザーID|スコープ|クライアント|有効期限|
|:--|:--|:--|:--|:--|
|03fbss35dsa|tom|openid+profile|ClientA|60秒|
|deef0f5da11|tom|openid+profile+address|ClientA|60秒|
|5da11deeghy|tom|openid+profile|ClientB|60秒|

ところで、同意画面があります。

Googleとかに認可要求すると、ログイン後に、クライアントにこんな情報渡すけど良いのかお前？って聞かれるやつです。

あれ、同じクライアントに対して同じスコープの認可要求だったら、一度同意したら（有効期限内であれば）スキップさせたくないですか？（`prompt=consent`がない場合）

前述のように、認可コードとスコープ、クライアントを紐付けて管理している場合、同意画面をスキップする有効期限は別に管理する必要がありそうです。
では、アクセストークンが有効な間は同意画面を出さない設計にすればよいでしょうか？
アクセストークンがリボークされた後に認可要求をしたら再度同意画面を出すべきでしょうか？リフレッシュトークンが有効な間は認可要求しないから毎回同意画面表示でもいいかもしれません。

このあたりの結論が出なかったので、**「同意画面をスキップするかどうかの有効期限」を別で設定できるように「同意したという行為」を別に管理する**ことにしました。やりすぎ感もあるかなとも思っていますが、同意履歴をコードやトークンとは別の概念で管理することは意味があるかなと思いました。

このあたりベスプラがあれば是非教えてほしいです。

## パスワードポリシー

認証機能を作る際、（FIDO等を使わなければ）パスワードのポリシーを決めることは避けては通れない道です。
よくある進め方として、[NIST SP800-63-B](https://pages.nist.gov/800-63-3/sp800-63b.html)などのガイドラインや他社の実装状況をもとにパスワードのポリシーを決めていくことになると思います。

ですが今回は、「**パスワードポリシー自体を運用者・管理者が変更できる**ようにしてくれ」というオーダーがありました。そうなると、大文字小文字混在～とか、定期変更が～とか、思いついたポリシーについて検討していけば良いとはならず、**パスワードのポリシーとして決めなくてはならない項目を網羅**した上で、一つ一つの項目を設定できるようにする必要があります。

色々調べているうちに、[SCIM Password Management Extension](https://tools.ietf.org/html/draft-hunt-scim-password-mgmt-00)という仕様を発見しました。

### SCIM Password Management Extension について

SCIM Password Management Extension は、 SCIM と呼ばれるプロトコル・スキームの拡張仕様です。

SCIMとは、System for Cross-domain Identity Management の略で、システム間でアイデンティティ情報を連携する際に用いられるプロトコルとスキーマの定義です。

ユーザーのアイデンティティ情報のCRUD操作を定義したプロトコル
ユーザーのアイデンティティ情報スキーマを定義したスキーマ
などがあります。

そのSCIMのスキーマを拡張した仕様として、SCIM Password Management Extensionがドラフト版として、提示されています。
バージョンが 00 のみで、最終更新日も2015年なので、いまいち怪しい気もしますが、参考にします。

このRFCでは、パスワードに関する以下の拡張スキーマが定義されています。

- Password Schema Extension
  - ユーザのパスワード状態を管理するスキーマ `urn:ietf:params:scim:schemas:uniid:2.0:Password`
- Password Policy
  - システム全体のパスワードポリシーを管理するスキーマ `urn:ietf:params:scim:schemas:core:2.0:policy:Password`

他にも、パスワードリセット時の仕様等を定義しています。

- Management Requests
  - PasswordResetRequest
  - PasswordValidateRequest
  - UsernameValidateRequest
  - UsernameGenerateRequest
  - UsernameRecoverRequest

では、実際にパスワードポリシーのスキーマを見ていきましょう。
SCIM Password Management Extension では、以下の35項目が定義されています。

|#|名前|説明|
|----|----------------------|--------------------------------|
| 1 | name | ポリシー名 |
| 2 | description | 説明 |
| 3 | maxLength | 最大文字数 |
| 4 | minLength | 最小文字数 |
| 5 | minAlphas | 最小アルファベット数 |
| 6 | minNumerals | 最小数字数 |
| 7 | minAlphaNumerals | 最小英数字数 |
| 8 | minSpecialChars | 最小特殊文字数 |
| 9 | maxSpecialChars | 最大特殊文字数 |
| 10 | minUpperCase | 最小大文字数 |
| 11 | minLowerCase | 最小小文字数 |
| 12 | minUniqueChars | 最小ユニーク文字数 |
| 13 | maxRepeatedChars | 最大同一文字繰り返し回数 |
| 14 | startsWithAlpha | アルファベット始まりの強制 |
| 15 | minUnicodeChars | Unicode文字数 |
| 16 | firstNameDisallowed | 名の利用不可 |
| 17 | lastNameDisallowed | 姓の利用不可 |
| 18 | userNameDisallowed | ユーザーIDの利用不可 |
| 19 | minPasswordAgeInDays | 最小パスワード変更禁止期間(日) |
| 20 | warningAfterDays | パスワード変更警告表示日数 |
| 21 | expiresAfterDays | パスワード有効期間(日) |
| 22 | requiredChars | 必須文字 |
| 23 | disallowedChars | 禁止文字 |
| 24 | disallowedSubStrings | 禁止文字列 |
| 25 | dictionaryLocation | ブラックリストのURL |
| 26 | passwordHistorySize | 利用禁止過去パスワード数 |
| 27 | maxIncorrectAttempts | 最大リトライ回数 |
| 28 | lockOutDuration | ロックアウトの時間 |
| 29 | challengesEnabled | 秘密の質問を利用するか |
| 30 | defaultQuestions | デフォルトの質問 |
| 31 | minQuestionCount | 最小設定質問数 |
| 32 | minAnswerCount | 最小回答質問数 |
| 33 | allAtOnce | 全質問表示 |
| 34 | minResponseLength | 最小回答文字数 |
| 35 | maxIncorrectAttempts | 最大リトライ回数 |

つまり、最初の2つを除く、33個のルールを変数として設定できるような仕組みを作れば、世の中の大体のパスワードポリシーを設定レベルで実装できるということですね（辛い）

## 終わりに

OpenID Connect の IdP をスクラッチで実装したときに悩まされた点を一部書きました。どのフローを選択するかとか、state、nonce、at_hash警察などの記事は多いので、それ以外の点で書こうと思ったのですが、何のまとまりもない文章になってしまいました。（ちなみに今回のシステムでは`state`は必須にするように実装しています。）

ここまで考えなければいけないケースは少ないかもしれませんが、IdP実装はやはり考えることが多いので、サービスなり製品なりを使ったほうが無難かもしれません。サービスや製品選定の際にこういう視点もあるんだなと参考になれば幸いです。