新しい Spring Authorization Server について

# はじめに

Spring Security プロジェクトは、認可サーバーの実装を今後サポートしない旨、2019 年 11 月 14 日付の『[Spring Security OAuth 2.0 Roadmap Update](https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update)』でアナウンスしました。しかしその後、一部の有志が Spring の認可サーバー実装プロジェクト『[Spring Authorization Server](https://github.com/spring-projects-experimental/spring-authorization-server)』を開始しました。この記事は、そのプロジェクトに対する警鐘となります。

# OAuth 2.0 Multiple Response Type Encoding Practices

2012 年 10 月の **[RFC 6749](https://tools.ietf.org/html/rfc6749)**（The OAuth 2.0 Authorization Framework）のリリースから 1 年 4 ヶ月後の 2014 年 2 月、**[OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)**（以降 OAuth.Responses）という仕様が 2014 年 2 月に公開されました。その 9 ヶ月後の 2014 年 11 月に公開された **[OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)** は、当仕様を大前提としています。

| 公開日 | 仕様 |
|:--|:--|
| 2012 年 10 月 | [RFC 6749](https://tools.ietf.org/html/rfc6749)（The OAuth 2.0 Authorization Framework）|
| 2014 年 02 月 | [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) |
| 2014 年 11 月 | [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) |

OAuth.Responses が認可サーバーの実装に与える影響は甚大で、[Spring Security OAuth](https://github.com/spring-projects/spring-security-oauth) が行き詰まった理由の一つだと私は考えています（参考：[Issue #619 Handling additional response_types](https://github.com/spring-projects/spring-security-oauth/issues/619)）。

OAuth.Responses により、認可リクエストの `response_type` パラメーターの処理方法が大きく変わりました。それまでは、つまりは RFC 6749 の時点では、「`response_type` の値は `code` または `token` のどちらかになる」という前提で認可サーバーの実装を書いていればよかったのですが、OAuth.Responses 以降は、「`response_type` は、`code`/`id_token`/`token` の任意の**組合せ**、もしくは単独の `none`」という複雑な状況に対応しなければならなくなりました。

||response_type|
|:--|:--|
| RFC 6749 時点 | `code` もしくは `token` |
| OAuth.Responses 以降 | `code`/`id_token`/`token` の任意の組合せ、もしくは単独の `none` |

仕様上、RFC 6749 は `response_type` に複数の値を入れることを許容しています。しかし、認可レスポンスパラメーターを置く場所に関して、`code` と `token` で仕様が衝突するため、衝突の解決方法が提示されていない段階では（OAuth.Responses が規定されるまでは）、`code` と `token` が同時に `response_type` に含まれることを想定することは困難でした。

RFC 6749 の時点では、`response_type` の処理の実装は、下記の仮想コードのように、`response_type` の値が特定の値と「一致するかどうか」を調べていれば済みました。

```python
if (response_type == 'code') {
    // 認可コードフロー (RFC 6749 Section 4.1)
    // 認可コードを発行する
}
else if (response_type == 'token') {
    // インプリシットフロー (RFC 6749 Section 4.2)
    // アクセストークンを発行する
}
else {
    // response_type の値が不正
}
```

一方、OAuth.Responses 以降は、`response_type` に特定の値が「含まれているか」を調べる必要がでてきます。

```python
if ('code' in response_type) {
    // 認可コードを発行する
}

if ('token' in response_type) {
    // アクセストークンを発行する
}

if ('id_token' in response_type) {
    // ID トークンを発行する
}

if ('none' in response_type) {
    // 何も発行しない. response_type に none 以外が
    // 含まれていればエラー.
}
```

`response_type` に複数の値が含まれるということは、認可エンドポイントから複数のトークンが発行されうるということを意味します。例えば `response_type` が `code id_token token` の場合、認可エンドポイントからは認可コード、ID トークン、アクセストークン、の三つが発行されます。詳細は『[OpenID Connect 全フロー解説](https://qiita.com/TakahikoKawasaki/items/4ee9b55db9f7ef352b47)』を参照してください。

| response_type | 認可コード | ID トークン | アクセストークン |
|:--|:-:|:-:|:-:|
| `code` | :heavy_check_mark: | | |
| `token` | | | :heavy_check_mark: |
| `id_token` | | :heavy_check_mark: | |
| `id_token token` | | :heavy_check_mark: | :heavy_check_mark: |
| `code id_token` | :heavy_check_mark: | :heavy_check_mark: | |
| `code token` | :heavy_check_mark: | | :heavy_check_mark: |
| `code id_token token` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| `none` | | | |

ID トークンと同時に認可コードやアクセストークンが発行される場合、それぞれのハッシュ値を `c_hash` クレームや `at_hash` クレームの値として ID トークンに含めなければならないというルールなどもあり（OIDC Core 1.0 [Section 3.3.2.11](https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken)）、OAuth.Responses により認可エンドポイントの実装はかなり複雑になります。

# Spring Authorization Server の実装

Spring Security が認可サーバーの実装を今後サポートしないことを発表してから約 9 ヶ月後の 2020 年 8 月 21 日、Spring.IO のブログ『[Get the very first bits of Spring Authorization Server 0.0.1 !](https://spring.io/blog/2020/08/21/get-the-very-first-bits-of-spring-authorization-server-0-0-1)』により、Spring 用の認可サーバー実装のバージョン 0.0.1 がアナウンスされました。この実装に大きな期待を寄せている方々もいらっしゃるでしょう。

この実装の `response_type` の処理は <code>[OAuth2AuthorizationEndpointFilter.java](https://github.com/spring-projects-experimental/spring-authorization-server/blob/master/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java)</code> にあり、現時点では次のようになっています。

```java
// response_type (REQUIRED)
if (!StringUtils.hasText(authorizationRequestContext.getResponseType()) ||
		authorizationRequestContext.getParameters().get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
	authorizationRequestContext.setError(
			createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE));
	return;
} else if (!authorizationRequestContext.getResponseType().equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
	authorizationRequestContext.setError(
			createError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE));
	return;
}
```

ご覧の通り、OAuth.Responses 仕様を念頭に置いていないことは明らかで、**Spring Security OAuth プロジェクトの OpenID Connect サポートが行き詰まったのと同じ過ちを繰り返しています**。

このコードが参照している `OAuth2AuthorizationResponseType.CODE` ですが、これは Spring Security 本体に含まれています。<code>[OAuth2AuthorizationResponseType](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationResponseType.html)</code> クラスには現在、`CODE` と `TOKEN` の 2 つのクラス変数が定義されています。少なくともここに `ID_TOKEN` が入ってこないと、新 Spring Authorization Server プロジェクトで OpenID Connect をサポートすることはできないでしょう。

<code>[OAuth2AuthorizationEndpointFilter.java](https://github.com/spring-projects-experimental/spring-authorization-server/blob/master/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java)</code> の認可リクエストパラメーターの処理順序を見ると、`scope` よりも前に `redirect_uri` がチェックされています。この処理順序ですと、「`scope` に `openid` が含まれる場合は `redirect_uri` が必須」という OpenID Connect Core 1.0 の要求事項のチェックができません。新 Spring Authorization Server プロジェクトが OpenID Connect を考慮していないことが分かります。

当然、OpenID Connect Core 1.0 で規定されているリクエストオブジェクトの実装は含まれていません。しかし、リクエストオブジェクトはリクエストパラメーターの処理に大きな影響を与えるので、設計段階から考慮に入れておく必要があります。なお、**[Financial-grade API](https://fapi.openid.net/)**（**FAPI**）の [Part 2](https://openid.net/specs/openid-financial-api-part-2-wd-07.html) でリクエストオブジェクトが必須になっていることからも分かるとおり、高いセキュリティが要求される環境では、リクエストオブジェクトの実装が必要です。リクエストオブジェクトの詳細は『[JAR (JWT Secured Authorization Request) に関する実装者の覚書](https://qiita.com/TakahikoKawasaki/items/d52ce91866ae0dcc2ff7)』を参照してください。

何年も前に明らかになっている問題点を繰り返していること、ソースコードの読みにくさ、認可画面の HTML を <code>[OAuth2AuthorizationEndpointFilter.java](https://github.com/spring-projects-experimental/spring-authorization-server/blob/master/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java)</code> にハードコーディングするセンス、そもそも認可エンドポイントでの処理を『フィルター』として実装しようとするアプローチ、を鑑みれば、新 Spring Authorization Server の実装品質が良くないことは明らかです。

新 Spring Authorization Server プロジェクトに期待をしている方々には、Spring の名を過信せず、ソースコードの品質を確認した上で利用の可否を判断するよう、助言させていただきたく思います。

# 代替プロジェクト

オープンソースに限っても他に選択肢はいろいろあります。中には、正式に [OpenID 認証](https://openid.net/certification/)を取得し、商用レベルの品質のものもあります。[OpenID Foundation](https://openid.net/) のウェブサイトに[ライブラリ・製品・ツール](https://openid.net/developers/libraries/)のリストがあるので、そちらで要件に合うものを探されるのが良いと思います。