🏝️

ALB の x-amzn-oidc-accesstoken で API Gateway Cognito オーソライザーの認可を通す

に公開

背景

ALBとCognitoを連携した認証から払い出される情報を用いて、API GatewayのCognitoオーソライザーの認可を通したい。

しかし認証のフローから、ALBとCognitoを連携して認証を行うと、認可コードは発行されるが、ALBが認可コードを握りつぶしてx-amzn-oidc-accesstoken, x-amzn-oidc-identity, x-amzn-oidc-dataをターゲットグループへヘッダーとして返却する。この場合、ターゲットグループ側では認可コードを利用してID Tokenを発行できないため、ターゲットグループからAPI GatewayのCognitoオーソライザーをID Tokenを使用して認証することができない。

ここでx-amzn-oidc-dataは、ユーザークレームのエンコードと署名の検証からhttps://public-keys.auth.elb.region.amazonaws.com/key-idの公開鍵で検証できる。しかし、Cognitoの公開鍵(https://cognito-idp.<Region>.amazonaws.com/<userPoolId>/.well-known/jwks.json)とは異なるため、Cognitoオーソライザーには使用できないことが分かる。

一方x-amzn-oidc-accesstokenについては、Cognitoの公開鍵で検証できるため、これをCognitoのアクセストークンとしてCognitoオーソライザーの認可に使えないかと考えた。結果として、x-amzn-oidc-accesstokenscopeを設定することでCognitoオーソライザーの認可を通すことができたので、その方法をメモしておく。

AWSのインフラ構成

以下の構成で検証を行う。はじめに、example.comでALB経由でFargateへアクセスし、そこで払い出されるx-amzn-oidc-accesstoken, x-amzn-oidc-identity, x-amzn-oidc-dataを取得する。そして、そのトークンを公開鍵で検証し、ユーザー属性エンドポイントとAPI Gateway Cognitoオーソライザーへアクセスできるかテストする。

room-208/alb-accesstoken-cognito-authorizerを使用してAWSのインフラを立ち上げる。使い方はリポジトリのAWS CDKの立ち上げを確認してください。

カスタムスコープの設定

リソースサーバーを使用したスコープ、M2M、および APIAmazon Cognito ユーザープールをオーソライザーとして使用して REST API へのアクセスを制御するから、アクセストークンを用いてCognitoオーソライザーの認可を得るためにはカスタムスコープの設定が必要であることが分かる。

この章では、x-amzn-oidc-accesstokenを使用してCognitoオーソライザーの認可を得るために必要な設定をメモする。

Cognitoの設定

Cognitoでは、まずリソースサーバーを作成し、それに対応するスコープを定義する必要がある。

その後、Cognitoのアプリケーションクライアントへ払い出すカスタムスコープを追加する。

以上の設定で、このアプリケーションクライアントの認証を受けたユーザーがカスタムスコープの権限を取得できる。つまり、以下のようにアクセストークンのペイロードのscopeへ、カスタムスコープが追加されるということである。

{
  "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX",
  "version": 2,
  "client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "origin_jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "token_use": "access",
  "scope": "openid example.com/api:all",
  "auth_time": 1759083604,
  "exp": 1759087204,
  "iat": 1759083604,
  "jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

ALBの設定

ALBが認可コードを使用してトークンエンドポイントへアクセスし、アクセストークンを払い出すため、ALBがどのスコープを要求するか事前に設定する必要がある。そのため以下のように、ALBのリスナールールのCognito認証において、カスタムスコープを定義する必要がある。

これによって、以下のようにx-amzn-oidc-accesstokenのペイロードのscopeへ、カスタムスコープが追加される。

{
  "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX",
  "version": 2,
  "client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "origin_jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "event_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "token_use": "access",
  "scope": "openid example.com/api:all",
  "auth_time": 1759083358,
  "exp": 1759086958,
  "iat": 1759083358,
  "jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

API Gatewayの設定

API Gatewayにリクエストされるアクセストークンとして、どのカスタムスコープを持っていた場合にアクセス許可を出すか設定する必要がある。そのため以下のように、REST APIの認可スコープへ、カスタムスコープを設定する必要がある。

トークンのテスト

x-amzn-oidc-accesstoken, x-amzn-oidc-dataの検証とアクセス確認を行う。具体的なテスト方法はリポジトリのトークンのテストを確認してください。

トークンの公開鍵を用いた検証

以下のalb-accesstoken-cognito-authorizer/python/01_test_token.pyを用いて、アクセストークンを公開鍵で検証できる。結果としては、改ざんがなくデコードできることを確認できた。

alb-accesstoken-cognito-authorizer/python/01_test_token.py
import os

import dotenv
import jwt
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from jwt import PyJWKClient

dotenv.load_dotenv()

REGION = os.environ["REGION"]

COGNITO_USER_POOL_ID = os.environ["COGNITO_USER_POOL_ID"]

HTTP_X_AMZN_OIDC_DATA = os.environ["HTTP_X_AMZN_OIDC_DATA"]
HTTP_X_AMZN_OIDC_ACCESSTOKEN = os.environ["HTTP_X_AMZN_OIDC_ACCESSTOKEN"]

alb_public_key_base_url = f"https://public-keys.auth.elb.{REGION}.amazonaws.com"
cognito_public_key_url = f"https://cognito-idp.{REGION}.amazonaws.com/{COGNITO_USER_POOL_ID}/.well-known/jwks.json"

# verify x-amzn-oidc-data
alb_header = jwt.get_unverified_header(HTTP_X_AMZN_OIDC_DATA)
alb_kid = alb_header["kid"]
alb_public_key_url = f"{alb_public_key_base_url}/{alb_kid}"
alb_pem = requests.get(alb_public_key_url).text
alb_public_key = serialization.load_pem_public_key(
    alb_pem.encode(), backend=default_backend()
)
alb_payload = jwt.decode(HTTP_X_AMZN_OIDC_DATA, alb_public_key, algorithms=["ES256"])

print(alb_payload)
# {
#    "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#    "email_verified": "true",
#    "email": "example@example.com",
#    "username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#    "exp": 1759073152,
#    "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX",
# }

# verify x-amzn-oidc-accesstoken
cognito_jwk_client = PyJWKClient(cognito_public_key_url)
cognito_signing_key = cognito_jwk_client.get_signing_key_from_jwt(
    HTTP_X_AMZN_OIDC_ACCESSTOKEN
)
cognito_payload = jwt.decode(
    HTTP_X_AMZN_OIDC_ACCESSTOKEN, cognito_signing_key.key, algorithms=["RS256"]
)

print(cognito_payload)
# {
#    "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#    "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX",
#    "version": 2,
#    "client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
#    "origin_jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#    "event_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#    "token_use": "access",
#    "scope": "openid api.example.com/api:all",
#    "auth_time": 1759073032,
#    "exp": 1759076632,
#    "iat": 1759073032,
#    "jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#    "username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
# }

ユーザー属性エンドポイントへのアクセス確認

以下のalb-accesstoken-cognito-authorizer/python/02_test_user_info.pyを用いて、アクセストークンを用いてユーザー属性エンドポイントへアクセスできるかテストできる。結果としては、ユーザー属性エンドポイントへアクセスできることを確認できた。

alb-accesstoken-cognito-authorizer/python/02_test_user_info.py
import os

import dotenv
import requests

dotenv.load_dotenv()

TOKEN = os.environ["HTTP_X_AMZN_OIDC_ACCESSTOKEN"]
COGNITO_DOMAIN = os.environ["COGNITO_DOMAIN"]

headers = {"Authorization": f"Bearer {TOKEN}"}

response = requests.get(f"https://{COGNITO_DOMAIN}/oauth2/userInfo", headers=headers)

print(response.text)
# {
#    "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
#    "email_verified": "true",
#    "email": "example@example.com",
#    "username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
# }

APIへのアクセス確認

以下のalb-accesstoken-cognito-authorizer/python/03_test_api_auth.pyを用いて、アクセストークンを用いてAPIへアクセスできるかテストできる。結果としては、API GatewayのCognitoオーソライザーの認可へ使用できることを確認できた。

alb-accesstoken-cognito-authorizer/python/03_test_api_auth.py
import os

import dotenv
import requests

dotenv.load_dotenv()

TOKEN = os.environ["HTTP_X_AMZN_OIDC_ACCESSTOKEN"]
API_DOMAIN = os.environ["API_DOMAIN"]

api_url = f"https://{API_DOMAIN}"
headers = {"Authorization": f"Bearer {TOKEN}"}

response = requests.get(api_url, headers=headers)

print(response.text)
# {"message":"Hello, world!"}

参考資料

Discussion