認証・認可
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 はそれらを「アプリクライアント」「リソースサーバー」という設定項目として登録させる。
Cognito が「作ってくれる」のは認可サーバーだけである。アプリクライアントとリソースサーバーは、自分のアプリと API を Cognito に名乗らせるための登録であって、実体は自分で持つ。
02ユーザープールと ID プール
Cognito には性格の異なる 2 つのプールがある。OAuth の話をするときに主役になるのはユーザープール(user pool)であり、ID プール(identity pool、旧称 federated identities)は OAuth の外側で「トークンを AWS の一時認証情報に交換する」役を担う。名前が似ているため混同されやすいが、規格上の立ち位置はまったく違う。
ユーザープールだけで完結する構成は多い。自作 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 を対応させる。
手順の要点を規格の言葉で整理すると次のようになる。
- Client が一時的な乱数
code_verifierを生成し、その SHA-256 ハッシュcode_challengeを作る(PKCE) - ブラウザを Authorization Server の認可エンドポイントへ送る。Cognito では
https://{ドメイン}/oauth2/authorize。/loginを直接指定すると Managed Login の画面へ直行する - Authorization Server がユーザーを認証する。ここが Cognito の Managed Login。ユーザープールのディレクトリで認証するか、外部 IdP に委譲する
- 認証に成功すると、Authorization Server は短命の認可コードを
redirect_uriに付けて返す。トークンそのものはブラウザの URL に載らない - Client はバックチャネルで
/oauth2/tokenにコードとcode_verifierを送る。Cognito は S256 ハッシュが 2 で受け取ったcode_challengeと一致するか確かめ、横取りされたコードでは交換できないようにする - Authorization Server がトークンを返す(04 章)
- Client は Resource Server にアクセストークンを Bearer ヘッダで送る
- Resource Server は Cognito の JWKS で署名を検証する(09 章)
PKCE はもともとクライアントシークレットを持てない公開クライアント(SPA、モバイル)のためのものだが、現在は機密クライアントでも使うのが推奨である。Cognito の認可コードグラントは追加設定なしに PKCE を受け付ける。
043 種類のトークン
トークンエンドポイントは 3 つのトークンを返す。ID トークンとアクセストークンは JWT で、リフレッシュトークンは不透明な文字列である。ID トークンは OIDC の成果物、アクセストークンとリフレッシュトークンは OAuth 2.0 の成果物であり、目的がまったく違う。
| トークン | 規格上の出自 | 形式 | 主なクレーム | 有効期限(アプリクライアントごとに設定) | 用途 |
|---|---|---|---|---|---|
| ID トークン | OIDC | JWT | token_use=id aud(client_id) sub email cognito:groups カスタム属性 | 既定 1 時間。5 分〜1 日 | アプリがユーザーを識別する。ID プールへの入力にもなる |
| アクセストークン | OAuth 2.0 | JWT | token_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_type | PKCE | 返るトークン | 向いているクライアント | 備考 |
|---|---|---|---|---|---|
| 認可コード | code / authorization_code | 可 | ID・アクセス・リフレッシュ | Web アプリ、SPA、モバイル | 標準。トークンがブラウザの URL に出ない |
| インプリシット | token / なし | 不可 | ID・アクセス(リフレッシュなし) | レガシーの SPA | URL フラグメントにトークンが載る。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 予約スコープ
| スコープ | 出自 | 効果 |
|---|---|---|
openid | OIDC | ID トークンを発行する。/oauth2/userInfo を呼ぶ最低条件 |
profile email phone | OIDC | ID トークンと userInfo に含める属性の範囲を広げる |
aws.cognito.signin.user.admin | Cognito 固有 | そのアクセストークンで Cognito の自己管理 API(属性更新、パスワード変更、MFA 設定など)を呼べる |
カスタムスコープとリソースサーバー
自作 API のためのスコープは、まず Cognito にリソースサーバーを登録して定義する。識別子(例 https://api.example.com や orders)と、その配下のスコープ名(例 read、write)を登録すると、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 のトークンはアプリに渡らない。
外部 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 プールの「ログインプロバイダ」にユーザープールを登録するときは、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
- JWT ヘッダの
kidに一致する鍵を JWKS から選び、署名を検証する。JWKS は鍵のローテーションに備えてキャッシュしつつ、未知のkidが来たら再取得する issが自分のユーザープールの発行者と一致することを確認するexpが現在時刻より後であることを確認するtoken_useを確認する。API ならaccess、アプリ内のユーザー識別ならid- 宛先を確認する。ID トークンは
aud、アクセストークンはclient_idが自分のアプリクライアント ID と一致すること - 必要なら
scopeやcognito:groupsを見て認可する
API Gateway の Cognito ユーザープールオーソライザー、ALB の認証アクション、AppSync の Cognito 認可モードは、この手順を代行してくれる。自前で書く場合は aws-jwt-verify(Node.js)のような検証ライブラリを使うと、上記の手順が一通り実装されている。
署名検証のために Cognito に問い合わせる必要はない。JWKS は公開鍵の一覧であり、取得後はオフラインで検証できる。/oauth2/userInfo はトークン検証のためではなく、属性を取り直すためのエンドポイントである。ただしリフレッシュトークンの失効(/oauth2/revoke)はオフライン検証には伝わらない。失効を即時に効かせたい場合は、トークン寿命を短くするか、Cognito の自己管理 API 側で GetUser のように失効チェックが入る呼び出しを併用する。
10エンドポイント一覧と設計指針
ユーザープールのドメイン配下に生えるエンドポイントを、規格上の名前と並べて整理する。
| Cognito のパス | 規格上の名前 | 呼び出し元 | 役割 |
|---|---|---|---|
/oauth2/authorize | Authorization Endpoint | ブラウザ | 認可コードまたはトークンを発行する入口。IdP 選択や Managed Login へ振り分ける |
/login | (Cognito 固有) | ブラウザ | Managed Login のサインイン画面へ直行する。パラメータは authorize と同じ |
/oauth2/token | Token Endpoint | Client(バックチャネル) | コード交換、リフレッシュ、クライアントクレデンシャル |
/oauth2/userInfo | UserInfo Endpoint(OIDC) | Client | アクセストークンで属性を取得。openid スコープが必要 |
/oauth2/revoke | Revocation Endpoint(RFC 7009) | Client | リフレッシュトークンと、それで発行された全トークンを失効 |
/logout | (RP-Initiated Logout 相当) | ブラウザ | Managed Login のセッション Cookie を消し、sign-out URL に戻す |
/oauth2/idpresponse /saml2/idpresponse | Redirection Endpoint / ACS | 外部 IdP | フェデレーションのコールバック。外部 IdP に登録する URL |
{発行者}/.well-known/openid-configuration | Discovery(OIDC) | Client / Resource Server | エンドポイントと鍵の場所を配布。ドメインではなく発行者 URL 配下にある |
{発行者}/.well-known/jwks.json | JWKS | Resource Server | 署名検証用の公開鍵 |
以上の構造から、次の指針が導かれる。
- ブラウザやモバイルを持つアプリは認可コード + PKCE を選ぶ。インプリシットは新規では使わない
- API には必ずアクセストークンを送る。ID トークンはアプリの中で使い切る
- 自作 API の権限はリソースサーバーのカスタムスコープで表現し、API 側で
scopeクレームを検査する。ユーザーの所属による認可はcognito:groupsで補う - サーバー間通信はクライアントクレデンシャル専用のアプリクライアントを別に作り、カスタムスコープだけを許可する
- フロントエンドから AWS サービスを直接呼ぶときだけ ID プールを足す。そのときの認可は IAM ロールで決まるため、ロールの信頼ポリシーと権限を最小にする
- 外部 IdP を足すときは、IdP 側に登録する redirect URI が
/oauth2/idpresponseであることを忘れない - トークン寿命は既定の 1 時間 / 30 日から始め、失効を早く効かせたい API ではアクセストークンを短くしてリフレッシュに頼る
- 検証コードは JWKS をキャッシュしたオフライン検証にし、Cognito への同期呼び出しをリクエスト経路に置かない