認証・認可

Amazon Cognito と OAuth 2.0

OAuth 2.0 / OpenID Connect の登場人物と手順を出発点にして、Amazon Cognito のユーザープール、ID プール、3 種類のトークン、エンドポイントがそれぞれ規格のどこに対応するかを図で辿る。Cognito 固有の名前を、規格の言葉に翻訳できるようになることを目標にする。

01OAuth 2.0 の登場人物と Cognito

OAuth 2.0(RFC 6749)は、ユーザーの代わりにアプリケーションが API を呼ぶための認可(authorization)の枠組みである。登場人物は 4 つに固定されている。OpenID Connect(OIDC)はその上に「誰がログインしたか」を伝える認証(authentication)の層を足した規格で、ID トークンという成果物を追加する。

Resource Owner
リソースの持ち主。通常はエンドユーザー
Client
ユーザーの代わりに API を呼ぶアプリケーション。Web アプリ、SPA、モバイルアプリ、バッチなど
Authorization Server
ユーザーを認証し、同意を取り、トークンを発行するサーバー
Resource Server
トークンを受け取って保護された API を提供するサーバー

Cognito のユーザープールは、このうち Authorization Server の役をまるごと引き受けるマネージドサービスである。ユーザーディレクトリ(ID とパスワード、属性、グループ)を内包し、OIDC 準拠のトークンを発行する。Client と Resource Server は自分で作るものであり、Cognito はそれらを「アプリクライアント」「リソースサーバー」という設定項目として登録させる。

OAuth 2.0 / OIDC の役 Amazon Cognito での対応物 Resource Owner エンドユーザー ブラウザ / 端末を操作する Client Web アプリ / SPA / モバイル ユーザーの代わりに API を呼ぶ Authorization Server ユーザーを認証しトークンを発行 OIDC ではここが OpenID Provider Resource Server 保護された API アクセストークンを検証して応答 ユーザープール内のユーザー ディレクトリに登録された ID / 属性 / グループ。または 外部 IdP から連携されたユーザー アプリクライアント client_id(+ 任意で secret) 許可するグラント・スコープ・ redirect_uri をここで登録 ユーザープール Managed Login(旧 Hosted UI)と /oauth2/* エンドポイントを持つ マネージドな認可サーバー リソースサーバー設定 自作 API の識別子と カスタムスコープを登録。 API 本体は自分で持つ
FIG 01OAuth 2.0 の 4 つの役と Cognito の対応

Cognito が「作ってくれる」のは認可サーバーだけである。アプリクライアントとリソースサーバーは、自分のアプリと API を Cognito に名乗らせるための登録であって、実体は自分で持つ。

02ユーザープールと ID プール

Cognito には性格の異なる 2 つのプールがある。OAuth の話をするときに主役になるのはユーザープール(user pool)であり、ID プール(identity pool、旧称 federated identities)は OAuth の外側で「トークンを AWS の一時認証情報に交換する」役を担う。名前が似ているため混同されやすいが、規格上の立ち位置はまったく違う。

アプリ Client ブラウザ / モバイル ユーザープール OIDC 準拠の認可サーバー + ユーザーディレクトリ サインアップ / サインイン / MFA / パスキー 外部 IdP との連携(Google / SAML / OIDC) 発行物: ID / Access / Refresh トークン(JWT) ID プール トークン → AWS 一時認証情報 の交換所 内部で STS AssumeRoleWithWebIdentity を呼ぶ 発行物: AccessKeyId / SecretAccessKey / SessionToken 自作 API Resource Server API Gateway + Lambda、ECS、EC2 など Authorization: Bearer のアクセストークンを検証 認可判断はスコープ / groups クレームで行う AWS サービス S3 / DynamoDB / AppSync など SigV4 署名された呼び出しを受ける 認可判断は IAM ロールのポリシーで行う ① サインイン ② ID トークンを渡す アクセストークン 一時認証情報 ID プールの IdP として登録
FIG 022 つのプールの役割。上段が OAuth / OIDC、下段が AWS IAM の世界

ユーザープールだけで完結する構成は多い。自作 API を守るだけなら ID プールは不要である。ID プールが必要になるのは、ブラウザやモバイルから S3 や DynamoDB などの AWS サービスを直接呼びたいときに限られる。この場合、認可の主体はトークンのスコープではなく IAM ロールに移る(08 章)。

ユーザープールには OAuth を使わずにトークンを得る道もある。InitiateAuth などのネイティブ API(SRP やパスワード認証、選択式の USER_AUTH フロー)を SDK から呼ぶと、同じ 3 種類のトークンが返る。本稿で扱う /oauth2/* はもう一方の入口であり、外部 IdP との連携やホストされたログイン画面を使いたいときにこちらが選ばれる。

03認可コードフロー + PKCE

ブラウザやモバイルを持つアプリで標準となる手順が、認可コードグラント(authorization code grant)に PKCE(RFC 7636)を足したものである。Cognito ではユーザープールにドメインを設定すると、そのドメイン配下に Managed Login のサインイン画面と /oauth2/* エンドポイントが生える。以下の図で規格上の手順と Cognito の具体的な URL を対応させる。

ブラウザ Resource Owner の端末 アプリ Client ユーザープール Authorization Server 自作 API Resource Server code_verifier を生成 challenge = S256(verifier) 1 2 302 で認可エンドポイントへ誘導 GET /oauth2/authorize?response_type=code&client_id=…&redirect_uri=…&scope=openid+email &state=…&code_challenge=…&code_challenge_method=S256 Managed Login で認証 パスワード / パスキー / MFA または外部 IdP(07 章) 3 4 302 Location: {redirect_uri}?code=…&state=… アプリは state を照合する 5 POST /oauth2/token grant_type=authorization_code &code=…&code_verifier=…&client_id=…&redirect_uri=… 6 { id_token, access_token, refresh_token, expires_in } S256(code_verifier) が challenge と一致したときだけ発行 7 GET /orders Authorization: Bearer {access_token} JWKS で署名検証 scope / exp / client_id 確認 8
FIG 03認可コードフロー + PKCE を Cognito のエンドポイントに対応させたもの

手順の要点を規格の言葉で整理すると次のようになる。

  1. Client が一時的な乱数 code_verifier を生成し、その SHA-256 ハッシュ code_challenge を作る(PKCE)
  2. ブラウザを Authorization Server の認可エンドポイントへ送る。Cognito では https://{ドメイン}/oauth2/authorize/login を直接指定すると Managed Login の画面へ直行する
  3. Authorization Server がユーザーを認証する。ここが Cognito の Managed Login。ユーザープールのディレクトリで認証するか、外部 IdP に委譲する
  4. 認証に成功すると、Authorization Server は短命の認可コードredirect_uri に付けて返す。トークンそのものはブラウザの URL に載らない
  5. Client はバックチャネルで /oauth2/token にコードと code_verifier を送る。Cognito は S256 ハッシュが 2 で受け取った code_challenge と一致するか確かめ、横取りされたコードでは交換できないようにする
  6. Authorization Server がトークンを返す(04 章)
  7. Client は Resource Server にアクセストークンを Bearer ヘッダで送る
  8. Resource Server は Cognito の JWKS で署名を検証する(09 章)

PKCE はもともとクライアントシークレットを持てない公開クライアント(SPA、モバイル)のためのものだが、現在は機密クライアントでも使うのが推奨である。Cognito の認可コードグラントは追加設定なしに PKCE を受け付ける。

043 種類のトークン

トークンエンドポイントは 3 つのトークンを返す。ID トークンとアクセストークンは JWT で、リフレッシュトークンは不透明な文字列である。ID トークンは OIDC の成果物、アクセストークンとリフレッシュトークンは OAuth 2.0 の成果物であり、目的がまったく違う。

ユーザープール /oauth2/token が返す ID トークン JWT / token_use = "id" aud = client_id / sub, email, cognito:groups アクセストークン JWT / token_use = "access" client_id, scope, username, cognito:groups リフレッシュトークン 不透明な文字列(JWT ではない) 既定 30 日 / 60 分〜10 年 アプリが自分で読む 誰がログインしたかを知り、画面に名前を出す。 API に送らない。API 側も受け取らない Resource Server に送る Authorization: Bearer で API へ。scope で認可。 /oauth2/userInfo や Cognito の自己管理 API も受け付ける Authorization Server に送り返す /oauth2/token に grant_type=refresh_token で送ると 新しい ID / アクセストークンが返る。/oauth2/revoke で失効
FIG 043 つのトークンの行き先。上から OIDC、OAuth、OAuth
トークン規格上の出自形式主なクレーム有効期限(アプリクライアントごとに設定)用途
ID トークンOIDCJWTtoken_use=id aud(client_id) sub email cognito:groups カスタム属性既定 1 時間。5 分〜1 日アプリがユーザーを識別する。ID プールへの入力にもなる
アクセストークンOAuth 2.0JWTtoken_use=access client_id scope username cognito:groups既定 1 時間。5 分〜1 日API の認可。/oauth2/userInfo と Cognito の自己管理 API の呼び出し
リフレッシュトークンOAuth 2.0不透明なし既定 30 日。60 分〜10 年上 2 つの再発行。/oauth2/revoke で失効

Cognito のアクセストークンには aud クレームがなく、代わりに client_id が入る。また Cognito は ID トークンとアクセストークンを別の鍵で署名する。どちらの鍵もユーザープールの JWKS に含まれるので、検証側は kid で選べばよい(09 章)。

ID トークンを API の認可に使うのは規格上の誤用である。ID トークンの aud はアプリ自身であり、API 宛てではない。Cognito の API Gateway 用オーソライザーは歴史的に ID トークンも受け付けるが、スコープによる認可を効かせたいならアクセストークンを使う。

05グラント種別とアプリクライアント設定

OAuth 2.0 にはトークンを得る手順(grant)が複数ある。Cognito のアプリクライアントは、そのうち 3 つを設定項目として持つ。どれを許可するかは「そのクライアントがどんなアプリか」で決まる。

グラントresponse_type / grant_typePKCE返るトークン向いているクライアント備考
認可コードcode / authorization_codeID・アクセス・リフレッシュWeb アプリ、SPA、モバイル標準。トークンがブラウザの URL に出ない
インプリシットtoken / なし不可ID・アクセス(リフレッシュなし)レガシーの SPAURL フラグメントにトークンが載る。OAuth 2.1 では廃止。新規では選ばない
クライアントクレデンシャルなし / client_credentials不要アクセスのみ(カスタムスコープ)サーバー間、バッチ、マイクロサービスユーザーが介在しない。ID トークンは出ない。シークレット付きクライアントが必要

アプリクライアントには、グラント以外にも OAuth の規格を Cognito の設定に翻訳した項目が並ぶ。

client_id
アプリクライアント ID。トークンの aud / client_id クレームになる
client_secret
任意。機密クライアント(サーバーサイド)だけが持つ。トークンエンドポイントでは Authorization: Basic base64(client_id:client_secret) で提示する
callback URL
規格の redirect_uri の許可リスト。完全一致でしか許可されない
sign-out URL
/logout から戻す先の許可リスト
identity providers
このクライアントで使える IdP。Cognito のディレクトリ自身も「Cognito user pool」という 1 つの IdP として並ぶ
OAuth scopes
このクライアントが要求できるスコープの上限(06 章)

ID プールへの連携や外部 IdP のフェデレーションを使う場合も、入口はすべてこのアプリクライアント経由になる。1 つのユーザープールに用途の違うアプリクライアントを複数作り、それぞれにグラントとスコープを絞るのが基本形である。

06スコープとリソースサーバー

スコープ(scope)は「このアクセストークンで何ができるか」を表す文字列で、Client が認可リクエストで要求し、Authorization Server が承認してアクセストークンの scope クレームに書き込む。Cognito のスコープは 2 系統に分かれる。

OIDC 標準スコープと Cognito 予約スコープ

スコープ出自効果
openidOIDCID トークンを発行する。/oauth2/userInfo を呼ぶ最低条件
profile email phoneOIDCID トークンと userInfo に含める属性の範囲を広げる
aws.cognito.signin.user.adminCognito 固有そのアクセストークンで Cognito の自己管理 API(属性更新、パスワード変更、MFA 設定など)を呼べる

カスタムスコープとリソースサーバー

自作 API のためのスコープは、まず Cognito にリソースサーバーを登録して定義する。識別子(例 https://api.example.comorders)と、その配下のスコープ名(例 readwrite)を登録すると、orders/read のように識別子とスコープ名を / でつないだ文字列がカスタムスコープになる。

GET /oauth2/authorize?response_type=code
  &client_id=1example23456789
  &redirect_uri=https://app.example.com/callback
  &scope=openid+email+orders/read+orders/write
  &state=…&code_challenge=…&code_challenge_method=S256

発行されたアクセストークンの scope クレームは "openid email orders/read orders/write" のようになり、API 側はこの文字列を見て操作を許可する。API Gateway の Cognito オーソライザーはメソッドごとに必要スコープを設定でき、トークンに含まれていなければ 403 を返す。クライアントクレデンシャルグラントで得られるのはこのカスタムスコープだけである。

リソースサーバーの登録は Cognito が API の存在を「知る」ための宣言にすぎない。Cognito はその API に何かを配備するわけではなく、トークンにスコープ文字列を書き込むところまでしか関与しない。スコープを解釈して守るのは API の実装である。

07フェデレーション

ユーザープールは Authorization Server であると同時に、外部 IdP(Google、Apple、Facebook、Amazon、任意の OIDC プロバイダ、SAML 2.0 プロバイダ)に対しては Client として振る舞う。つまり OAuth の手順が二段重ねになる。アプリから見える相手は常に Cognito であり、外部 IdP のトークンはアプリに渡らない。

外側の OAuth: アプリ ⇄ Cognito 内側の OAuth: Cognito ⇄ 外部 IdP アプリ Client(外側) Cognito のトークンだけ受け取る。 Google のトークンは見えない ユーザープール Authorization Server(外側) かつ Client(内側) Google に登録した client_id / secret を保持 属性マッピングでプロフィールを取り込む 外部 IdP Authorization Server(内側) Google / Apple / Facebook / OIDC / SAML 2.0 /oauth2/authorize identity_provider=Google Cognito 発行の code → トークン Google の authorize へ /oauth2/idpresponse に code (SAML は /saml2/idpresponse) Cognito は Google の ID トークンを検証し、ユーザープール内にユーザーを作成または照合し、 自前の署名鍵で ID / アクセス / リフレッシュトークンを発行し直す(identities クレームに出自が残る)
FIG 05フェデレーションは OAuth の二段重ね。Cognito は両方の役を兼ねる

外部 IdP を設定するときに Google などのコンソールに登録する「承認済みリダイレクト URI」は、アプリの URL ではなく https://{ユーザープールのドメイン}/oauth2/idpresponse である。内側の OAuth の Client は Cognito なので、コールバックも Cognito に返るためである。SAML の場合は /saml2/idpresponse になる。

アプリ側から特定の IdP を直接指定したいときは、認可リクエストに identity_provider=Google のようなパラメータを足す。省略すると Managed Login が IdP の選択画面を出す。

フェデレーションの利点は、アプリと API が Cognito のトークン形式だけを知っていればよいことにある。IdP を増やしても、後段の検証コードは変わらない。

08ID プールで AWS 認証情報に交換する

ID プールは OAuth の外側にある。入力はユーザープール(または外部 IdP)の ID トークンで、出力は IAM の一時認証情報である。内部では STS の AssumeRoleWithWebIdentity が呼ばれ、どの IAM ロールを引き受けるかは ID プールのロール設定(認証済み / 未認証ロール、またはトークンのクレームに基づくロールマッピング)で決まる。

アプリ ID トークンを保持 (03 章の成果物) ID プール GetId → IdentityId を採番 GetCredentialsForIdentity ロール選択: 認証済みロール、または cognito:groups 等によるロールマッピング STS AssumeRoleWith WebIdentity AWS サービス S3 DynamoDB AppSync Location など IAM ポリシーで認可 ID トークン 一時認証情報 SigV4 署名した API 呼び出し(AccessKeyId / SecretAccessKey / SessionToken)
FIG 06ID トークンから一時認証情報への交換。ここから先は IAM の世界

ID プールの「ログインプロバイダ」にユーザープールを登録するときは、cognito-idp.{region}.amazonaws.com/{userPoolId} という発行者名とアプリクライアント ID の組で指定する。ユーザープールを経由せず、Google などの外部 IdP のトークンを ID プールに直接渡すこともできるが、ユーザーディレクトリやグループを一元管理したいなら、ユーザープールで受けてから ID プールに渡す二段構成が扱いやすい。

ID プールが返す一時認証情報は、トークンのスコープではなく IAM ロールの権限で制限される。ロールを緩く作ると、フロントエンドから AWS のリソースを広く触れてしまう。ロールの信頼ポリシーには ID プール ID の条件(cognito-identity.amazonaws.com:aud)と認証済みの条件(cognito-identity.amazonaws.com:amr)を必ず入れる。

09トークンの検証

Resource Server はアクセストークンを受け取るたびに検証する。Cognito の JWT は RS256 で署名されており、公開鍵はユーザープールごとの JWKS エンドポイントで配布される。検証の手順は OIDC の一般論と同じで、Cognito 固有なのは URL とクレーム名だけである。

発行者      https://cognito-idp.{region}.amazonaws.com/{userPoolId}
JWKS        {発行者}/.well-known/jwks.json
OIDC 設定   {発行者}/.well-known/openid-configuration
  1. JWT ヘッダの kid に一致する鍵を JWKS から選び、署名を検証する。JWKS は鍵のローテーションに備えてキャッシュしつつ、未知の kid が来たら再取得する
  2. iss が自分のユーザープールの発行者と一致することを確認する
  3. exp が現在時刻より後であることを確認する
  4. token_use を確認する。API なら access、アプリ内のユーザー識別なら id
  5. 宛先を確認する。ID トークンは aud、アクセストークンは client_id が自分のアプリクライアント ID と一致すること
  6. 必要なら scopecognito:groups を見て認可する

API Gateway の Cognito ユーザープールオーソライザー、ALB の認証アクション、AppSync の Cognito 認可モードは、この手順を代行してくれる。自前で書く場合は aws-jwt-verify(Node.js)のような検証ライブラリを使うと、上記の手順が一通り実装されている。

署名検証のために Cognito に問い合わせる必要はない。JWKS は公開鍵の一覧であり、取得後はオフラインで検証できる。/oauth2/userInfo はトークン検証のためではなく、属性を取り直すためのエンドポイントである。ただしリフレッシュトークンの失効(/oauth2/revoke)はオフライン検証には伝わらない。失効を即時に効かせたい場合は、トークン寿命を短くするか、Cognito の自己管理 API 側で GetUser のように失効チェックが入る呼び出しを併用する。

10エンドポイント一覧と設計指針

ユーザープールのドメイン配下に生えるエンドポイントを、規格上の名前と並べて整理する。

Cognito のパス規格上の名前呼び出し元役割
/oauth2/authorizeAuthorization Endpointブラウザ認可コードまたはトークンを発行する入口。IdP 選択や Managed Login へ振り分ける
/login(Cognito 固有)ブラウザManaged Login のサインイン画面へ直行する。パラメータは authorize と同じ
/oauth2/tokenToken EndpointClient(バックチャネル)コード交換、リフレッシュ、クライアントクレデンシャル
/oauth2/userInfoUserInfo Endpoint(OIDC)Clientアクセストークンで属性を取得。openid スコープが必要
/oauth2/revokeRevocation Endpoint(RFC 7009)Clientリフレッシュトークンと、それで発行された全トークンを失効
/logout(RP-Initiated Logout 相当)ブラウザManaged Login のセッション Cookie を消し、sign-out URL に戻す
/oauth2/idpresponse /saml2/idpresponseRedirection Endpoint / ACS外部 IdPフェデレーションのコールバック。外部 IdP に登録する URL
{発行者}/.well-known/openid-configurationDiscovery(OIDC)Client / Resource Serverエンドポイントと鍵の場所を配布。ドメインではなく発行者 URL 配下にある
{発行者}/.well-known/jwks.jsonJWKSResource Server署名検証用の公開鍵

以上の構造から、次の指針が導かれる。

  • ブラウザやモバイルを持つアプリは認可コード + PKCE を選ぶ。インプリシットは新規では使わない
  • API には必ずアクセストークンを送る。ID トークンはアプリの中で使い切る
  • 自作 API の権限はリソースサーバーのカスタムスコープで表現し、API 側で scope クレームを検査する。ユーザーの所属による認可は cognito:groups で補う
  • サーバー間通信はクライアントクレデンシャル専用のアプリクライアントを別に作り、カスタムスコープだけを許可する
  • フロントエンドから AWS サービスを直接呼ぶときだけ ID プールを足す。そのときの認可は IAM ロールで決まるため、ロールの信頼ポリシーと権限を最小にする
  • 外部 IdP を足すときは、IdP 側に登録する redirect URI が /oauth2/idpresponse であることを忘れない
  • トークン寿命は既定の 1 時間 / 30 日から始め、失効を早く効かせたい API ではアクセストークンを短くしてリフレッシュに頼る
  • 検証コードは JWKS をキャッシュしたオフライン検証にし、Cognito への同期呼び出しをリクエスト経路に置かない