Amazon Cognito と最新 OAuth/OIDC 仕様

はじめに

Amazon Cognito user pools implements ID, access, and refresh tokens as defined by the OpenID Connect (OIDC) open standard” (Amazon Cognito は OpenID Connect (OIDC) オープン標準で定められている通りに ID トークン、アクセストークン、レフレッシュトークンを実装しています) — “Using Tokens with UserPools” より抜粋

しかしながら、Cognito の OIDC 実装は非常に限定的で柔軟性もないため、Cognito の OIDC 実装がシステム要件を満たせないということはよくあります。例えば、Cognito が発行する ID トークンの署名アルゴリズムは RS256 ですが、セキュリティ上の理由により Financial-grade API (FAPI) が同署名アルゴリズムを禁止しているにも関わらず、それを変更する方法がありません。

このチュートリアルでは、Cognito をユーザーデータベースとしてだけ用いて OAuth/OIDC 関連のタスクを Authlete に委譲することで、Cognito を使いつつも同時に Financial-grade API などの最新 OAuth/OIDC 仕様 (参考: Authlete スペックシート) をサポートする方法を説明します。

アーキテクチャ

OAuth 2.0 の文脈では、アクセストークン (及び任意でリフレッシュトークン) を発行するサーバーを認可サーバーと呼びます。一方、OpenID Connect の文脈では、ID トークンを発行するサーバーを OpenID Provider (IdP) と呼びます。OIDC は意図的に OAuth 2.0 上に定義されたため、一つのサーバーが両方の役割を持つことはよくあります。そのため、同じサーバーが文脈によって認可サーバーと呼ばれたり IdP と呼ばれたりします。このチュートリアルではそのようなサーバーを統一的に認可サーバーと呼びます。

OAuth/OIDC で最も一般的なフローである認可コードフロー (RFC 6749 Section 4.1) をサポートするためには、認可サーバーは二つのエンドポイントを実装しなければなりません。それらは認可エンドポイント (RFC 6749 Section 3.1)、トークンエンドポイント (RFC 6749 Section 3.2) と呼ばれます。Cognito ユーザープールはこれら二つのエンドポイントの実装を提供しますが、Cognito の OIDC 実装に満足できない場合は、自分でこれらのエンドポイントを実装する必要があります。

次の図は、Cognito と Authlete を併せて使った場合の認可コードフローにおけるコンポーネント間の関係を示しています。


Cognito と Authlete による認可コードフロー

ユーザー認証は Cognito が行うが OAuth/OIDC 関連のタスクは Authlete に委譲されるというのがこの図の要点です。OAuth 2.0 の中心となる仕様 (RFC 6749) が “The way in which the authorization server authenticates the resource owner (e.g., username and password login, session cookies) is beyond the scope of this specification.” (認可サーバーがリソースオーナーを認証する方法はこの仕様の範囲外である) と述べていることを考慮すると、この明確な分離には、ユーザー管理ソリューションが直接 OAuth/OIDC をサポートするアプローチよりも多くの利点があります。

後続のセクションでは、このチュートリアル用に用意された認可サーバーが認可エンドポイントとトークンエンドポイントの実装内で行っていることを詳細に説明します。

認可エンドポイント

サンプル認可サーバーの認可エンドポイントは次のことを行います。

  1. Web ブラウザ経由でクライアントアプリケーションから認可リクエスト (RFC 6749 Section 4.1.1) を受け取ります。
  2. 認可リクエストからクエリーパラメーター群を抜き出します。
  3. 抜き出したクエリーパラメーター群を Authlete の /api/auth/authorization API に渡します。
  4. Authlete API から返された情報を元に認可ページを組み立てます。
  5. 認可ページを Web ブラウザにに返します。
  6. 認可ページに埋め込んだログインフォームを介して、ユーザーからユーザー名とパスワードを取得します。
  7. ユーザーを認証するため、ユーザー名とパスワードを Cognito の AdminInitiateAuth API に渡します。
  8. ユーザー属性を取得するため、ユーザー名を Cognito の AdminGetUser API に渡します。
  9. ユーザーの一意識別子と属性群を Authlete の /api/auth/authorization/issue API に渡します。
  10. Authlete API から返された情報を元に認可レスポンス (RFC 6749 Section 4.1.2) を組み立てます。
  11. 認可レスポンスを Web ブラウザに返します。

トークンエンドポイント

サンプル認可サーバーのトークンエンドポイントは次のことを行います。

  1. クライアントアプリケーションからトークンリクエスト (RFC 6749 Section 4.1.3) を受け取ります。
  2. トークンリクエストからフォームパラメーター群を抜き出します。
  3. 抜き出したフォームパラメーター群を Authlete の /api/auth/token API に渡します。
  4. Authlete API から返された情報を元にトークンレスポンス (RFC 6749 Section 4.1.4) を組み立てます。
  5. トークンレスポンスをクライアントアプリケーションに返します。

実装

上記に説明したアーキテクチャは django-oauth-server に実装されています。django-oauth-server は Django Web フレームワークを用いて Python で書かれたオープンソースの認可サーバーです。このサーバーを実行するには、次の手順を踏んでください。

Cognito 設定

  1. Cognito ユーザープールを作成してください。後ほどテストで使用するので、email 属性を含めておいてください。
  2. Cognito ユーザープールにクライアントを追加してください。当該クライアントが Server-Side Authentication Flow を使えるように、クライアント設定の「認証フローの設定」で ALLOW_ADMIN_USER_PASSWORD_AUTH を有効にしてください。
  3. Cognito ユーザープールにユーザーを追加してください。
  4. 使用する AWS アカウントに、Cognito の AdminInitiateAuth API と AdminGetUser API を呼ぶのに必要な権限を与えてください。

認可サーバー設定

必要な Python ライブラリをインストールしてください。

$ pip install authlete           # Python 用 Authlete ライブラリ
$ pip install authlete-django    # Django 用 Authlete ライブラリ
$ pip install boto3              # Python 用 AWS SDK

認可サーバー実装のソースコードをダウンロードしてください。

$ git clone https://github.com/authlete/django-oauth-server.git
$ cd django-oauth-server

Authlete API にアクセスするため、Authlete 設定ファイル (authlete.ini) を編集してください。

$ vi authlete.ini

Django 設定ファイル (django_oauth_server/settings.py) を開き、

$ vi django_oauth_server/settings.py

backends.CognitoBackendAUTHENTICATION_BACKENDS に追加してください。Django の認証バックエンドの詳細については、Customizing authentication in DjangoSpecifying authentication backends を参照してください。

AUTHENTICATION_BACKENDS = ('backends.CognitoBackend',)

また、同ファイル内の COGNITO_USER_POOL_IDCOGNITO_CLIENT_ID を適切に編集してください。

COGNITO_USER_POOL_ID = 'YOUR_COGNITO_USER_POOL_ID'
COGNITO_CLIENT_ID    = 'YOUR_COGNITO_CLIENT_ID'

Cognito の AdminInitiateAuth API と AdminGetUesr API の呼び出し方に興味がある場合は、ソースコード cognito_backend.py を調べてみてください。

認可サーバー起動

認可サーバーを起動するには、次のコマンドを入力してください。

$ python manage.py runserver

make run” でも同じことができます。

$ make run

認可サーバーは下表にリストされているようなエンドポイントを幾つか公開しています。Authlete の設定 (authlete.ini) が正しいかどうかを確認する簡単な方法は、ディスカバリーエンドポイント (http://localhost:8000/.well-known/openid-configuration) にアクセスし、OpenID Connect Discovery 1.0 に準拠する JSON が返ってくるかどうかを確認することです。

エンドポイント URL
認可エンドポイント http://localhost:8000/api/authorization
トークンエンドポイント http://localhost:8000/api/token
ディスカバリーエンドポイント http://localhost:8000/.well-known/openid-configuration

テスト

全ての準備が整いました。認可コードフローでアクセストークンと ID トークンを取得してみましょう。

認可リクエスト

認可コードフローの最初のステップは、認可サーバーの認可エンドポイントに Web ブラウザ経由で認可リクエストを送ることです。このチュートリアルでは、認可エンドポイントは django-oauth-server が提供する http://localhost:8000/api/authorization です。認可リクエストを表す下記の URL の CLIENT_IDREDIRECT_URI を適切に置き換え、Web ブラウザを使ってその URL にアクセスしてください。

http://localhost:8000/api/authorization?response_type=code&client_id=CLIENT_ID&scope=openid+email&state=123&nonce=abc&redirect_uri=REDIRECT_URI

Web ブラウザには認可サーバーが生成した認可ページが表示されます。次のように見えるでしょう。


Authorization page in authorization code flow

ページにはログイン ID とパスワードを入力するフィールドがあります。そこに Cognito ユーザープールに登録したユーザーのユーザー名とパスワードを入力し、Authorize ボタンを押してください。Web ブラウザはあなたのクライアントアプリケーションのリダクレクトエンドポイントにリダイレクトされます。

ブラウザのアドレスバーに表示されているリダイレクトエンドポイントの URL には、次のように code レスポンスパラメーターが含まれています。

REDIRECT_URI?state=123&code=RwRq2Lp0bJVMiLPKAFz4qB1hxieBD1X5HKuv8EPkJeM

code レスポンスパラメーターの値は、あなたのクライアントアプリケーションに対して認可サーバーから発行された認可コードです。この認可コードはクライアントアプリケーションがトークンリクエストを投げる際に必要となります。

トークンリクエスト

認可コード取得後、クライアントアプリケーションは認可サーバーのトークンエンドポイントトークンリクエストを投げます。このチュートリアルでは、トークンエンドポイントは django-oauth-server が提供する http://localhost:8000/api/token です。

トークンリクエストはシェル端末で curl コマンドを用いて投げることができます。下記はトークンリクエストの例です。タイプする前に、CLIENT_IDREDIRECT_URICODE を実際の値で置き換えてください。

$ curl http://localhost:8000/api/token -d grant_type=authorization_code -d client_id=CLIENT_ID -d redirect_uri=REDIRECT_URI -d code=CODE
引数 説明
http://localhost:8000/api/token トークンエンドポイントの URL。
-d grant_type=authorization_code 認可コードフローであることを示す。
-d client_id=CLIENT_ID クライアント ID を指定する。CLIENT_ID を実際のクライアント ID で置き換えること。
-d redirect_uri=REDIRECT_URI リダイレクト URI を指定する。REDIRECT_URI を認可リクエストで用いたものと同じ値で置き換えること。
-d code=CODE 認可コードを指定する。CODE を実際の認可コードで置き換えること。

トークンリクエストが成功すると、トークンエンドポイントは access_tokenid_token を含む JSON を返します。

{
  "access_token": "FrGIJQpW51-l5mYJHcqGUNIKGJ1W23fFlW6c9AQEZEc",
  "refresh_token": "jhvKm9-haLQwnIR4CfkL6bfPIBBlqluFeqZKAgdPNjM",
  "scope": "email openid",
  "id_token": "eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRha2FAYXV0aGxldGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInN1YiI6IjIiLCJhdWQiOlsiNDMyNjM4NTY3MCJdLCJleHAiOjE2MTY0MTI3NDIsImlhdCI6MTYxNjMyNjM0MiwiYXV0aF90aW1lIjoxNjE2MzI2MTAwLCJub25jZSI6ImFiYyIsInNfaGFzaCI6InBtV2tXU0JDTDUxQmZraG43OXhQdUEifQ.7sXy2FcELxHo3LCQkb9teLaUE9jtRxXsa8diJKnkwAo",
  "token_type": "Bearer",
  "expires_in": 86400
}

access_token の値が発行されたアクセストークンです。同様に、id_token の値が発行された ID トークンです。

このチュートリアルで発行された ID トークンのペイロード部は、デコードすると下記のようになります。ペイロード内の email の値が Cognito ユーザープール内のユーザーの email 属性と一致するかどうかをチェックすることで、認可サーバーと Cognito ユーザープールの通信が成功したかどうかを確認することができます。

{
  "email": "taka@authlete.com",
  "iss": "https://example.com",
  "sub": "2",
  "aud": [
    "4326385670"
  ],
  "exp": 1616412742,
  "iat": 1616326342,
  "auth_time": 1616326100,
  "nonce": "abc",
  "s_hash": "pmWkWSBCL51Bfkhn79xPuA"
}

おめでとうございます!

あなたはこのチュートリアルを完了しました。Authlete を使うことで、認可サーバーに Amazon Cognito をユーザーデータベースとして使わせつつ同時に最新の OAuth/OIDC 仕様をサポートさせる方法を学びました。

サポートが必要であればお問い合わせください。いつでも歓迎します!