
初めての Cognito Managed Login
製造ビジネステクノロジー部のかずえです。
Cognito初心者です。ヨチヨチです。Webアプリのログイン機能と、ログインユーザー管理に便利なサービス、くらいの雑な理解しかありません。
しかし仕事で使うことになったので勉強しているところです。
Cognitoには「ユーザープール」と「IDプール」の2つの機能がありますが、本記事ではログイン機能を担うユーザープールを扱います。
Cognitoが無い世界
別にCognitoを使わなくても、WebアプリのデータベースにユーザーのIDとパスワードを保存しておけばログイン処理は作れます。昔作ってました。むしろCognitoという第三者が増えることでわかりにくくなるのでは?Cognitoを追加すると何が嬉しいの?
Cognitoがあると嬉しいこと
パスワードを自分のデータベースに持たなくて済む
なるほど。たしかに自前でログイン処理を実装するとそのアプリのDBにパスワードを保存する必要がありますね。ハッシュ化、ソルト付きなどして万一漏洩した際のことも考慮するとは思いますが、それでも心配です。Cognitoに任せればこの心配は消すことができます。
ログインに付随する機能を作らなくていい
ログイン機能には色々必要な周辺機能があります。
- サインアップ時のメール/電話番号の確認
- パスワードを忘れたときのリセット
- MFA(TOTP、SMS、メールのワンタイムコード)
- パスキーやパスワードレスでのサインイン
- サインイン画面そのもの
これらの実装をCognitoを使ってショートカットすることができます。
※ ただし、メールのワンタイムコードによるMFAやパスキー、パスワードレスでのサインインは Essentials プラン以上の機能です(新しく作るユーザープールの初期プランは Essentials)。
外部のIDでのログインもまとめて扱える
前述の「実装をCognitoを使ってショートカット」と重複しているとも言えるかもしれませんが、
Google などのソーシャルログインや、Okta、ADFS といった企業の ID 基盤(SAML/OIDC)を利用したログインに対応しています。どの方法でログインしても、Cognito が JWT という同じ形式のトークンに揃えてアプリに渡します。アプリ側はログイン方法ごとの違いを気にしなくて済みます。これらを一種類ごとに個別実装していくのは骨が折れるでしょう。
フローを学ぶ

Opusが作ってくれた図
上記図はアプリにログインするときの基本形だそうです。(Opus談) 「やっぱCognito入れると複雑になるやん」と思いました。
②と④はリダイレクト(302)です。Cognitoの画面にリダイレクトしてログインして、その後またリダイレクトしてアプリに戻ってくる。④のURLはhttps://アプリ/callback?code=abc123 のような形で、code がURLにそのまま載っています。
ここで1点疑問です。④で code ではなく、いきなり token をURLに渡せばよいのでは?そうすれば⑥⑦が要らなくなるのでは?
実はこのやり方(=いきなり token をURLに渡す)も Implicit グラントという方式として存在し、Cognito でも選べます。ただし Cognito のドキュメントでは旧来の方式(legacy)という扱いで、推奨されていません。
推奨されない理由は、トークンが盗まれる可能性があるから、です。前述のとおり④では値をURLに載せるので、ブラウザのアドレスバーや履歴、拡張機能など、いろいろな場所から見える可能性があります。なので④の段階では漏れても単体では悪用しにくい codeを返しています。tokenはブラウザを介さない ⑥⑦のやりとりにて取得します。
- code を token に交換するには、⑥でアプリしか持っていないクライアントシークレット(client secret)が必要です
- code の有効期限は5分で、一度使うと無効になります(Cognito のドキュメント)
Managed Loginしてみる
今回はまずは図の④(code が URL に載って返ってくる瞬間)までを確認します。

図の再掲
1. ユーザープールの作成
マネジメントコンソールでCognitoを開いて、「ユーザープールを作成」をクリックします。

アプリケーションタイプは「従来のウェブアプリケーション」を選択します。

オプション設定の「サインイン識別子のオプション」は「メールアドレス」を選択します。

リターン URLには http://localhost:3000/callback を入れ、「ユーザーディレクトリを作成する」をクリックします。

画面遷移し、「アプリケーションとユーザープールが正常に作成されました」と出ます。

ここで作られた2つは、ざっくり次のような役割です。
- ユーザープール: ユーザーの情報(メールアドレスやパスワードなど)を保存しておく場所です。いわばユーザー名簿
- アプリケーション(ドキュメント上は「アプリクライアント」): ユーザープールを利用するアプリごとの設定です。クライアントID、クライアントシークレット、この手順で入力したリターンURL、許可するログイン方式などを持っています
1つのユーザープールにアプリケーションは複数作れるので、たとえばWebアプリとAndroidアプリで同じユーザー名簿を共有しつつ、設定はアプリごとに分ける、といった使い方ができます(Cognito のドキュメント)。図の⑥で使うクライアントシークレットも、このアプリケーションに紐づいています。
さて、上記「アプリケーションとユーザープールが正常に作成されました」が表示された画面には Quick Setupガイドが記載されていますが、無視して画面最下部「概要に移動」をクリックします。

2. ユーザーを作成
左ペインから「ユーザー」をクリック。

ユーザーをひとり作成します。

以下で作成します。
- 招待を送信しない
- パスワードの設定

作成できました。

3. ログインページを表示
左ペインから「アプリケーションクライアント」をクリック。そのあと先程作ったアプリクライアントをクリック。

右上「ログインページを表示」をクリック。

出ました!ログインページです。

URLは https://ap-northeast-1xxxx.auth.ap-northeast-1.amazoncognito.com/login?client_id=yyy&response_type=code&scope=email+openid+phone&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback という感じになっていました。設定したリターンURLはクエリパラメータとして付いているんですね。
4. ログインしてみる
「2. ユーザーを作成」で作成したユーザーのメールアドレスとパスワードでログインしてみます。
入力後、Change passwordの画面に遷移しました。

たしかにユーザー作成時に「パスワードを強制的に変更」と表示されてましたね。↓再掲

再掲 ユーザー作成直後: 「確認ステータス」欄を見てください
新パスワードを登録すると、「このサイトにアクセスできません」に遷移しました。

URLは http://localhost:3000/callback?code=43609aaf-7271-46b0-82f3-afd7b08d65d2 となっています。localhost:3000 で何も稼働させてないので、こうなる(=「このサイトにアクセスできません」)のは当然です。リターン URLにリダイレクトして、codeがクエリパラメーターに付いていますね。前述の図の「④codeをつけてアプリへ戻す」が確認できました!

図の再掲
次は続きの⑥⑦のトークン交換の部分を確認します。
トークン交換を自分でやってみる
1. 必要な情報を控えておく
マネジメントコンソールに表示されている以下3つを控えておきます。
ドメイン
左ペインの「ブランディング → ドメイン」にあります。

クライアント ID と クライアントシークレット
「アプリケーションクライアント」の画面にあります。

2. クライアントシークレットを変数に格納
以下コマンド実行後に前述のクライアントシークレットをペーストします。CLIENT_SECRET変数に値が代入されます。
read -s CLIENT_SECRET
3. code再取得
先ほど取得したcodeは失効しているかと思います。再度ログインして新codeをゲットしましょう。
4. tokenエンドポイントを叩く
curl -s -X POST "https://<ドメイン>/oauth2/token" \
-u "<クライアント ID>:${CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<新しいcode>" \
-d "redirect_uri=http://localhost:3000/callback"
成功するとJSON が返ってきます。
ちなみにもう一度同じ curlコマンドを実行すると {"error":"invalid_grant"} というレスポンスになります。code → token の交換は一度きりだということです。
レスポンスの JSON には、次の5つのフィールドが入っています。
| フィールド | 中身 | 何に使うか |
|---|---|---|
id_token |
JWT | 「誰がログインしたか」の証明書です。アプリ(BFF)がユーザー情報を知るために読みます。 |
access_token |
JWT | 「何をしてよいか」の通行証です。API を呼ぶときに Authorization: Bearer ... として付けて送ります |
refresh_token |
暗号化された JWT(JWE)。中身はアプリからは読めず、Cognito に渡して使うだけ | 上の2つが期限切れになったとき、ユーザーにログインし直させずに再発行してもらうためのものです。 期限が長いので(既定30日)、漏れたときの被害も一番大きくなります。 |
expires_in |
数値(既定は 3600) |
id_token と access_token があと何秒有効か。既定では1時間です |
token_type |
"Bearer" |
access_token の使い方の種類です。「持っている人が使える(Bearer = 持参人)」という意味です。 token を持っているだけで本人扱いされるので、盗まれればそのまま使われます。 |
JWT?
ジョットと読みます。JSON Web Token の略です。
JWT には署名付き(JWS)と暗号化(JWE)の2つの形式があります。id_token と access_token は署名付きのほうで、ヘッダー.ペイロード.署名 の3つを . でつないだものです(暗号化の JWE は5つに分かれます)。
署名付きの JWT のペイロードは暗号化されておらず、Base64url(URL で使える文字だけにした Base64)で符号化されているだけです。誰でもデコードして読めるので、秘密情報は入れないのが原則です。
改ざんを防いでいるのは3つ目の署名です。アプリは、Cognito が公開している公開鍵でこの署名を検証してから中身を信用します。
id_token の中身を見る
ログインし直して新しい code を取り、さっきの curl を RESP=$(...) で囲んで実行してください。これで、さきほどのJSONをRESP という中間変数に格納し、次のpythonコマンドでデコードします。同じセッション内なら 先ほど定義した変数 CLIENT_SECRET はそのまま使えます。
RESP=$(curl -s -X POST "https://<ドメイン>/oauth2/token" \
-u "<クライアント ID>:${CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<新しいcode>" \
-d "redirect_uri=http://localhost:3000/callback")
id_token の中身(ペイロード)をデコードして表示します。
python3 -c 'import json,sys,base64; t=json.loads(sys.stdin.read())["id_token"].split(".")[1]; print(json.dumps(json.loads(base64.urlsafe_b64decode(t+"="*(-len(t)%4))),indent=2,ensure_ascii=False))' <<< "$RESP"
JSONが出力されると思います。
{
"at_hash": "BV9bhy0XmKRLc0HuAntskA",
"sub": "47a40af8-8021-7087-bc3b-xxxxxxxxxxxx",
"iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxxxx",
"cognito:username": "47a40af8-8021-7087-bc3b-xxxxxxxxxxxx",
"origin_jti": "f02c185e-de3b-4182-98ad-366f0dbc228f",
"aud": "72cd9abvmlmkt99xxxxxxxxxxx",
"token_use": "id",
"auth_time": 1792251173,
"exp": 1792254773,
"iat": 1792251173,
"jti": "41f55969-8359-4a1f-bcec-f989e549a454",
"email": "kazue@example.com"
}
JSONの各フィールド名は以下表のクレーム列と一致しているはずです。それぞれの役割を説明します。
| 役割 | クレーム | 意味 |
|---|---|---|
| 誰か | sub |
ユーザープール内でユーザーを一意に表す ID(UUID) |
cognito:username |
Cognito 上のユーザー名 | |
email |
メールアドレス | |
| 有効性の検証 | iss |
発行者。ユーザープールの URL。ほかのプールの token を弾くために照合します |
aud |
宛先。クライアント IDが入っていました。前述のとおり1つのユーザープールにアプリケーションは複数作れるので、受け取った token が自分宛てかどうかをこれで見分けます。 | |
exp / iat |
有効期限と発行時刻(UNIX 秒) | |
auth_time |
実際にパスワードを入れてログインした時刻。refresh で再発行しても変わりません | |
| token の管理 | token_use |
id か access か。取り違え防止用です |
jti / origin_jti |
この token 自体の ID と、元になったログインの ID。失効処理に使われます | |
at_hash |
一緒に発行された access_token のハッシュ。2つがセットで発行されたことの証明です |
これは「ログインできた」と言えるのか?
ここまで、Cognitoを使ったログイン処理の流れを追ってきました。

図の再掲
図中の①から⑦まで完遂したわけですが、これで(=⑦の段階で)「ユーザーのログインは完了した?」と言えるのでしょうか?
この問いは、誰の視点で見るかによって答えが変わる、というのが面白いところです。登場人物ごとに「ログインした」状態が別々にあります。
まず Cognito の視点です。Cognito にとっての「ログインできた」は ③で ID とパスワードが正しいと確認できた時点です。このとき Cognito は、自分のドメイン宛てに Cookie(Cognito 側のセッション)をブラウザに置きます。
では、アプリ(BFF)にとってはどうでしょうか。実は⑦時点ではまだログインできたとは言えません。
- 検証: 受け取った id_token の署名、iss、aud、exp を確かめます。ここを通るまでは、中身を信用できません。(「JWT?」項でも「アプリは、Cognito が公開している公開鍵でこの署名を検証してから中身を信用します。」と書いていました)
- アプリのセッションを作る: 一般的には、検証が通ったら、アプリは「この人は sub = xxx のユーザー」という情報をサーバー側に保存します。そのセッション ID を Cookie に入れてブラウザに返し、元のページへリダイレクトします。
2のおかげで、以降のリクエストではブラウザが Cookie を送ってくるだけで、アプリは「さっきの人だ」とわかります。毎回 Cognito に問い合わせる必要はありません。
つまり、ブラウザ・アプリ・Cognito を合わせると、ログイン状態は3つの層で持たれています。
| 層 | どこにあるか | 寿命の目安 |
|---|---|---|
| Cognito のセッション | Cognito ドメインの Cookie | Cognito の設定次第。検証時は、ログインから約1時間で期限切れになる Cookie でした。 |
| token | アプリのサーバー側 | id/access は1時間、refresh は30日 |
| アプリのセッション | アプリのドメインの Cookie + サーバー側の保存先 | アプリが決める |
というわけで、アプリにとってはまだ「ログインできた」状態ではありません。途中段階です。
まとめ
Cognito のユーザープールを作り、マネジメントコンソールと curl だけで、ログインの流れを一つずつ追ってみました。
- Cognito を使うと、パスワードの保管やログイン周りの機能をアプリから切り離せる
- ログインは「ブラウザ経由で code を受け取る(①〜⑤)」と「サーバー同士で code を token に交換する(⑥⑦)」の2段構え。URL に載る code は、漏れても単体では悪用しにくいようになっている
- code の有効期限は5分で、使えるのは1回だけ。同じ code で2回目の交換をすると
invalid_grantになる - token エンドポイントからは
id_token・access_token・refresh_tokenが返ってくる。id_tokenは誰でもデコードして中身を読める id_tokenのaudにはクライアント ID が入っていて、token が自分宛てかどうかを見分けられる- ただし、⑦で token を受け取っただけでは、アプリにとってはまだ「ログイン完了」ではない。検証とセッションの作成が必要
最初は「やっぱ Cognito 入れると複雑になるやん」と思っていましたが、手で一つずつ叩いてみると、遠回りに見えるステップにもそれぞれ理由があることがわかりました。
次回は、今回保留にした「誰でも読める token の中身を、なぜ信用してよいのか」、つまり署名の検証を手で確かめてみたいと思います。








