Client Credentials Flow 仕様と Cognito を使った実装

## 書くこと

システム間データ連携の API サーバを作るにあたり、認証の方法を OAuth 2.0 Client Credentials Flow にすることがありました。

API サーバを作る担当、認証サーバを作る担当、API サーバを使うシステムを作る担当はそれぞれ異なり、仕様を明確に伝える必要がありました。

認証サーバは実現方法として AWS に寄せる考えで容易な Amazon Cognito を使って作りました。
Client Credentials Flow の仕様に対する Amazon Cognito の対応について情報が少なく、書いてまとめます。

仕様の話が多いです。
Terraform による Cognito の実装や、Spring Security による API サーバ・API サーバを使うシステムの実装の話も少しします。


## Client Credentials Flow 仕様について

認証の方式の１つであり、次のような流れになります

1. Client（API サーバを使うシステム）から Authorization Server （Cognito）のトークンエンドポイントへリクエストして、アクセストークンを得る
1. Client（API サーバを使うシステム）からアクセストークンをリクエストに含めて Resource Server（API サーバ）の API へリクエストして、アクセストークンを検証され、Resource（API サーバが持つ連携データ）を得る

アクセストークンの取得は [RFC 6749 OAuth 2.0 原文](https://datatracker.ietf.org/doc/html/rfc6749#autoid-49) [日本語訳](https://openid-foundation-japan.github.io/rfc6749.ja.html#grant-client) で Client Credentials Grant として定められています。

アクセストークンには Scope（Resource について与える権限）があり、内容は自分で Resource やユースケースを見て考える必要があります。
たとえば basic（全体的に権限少なめ）や customers.read（顧客データの一覧や閲覧などできる）や customers:get（顧客取得 API のみできる）など。


## Cognito の実装

Amazon Cognito はユーザ管理ができる AWS のサービス
ホストされた UI を使うことで OAuth 2.0 もサポートし、ユーザ管理じゃないけど Client Credentials Flow もできます。

Client Credentials Flow では次の AWS リソースを扱います。

- アプリケーションクライアント
    - Client に相当
    - 作成単位は自分で考える。システムごとなど
    - アクセストークンの有効期限を設定する（5 分～1 日の範囲、デフォルト 1 時間）。Client に聞くなどして決める
    - アクセストークンに与えられるカスタムスコープを設定する
- カスタムスコープ
    - Scope に相当
    - 内容は自分で考える。前述通り
- リソースサーバー
    - Resource Server を表現したもの。実物ではない
    - Scope の接頭辞になる
    - 複数の Resource Server を識別するためのものなのかな

例として Client（ふがシステム）が Resource Server（ほげシステム API サーバ）を使い、カスタムスコープ customer.read のみを与えられる、Terraform 実装を示します。

```terraform
resource "aws_cognito_user_pool" "auth_server" {
  name = "hoge-system-user-pool"
}

# ホストされた UI を有効にし、OAuth 2.0 をサポート
resource "aws_cognito_user_pool_domain" "auth_server" {
  domain       = "hoge-system-user-pool-domain"
  user_pool_id = aws_cognito_user_pool.auth_server.id
}

# アプリケーションクライアント
resource "aws_cognito_user_pool_client" "auth_server" {
  name                                 = "hoge-system-fuga-client"
  user_pool_id                         = aws_cognito_user_pool.auth_server.id
  generate_secret                      = true
  allowed_oauth_flows_user_pool_client = true
  allowed_oauth_flows                  = ["client_credentials"]
  # 与えられるカスタムスコープ
  allowed_oauth_scopes                 = ["customers.read"]
  # 有効期限
  access_token_validity                = 3600
  token_validity_units {
    access_token = "seconds"
  }
  enable_token_revocation       = true
  prevent_user_existence_errors = "ENABLED"
}

# リソースサーバー
resource "aws_cognito_resource_server" "auth_server" {
  identifier   = "hoge-system-resource-server"
  name         = "hoge system"
  user_pool_id = aws_cognito_user_pool.auth_server.id
  # カスタムスコープ
  scope {
    scope_name        = "customers.read"
    scope_description = "readable customer resource"
  }
  scope {
    scope_name        = "customers.write"
    scope_description = "writable customer resource"
  }
}
```


## トークンエンドポイント仕様

Cognito では [トークン発行者エンドポイント](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html) に説明があります。
Client Credentials Flow に絞ると次のようになります。

リクエストヘッダ

- Content-Type: application/x-www-form-urlencoded
- Authorization: Basic {クライアント認証情報}

リクエスト本文

- grant_type=client_credentials
- client_id={クライアント ID}
- client_secret={クライアントシークレット}
- scope={スペース区切りの複数の Scope}

クライアントの認証は Authorization ヘッダー（HTTP Basic 認証）、または本文 client_id, client_secret を指定します。
OAuth 2.0 では Client が Authorization ヘッダーが使えないときのための client_id, client_secret を指定する認証は "MAY support" ですが、Cognito ではサポートしています。

scope は省略した場合、アプリケーションクライアントに設定したスコープが付与されます。
OAuth 2.0 ではデフォルト値で付与するか、リクエストを失敗させるかとしていますが、Cognito では前者のようです。

レスポンスヘッダ

- Content-Type: application/json

レスポンス本文（成功時）

- "access_token": "{アクセストークン}"
- "expires_in": {アプリケーションクライアントに設定した有効期限の秒}
- "token_type": "Bearer"

レスポンス本文（エラー時）

- "error": "invalid_request |invalid_client|invalid_grant|unauthorized_client|unsupported_grant_type|invalid_scope"

error の値のうち OAuth 2.0 仕様にあり Cognito の説明にない invalid_scope は Cognito でも試したところ返せるようです。

## API サーバを使うシステムの実装

Cognito の各 AWS リソースを作ったら、Client（API サーバを使うシステム）を作る担当へ、トークンエンドポイント仕様、ホストされた UI のドメイン、アプリケーションクライアントの Scope・クライアント ID・クライアントシークレットを伝えます。

実装の例として Spring Security の application.yaml を示します。

```yaml
spring:
  security:
    oauth2:
      client:
        registration:
          cognito:
            client-id: {もらったクライアント ID}
            client-secret: {もらったクライアントシークレット}
            authorization-grant-type: client_credentials
            # もらった Scope
            scope:
              - hoge-system-resource-server/customers.read
        provider:
          cognito:
            token-uri: {もらったホストされた UI のドメイン}/oauth2/token
```


## アクセストークン検証仕様

OAuth 2.0 ではアクセストークン検証の方法を定めていませんが、Cognito では [アクセストークンは JWT](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html) であり、JWT の検証で行います。

JWT の検証の仕様は [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#autoid-25) で定められ、
Cognito では [JSON ウェブトークンの検証](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html) に説明があります。


## API サーバの実装

Cognito の各 AWS リソースを作ったら、Resource Server（API サーバ）を作る担当へ、アクセストークン検証仕様、Issuer URI、すべての Scope を伝えます。

Resource Server 実装の例として Spring Security の application.yaml と SecurityFilterChain を示します。

```yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # もらった Issuer URI
          issuer-uri: https://cognito-idp.{Cognito ユーザープールのリージョン}.amazonaws.com/{Cognito ユーザープールの ID}
```

```java
import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(authorize -> authorize
                // もらった Scope を使い認可を実装
                .requestMatchers(HttpMethod.GET, "/customers/**")
                    .access(hasScope("hoge-system-resource-server/customers.read"))
                .requestMatchers(HttpMethod.POST, "/customers/**")
                    .access(hasScope("hoge-system-resource-server/customers.write"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            )
            .build();
    }
}
```

## 参考

最後にそのほか参考にしたものを載せます。

- [Implementing OAuth 2.0 Client Credentials Flow with AWS Cognito for Secure Server-to-Server Communication | by Vladislav Dektiarev | Soname Solutions](https://blog.soname.solutions/implementing-oauth-2-0-ee2806fe5a4e)