ALB(Application Load Balancer)のリスナールールにauthenticate-cognitoアクションを足すと、既存のWebアプリにログイン画面のコードを書かずにCognito認証を掛けられます。この記事では、Cognitoのアプリクライアント作成からactions.jsonによるルール設定、バックエンドでのx-amzn-oidc-dataの署名検証、ログアウトの実装までを、AWS公式ドキュメントの値に沿って手順で示します。後半で切り分けるのは、ALB認証を選ぶ条件と、アプリ側で認証を実装すべき場面です。
まとめ:ALB認証はブラウザ向け画面の後付けに使い、署名検証まで実装する
ALBのCognito認証は、社内ツールや管理画面のように「ブラウザで開く画面全体にログインを掛けたい」ケースで工数を大きく減らせます。必要な作業は、シークレット付きのアプリクライアント作成、コールバックURL/oauth2/idpresponseの登録、HTTPSリスナーへの認証ルール追加の3段階です。
ただしALBに任せて終わりではありません。バックエンドはx-amzn-oidc-dataのES256署名とsignerのALB ARNを検証してからユーザーを識別し、ログアウトではALBのCookieとCognitoのセッションの両方を消す必要があります。APIクライアントやモバイルアプリが主な利用者なら、ALB認証は選ばずAPI Gatewayかアプリ側の実装へ寄せます。
ALBのCognito認証の仕組み|リスナールールで既存アプリにログインを足す流れ
ALBの認証は、リスナールールの「認証アクション」と「転送アクション」を順番に並べる仕組みです。ALBそのものの役割やリスナールールの基本はElastic Load Balancingとは?ELBとALBの違い・4種類の使い分けと料金・設定で整理しています。
認証アクションとforwardを並べる構成と11段階のリダイレクトの流れ
ALBのユーザー認証の公式ドキュメントでは、認証の流れの説明は11段階です。ALBはまずリクエストに認証セッションCookieがあるかを見て、無ければCognitoの認可エンドポイントへリダイレクトします。ログイン後に戻ってきた認可コードをALB自身がトークンへ交換し、ユーザー情報エンドポイントからクレームを取り、Cookieを付けて元のURLへ戻します。
2回目以降のリクエストはCookieの検証から始まり、ターゲットへX-AMZN-OIDC-*ヘッダーを付けて転送されます。アプリから見ると「ログイン済みのユーザー情報がヘッダーで届く」状態になるため、OAuthのコード交換やセッション管理を自前で書く必要がありません。Cognito全体の設計は親記事のCognitoで認証基盤を実装する手順:ユーザープール構築からJWT検証までを前提にしています。
使える前提条件:HTTPSリスナー・東京と大阪・IdPへのIPv4通信
認証アクションはHTTPSリスナーでしか設定できません。HTTP:80のリスナーしか無い構成では、先にACM証明書を用意してHTTPS:443を追加します。Cognito連携の対応リージョンには東京(ap-northeast-1)と大阪(ap-northeast-3)が含まれています。
見落としやすいのは通信経路です。ALBはCognitoのトークンエンドポイントとユーザー情報エンドポイントへIPv4で通信するため、ALBのセキュリティグループとネットワークACLで外向きHTTPSを許可しておく必要があります。内部ALB(internal)の場合はNATゲートウェイ経由でないとエンドポイントへ届かず、ログイン後に戻ってきた時点で失敗します。
Cognito側の準備手順|シークレット付きアプリクライアントとコールバックURL
ALBが使うアプリクライアントは、SPA用やモバイル用のクライアントとは要件が違います。既存のクライアントを流用せず、ALB専用に1つ作るのが安全です。
ユーザープールのドメインとシークレット付きクライアントを作るCLI例
公式ドキュメントが求める条件は、クライアントシークレットを生成すること、認可コードグラント(code)を使うこと、ALBと同じOAuthスコープを許可することの3点です。create-user-pool-clientのCLIリファレンスにある引数で書くと次のようになります。ユーザープールIDやドメイン名は自社の値に置き換えてください。
# ユーザープールにドメイン(接頭辞)を割り当てる
aws cognito-idp create-user-pool-domain \
--domain example-app-auth \
--user-pool-id ap-northeast-1_EXAMPLE
# ALB専用のアプリクライアント(シークレットあり・認可コードフロー)
aws cognito-idp create-user-pool-client \
--user-pool-id ap-northeast-1_EXAMPLE \
--client-name alb-admin \
--generate-secret \
--allowed-o-auth-flows code \
--allowed-o-auth-scopes openid email \
--allowed-o-auth-flows-user-pool-client \
--supported-identity-providers COGNITO \
--callback-urls https://admin.example.co.jp/oauth2/idpresponse \
--logout-urls https://admin.example.co.jp/signed-out
スコープにはopenidを必ず含めます。ALBはIDトークンが返るスコープを前提にしており、aws.cognito.signin.user.adminだけではIDトークンが返りません。クライアント設定の各項目の意味はアプリクライアントの公式ドキュメントで確認できます。
コールバックURLは小文字の/oauth2/idpresponseで登録する理由
コールバックURLには、ALBのDNS名か独自ドメイン(CNAME)に/oauth2/idpresponseを付けた値を登録します。この受け口はALBが内部で処理するパスで、アプリ側にルーティングを作る必要はありません。利用者が独自ドメインでアクセスするなら、ALBのDNS名ではなく独自ドメイン側を登録します。
公式ドキュメントには「アプリクライアントのコールバックURLはすべて小文字にする」と明記されています。Admin.Example.co.jpのように大文字を混ぜるとリダイレクトURLの不一致でログインが完了しません。設定作業者には、ALBに認証を設定するためにcognito-idp:DescribeUserPoolClientの権限も付与しておきます。Cognitoの料金やユーザープールの基本はAmazon Cognito(コグニート)とは?料金・できること・使い方で確認できます。
ALBリスナーのCognito認証設定|JSONとCLIによる管理画面のルール追加
認証はリスナー全体にもパス単位にも掛けられます。既存アプリへの後付けでは、まず管理画面のパスだけに掛け、問題が無いことを確かめてから範囲を広げると切り戻しが簡単です。
create-ruleとactions.jsonで管理画面のパスだけに認証を掛ける例
次の内容をactions.jsonとして保存し、aws elbv2 create-ruleでルールを追加します。Orderの小さい認証アクションが先に評価され、通過したリクエストだけが転送アクションへ進みます。
[
{
"Type": "authenticate-cognito",
"Order": 1,
"AuthenticateCognitoConfig": {
"UserPoolArn": "arn:aws:cognito-idp:ap-northeast-1:123456789012:userpool/ap-northeast-1_EXAMPLE",
"UserPoolClientId": "1example23456789",
"UserPoolDomain": "example-app-auth",
"SessionCookieName": "AdminAuthCookie",
"SessionTimeout": 28800,
"Scope": "openid email",
"OnUnauthenticatedRequest": "authenticate"
}
},
{
"Type": "forward",
"Order": 2,
"TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/admin-tg/0123456789abcdef"
}
]
# /admin 配下だけに認証を掛ける(優先度10)
aws elbv2 create-rule \
--listener-arn LISTENER_ARN \
--priority 10 \
--conditions Field=path-pattern,Values="/admin/*" \
--actions file://actions.json
AuthenticateCognitoActionConfigのAPIリファレンスによると、必須はUserPoolArn・UserPoolClientId・UserPoolDomainの3つです。省略時はScopeがopenid、SessionCookieNameがAWSELBAuthSessionCookie、SessionTimeoutが604800秒(7日)になります。
OnUnauthenticatedRequestの3値をアプリの種類で選び分ける基準
未ログインのリクエストをどう扱うかはOnUnauthenticatedRequestで決めます。既定値はauthenticateです。
| 設定値 | 未ログイン時の動き | 向く画面 |
|---|---|---|
| authenticate | ログイン画面へ転送 | 管理画面・社内ツール |
| deny | 401を返す | 数秒ごとに通信するSPA |
| allow | ヘッダーなしで転送 | ログイン有無で表示を変える画面 |
実務ではauthenticateから始めれば足ります。SPAが裏で呼ぶAPIのパスにだけdenyを設定するのは、AJAX通信がログイン画面のHTMLを受け取って壊れる事故を防ぐためです。allowはヘッダーの有無でアプリが表示を分ける前提なので、後述の署名検証を省くと認証が素通りになります。
SessionTimeoutと7日のCookie期限、複数アプリのCookie名
Cookie自体の有効期限は7日で固定ですが、実際の認証セッションの長さはSessionTimeoutで決まります。最短1秒まで短くできるため、管理画面なら28800秒(8時間)のように業務時間に合わせて設定します。Cognitoがリフレッシュトークンを返す場合、ALBはアクセストークンの期限切れのたびにユーザー情報を更新し、セッションのタイムアウトまで再ログインを求めません。
1つのALBで複数のアプリを公開するなら、認証ルールごとにSessionCookieNameを変えます。同じ名前のままだと、片方のアプリでログインしたCookieでもう片方にも入れてしまい、アプリごとに別のクライアントで認証させたい意図が崩れます。
x-amzn-oidc-dataの検証実装|ES256署名とsignerの確認
ALBを通過したリクエストには、ユーザー情報を載せたヘッダーが付きます。アプリはこの値を鵜呑みにせず、署名を検証してからユーザーを識別します。
ALBの署名付きヘッダー1つと真正性を確認できない2つのヘッダー
ALBが付けるヘッダーは3種類ですが、署名されているのはx-amzn-oidc-dataだけです。x-amzn-oidc-accesstokenとx-amzn-oidc-identityは後方互換のための平文で、アプリ側で真正性を確かめる手段がありません。x-amzn-oidc-identityをそのままユーザーIDとして使う実装は、ALBを経由しない経路から偽のヘッダーを送られた時点で成りすましを許します。
対策は2つあります。ターゲットのセキュリティグループでALBのセキュリティグループからの通信だけを許可すること、そしてx-amzn-oidc-dataの署名を検証してsubクレームでユーザーを特定することです。前者だけでは同じVPC内の別経路を塞ぎ切れないため、両方を実装します。
PythonでES256署名とALBのARNを検証するコード例
JWTのヘッダーにあるkidで公開鍵を取得し、ES256で署名を検証します。公式ドキュメントで確認対象としているのは、署名に加えてヘッダーのsignerが自社ALBのARNと一致するかどうかです。PyJWTを使う場合はpip install "pyjwt[crypto]" requestsで依存を入れます。
import re
import jwt
import requests
REGION = "ap-northeast-1"
EXPECTED_ALB_ARN = (
"arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:"
"loadbalancer/app/admin-alb/0123456789abcdef"
)
_public_keys = {} # kidごとに公開鍵をキャッシュする
def verify_alb_user(headers):
token = headers.get("x-amzn-oidc-data")
if not token:
raise PermissionError("ALBの認証ヘッダーがありません")
header = jwt.get_unverified_header(token)
# 自社ALBが署名したトークンかを先に確かめる
if header.get("signer") != EXPECTED_ALB_ARN:
raise PermissionError("想定外のALBが署名しています")
kid = header.get("kid", "")
if not re.fullmatch(r"[0-9a-fA-F-]{36}", kid):
raise PermissionError("kidの形式が不正です")
if kid not in _public_keys:
url = f"https://public-keys.auth.elb.{REGION}.amazonaws.com/{kid}"
_public_keys[kid] = requests.get(url, timeout=5).text
claims = jwt.decode(token, _public_keys[kid], algorithms=["ES256"])
return claims["sub"], claims.get("email")
algorithms=["ES256"]を固定するのは、署名検証前のヘッダーのalgを信用して検証アルゴリズムを選ばないためです。アプリ側で指定したES256による署名検証を通過した値だけを、ユーザーの識別に使う前提です。kidは署名検証前の値なので、URLに埋め込む前に形式を確かめています。公開鍵の取得先はリージョンごとにpublic-keys.auth.elb.リージョン.amazonaws.comで、GovCloudだけは別のエンドポイントになります。
Node.jsのaws-jwt-verifyによるALB署名と発行元の検証
Node.jsでは、AWSが公開しているaws-jwt-verifyのAlbJwtVerifierが、署名・signer・発行者・クライアントIDの確認をまとめて引き受けます。README上の対応環境はNode.js 18以上です。
import { AlbJwtVerifier } from "aws-jwt-verify";
// 検証器はリクエストごとに作らず、起動時に1回だけ作る
const verifier = AlbJwtVerifier.create({
albArn: "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/admin-alb/0123456789abcdef",
issuer: "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_EXAMPLE",
clientId: "1example23456789",
});
export async function currentUserId(req) {
const payload = await verifier.verify(req.headers["x-amzn-oidc-data"]);
return payload.sub;
}
公開鍵の取得先はalbArnのリージョンから自動で組み立てられ、取得した鍵はキャッシュされます。複数のALBから同じアプリを呼ぶ構成での指定方法は、albArnを配列で渡す形式です。自前でJWTをデコードする処理を書かずに済むので、Node.jsのアプリではこちらを第一候補にします。
ログアウトとセッション切れの実装|分割CookieとCognitoの/logout
ALB認証で問い合わせが多いのは「ログアウトしたのにまた入れる」という症状です。ALBのCookieとCognitoのセッションは別物なので、両方を終わらせる処理をアプリ側に置きます。
最大4分割の認証Cookieを失効させてからCognitoの/logoutへ送る
ALBの認証Cookieは4KBを超えると複数に分割され、最大16KB・4分割まで使われます。公式ドキュメントの手順は、すべての認証Cookieの期限を過去にしたうえで、Cognitoのログアウトエンドポイントへリダイレクトする流れです。Flaskで書くと次のようになります。
from urllib.parse import urlencode
from flask import Flask, make_response, redirect
app = Flask(__name__)
COGNITO_DOMAIN = "https://example-app-auth.auth.ap-northeast-1.amazoncognito.com"
CLIENT_ID = "1example23456789"
COOKIE_NAME = "AdminAuthCookie" # actions.jsonのSessionCookieNameと合わせる
@app.get("/admin/logout")
def logout():
query = urlencode({
"client_id": CLIENT_ID,
"logout_uri": "https://admin.example.co.jp/signed-out",
})
resp = make_response(redirect(f"{COGNITO_DOMAIN}/logout?{query}"))
# 分割された認証Cookie(-0〜-3)をすべて失効させる
for i in range(4):
resp.set_cookie(f"{COOKIE_NAME}-{i}", "", expires=0, path="/", secure=True)
return resp
分割後のCookie名は、ブラウザの開発者ツールで実際の名前を確認してから合わせてください。logout_uriはアプリクライアントの許可済みサインアウトURLに登録した値と一致している必要があります。戻り先の/signed-outは認証ルールの外に置きます。認証の掛かったパスに戻すと、そのまま再ログイン画面へ飛ばされて「ログアウト完了」を表示できません。
15分のログイン期限と11KBのクレーム上限で起きる401と500の見分け方
ALB認証特有のエラーは2種類に絞れます。1つはログイン画面を開いたまま15分以上放置した場合の401で、この上限は変更できません。利用者にはページの再読み込みで解決すると案内するだけで足ります。
もう1つは、ユーザークレームとアクセストークンの合計が11KBを超えた場合の500です。このときCloudWatchのELBAuthUserClaimsSizeExceededメトリクスが増えるので、500が出たらまずここを見ます。Cognitoのカスタム属性に長い文字列を詰め込んだ構成で起きやすいため、ALB向けのスコープはopenid email程度に絞り、詳細な属性はアプリがユーザープールから取りに行く設計にします。
ALB認証を採用する条件と見送る場面|アプリ内実装・API Gatewayとの比較
ALB認証は「アプリに手を入れずにログインを足せる」点が最大の利点で、その裏返しとして細かい制御はできません。どこまで任せられるかで採否を決めます。
社内ツールや管理画面への認証の後付けでALB認証を選ぶ3つの条件
次の条件がそろうなら、ALB認証を選びます。
- 利用者がブラウザで画面を開く(API専用クライアントが主ではない)
- ログインできれば全員が同じ画面を使える、またはパス単位の出し分けで足りる
- 既存アプリの改修範囲を、ヘッダー検証とログアウト処理の追加に留めたい
市販のOSSツールや改修しにくいレガシーな管理画面にCognitoのMFAやEntra IDなどの外部IdP連携を掛けたい場面では、アプリ内に認証を実装する場合と比べて工数の差が最も大きく出ます。ユーザープールの設計から既存システムへの組み込みまで外部に任せたい場合は、認証基盤・ID管理システム開発として要件整理から対応できます。
画面ごとの権限制御やモバイル向けAPIが主ならアプリ側へ寄せる
ALB認証を見送るのは、ログイン後の権限制御がアプリの中心になる場合です。ALBが判断できるのは「ログイン済みかどうか」とパス単位のルールまでで、ロールごとの画面制御はアプリ側に残ります。モバイルアプリやバッチのようにブラウザのリダイレクトを使えないクライアントが主なら、認証アクションは使えません。
機械同士の通信には、ALBのリスナーにJWT検証(jwt-validation)を設定する方法があり、Cognitoのクライアント認証情報フローで発行したRS256のアクセストークンを検証できます。利用者別の流量制限やAPIキーが要るならAPI Gatewayが残り、その比較はAPI GatewayとALBの違い:Lambda・ECSの前段をタイムアウト・料金・認証で選ぶとAPI Gatewayの認証:Cognito・JWT・Lambdaオーソライザー・IAM認可の使い分けで扱っています。
ALBとCognitoの認証設定・画面表示・運用についてよくある質問と回答
ALBにCognito認証を設定するときに出やすい疑問を、公式ドキュメントの記載をもとに回答します。
Cognitoを使わずにEntra IDやOktaでログインさせることはできますか?
可能です。方法は2つあります。1つはALBのauthenticate-oidcアクションでIdPを直接指定する方法で、IdP側でクライアントIDとシークレットを発行し、コールバックURLに/oauth2/idpresponseを登録します。もう1つはCognitoのユーザープールにSAMLやOIDCのIdPをフェデレーションし、ALBはauthenticate-cognitoのまま使う方法です。複数のIdPをまとめたい、Cognito側でユーザーを管理したい場合は後者が向きます。
ALBのヘルスチェックや別サービスからのAPI呼び出しも認証で止まりますか?
ヘルスチェックはALBがターゲットへ直接送るもので、リスナールールの認証アクションは通りません。止まるのは、認証ルールに一致するパスへ外部から届くリクエストです。別サービスからのAPI呼び出しはブラウザのログイン画面を経由できないため、そのパスだけ認証なしのルールかJWT検証のルールに分けます。パスを分けずに全体へ認証を掛けると、連携していたバッチやWebhookが一斉に302や401で失敗するので、設定前に呼び出し元を洗い出しておきます。
ALB経由で表示されるログイン画面を日本語化したり自社ロゴを出せますか?
ALBが表示するログイン画面はCognitoのマネージドログイン(旧Hosted UI)なので、Cognito側のブランディング設定で変更できます。ロゴや配色、表示言語を変える場所は、ALBではなくCognito側のマネージドログインのブランディング設定です。具体的な手順はAWS Cognito Hosted UI(マネージドログイン)のカスタマイズ・日本語化と実装手順にまとめています。ALB側で変えられるのは、AuthenticationRequestExtraParamsで認可リクエストに追加パラメータを渡す部分(最大10個)だけです。
CloudFrontの後ろに置いたALBでもCognito認証は使えますか?
使えますが、CloudFront側に3つの設定が要ります。すべてのリクエストヘッダー・クエリ文字列・Cookieをオリジンへ転送することです。クエリ文字列が届かないとALBが認可コードを受け取れず、Cookieが届かないとログイン後も毎回ログイン画面へ戻されます。認証済みのページがキャッシュされると、セッション切れ後もキャッシュから表示されるため、キャッシュを無効にするかTTLを認証Cookieより短くします。経路全体でHTTPSの443番ポートを使うことも条件です。
ログアウトしたのにパスワードを入れずに再ログインできてしまうのはなぜですか?
ALBのCookieだけを消して、Cognito側のセッションが残っているのが典型的な原因です。ALBは次のアクセスでCognitoへリダイレクトしますが、Cognitoにログイン状態が残っていれば資格情報を聞かずに認可コードを返します。Cookieの失効に加えてCognitoの/logoutへリダイレクトしてください。外部IdPとフェデレーションしている場合、Cognitoの/logoutはIdP側のセッションまでは終了させないため、IdPのログアウトページへの誘導も必要です。
関連記事
- Cognitoで認証基盤を実装する手順:ユーザープール構築からJWT検証まで:ALB連携の前提になるユーザープールとJWT検証の全体像。
- Amazon Cognito(コグニート)とは?料金・できること・使い方をCLIとコード例で解説:Cognitoの機能と料金の基本。
- Elastic Load Balancingとは?ELBとALBの違い・4種類の使い分けと料金・設定を解説:リスナールールなどALBの基本設定。
- API GatewayとALBの違い:Lambda・ECSの前段をタイムアウト・料金・認証で選ぶ:前段にALBとAPI Gatewayのどちらを置くかの比較。
- 認証・ID管理とは?MFA・SSO・OIDC・IDaaSの違いと選定を解説:OIDCやSSOなど認証方式の選び方。