とほほのOpenID Connect入門

目次

OpenID Connectとは

用語

OpenID Connectを試してみる

OP側の準備 - AWS Cognito

OP として AWS Cognito を使用してみます。

ユーザープールと最初のクライアントを作成する

ここで言う「クライアント」は RP(Relying Party) を意味します。

  1. AWSマネジメントコンソール にログインする。
  2. [サービス]-[Cognito]-[ユーザープールを作成] から下記にチェックをつけて [次へ]
    ■ ユーザー名
    ■ Eメール
    
  3. 今回はテストなのでMFAなしを選択して [次へ]
    ● MFA なし
    
  4. 必須の属性に name を追加して [次へ]
    追加の必須属性:name
    
  5. テストなので Cognito でEメールを送信する設定(1日に最大50通の上限あり)で [次へ]
    ● Cognito で Eメールを送信
    
  6. 下記を入力、選択、追加して [次へ]。名前はリージョン内で重複できないので変更してください。
    ユーザープール名:example123
    ■ CognitoのホストされたUIを使用
    Cognitoドメイン:https://example123
    ● 秘密クライアント
    アプリケーションクライアント名:my-app-01
    認可されているコールバックURL:https://www.example.com/callback
    高度なアプリケーションクライアントの設定
      認証フロー:[ALLOW_USER_PASSWORD_AUTH] を追加
      OpenID Connect のスコープ:[電話番号] を削除、[プロファイル] を追加
    
  7. 内容を確認して [ユーザープールを作成]

作成されたパラメータを確認する

作成したユーザープール名をクリックして下記を確認します。

ユーザープールID:ap-northeast-1_nRedrVEYM

[アプリケーションの統合] をクリックして下記を確認します。

Cognito ドメイン:https://example123.auth.ap-northeast-1.amazoncognito.com

アプリケーションクライアント名をクリックして下記を確認します。

クライアントID:58mli5ubjefrok4j31glbkjovo
クライアントシークレット:ajvj5nn2vl59kn2il73cis5dafgh8rc32dp1rnc9gto0jidennq

ディスカバリ

リージョン名とユーザープールIDを用いて関連情報を確認することができます。これは OpenID Connect Discovery 1.0(↗) という仕様で定義されています。

curl https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_nRedrVEYM/.well-known/openid-configuration

{
  "issuer": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_nRedrVEYM",
  "authorization_endpoint": "https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize",
  "token_endpoint": "https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/token",
  "revocation_endpoint": "https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/revoke",
  "end_session_endpoint": "https://example123.auth.ap-northeast-1.amazoncognito.com/logout",
  "userinfo_endpoint": "https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/userInfo",
  "jwks_uri": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_nRedrVEYM/.well-known/jwks.json",
  "response_types_supported": [
    "code",
    "token"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "phone",
    "profile"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "subject_types_supported": [
    "public"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ]
}

ユーザープールにユーザを追加する

作成したユーザープールにユーザを追加します。

  1. [Cognito]-[ユーザープール]-(作成したユーザープール名)-[ユーザー]-[ユーザーを作成] で下記を入力して [ユーザーを作成]
    ユーザー名:yamada
    Eメールアドレス:yamada@example.com
    ■ Eメールアドレスを検証済みとしてマークする
    パスワード:***********
    

RP側の準備 - Pythonアプリ

検証用のコンテナを作成してログインします。

docker run -dit --name oidc-test -p 443:443 almalinux:9
docker exec -it oidc-test /bin/bash

コンテナ内でアプリを作成します。

cd
dnf -y update
dnf -y install git python3-pip
git clone https://github.com/curityio/example-python-openid-connect-client.git
cd ./example-python-openid-connect-client
pip3 install -r ./requirements.txt

settings.json を次のように書き換えます。

{
  "issuer": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_nRedrVEYM/",
  "port": 443,
  "base_url": "https://www.example.com",
  "verify_ssl_server": false,
  "debug": true,
  "scope": "openid profile email",
  "send_parameters_via": "query",
  "client_id": "58mli5ubjefrok4j31glbkjovo",
  "client_secret": "ajvj5nn2vl59kn2il73cis5dafgh8rc32dp1rnc9gto0jidennq",
  "redirect_uri": "https://www.example.com/callback"
}

SSL証明書を作成します。server's hostname に www.example.com などの FQDN を指定してください。

mkdir ./cert
cd ./cert
openssl genrsa 2048 > server.key
openssl req -new -key server.key > server.csr
cat server.csr | openssl x509 -req -signkey server.key > server.crt
cd ..

app.py の最後の1行を書き換えます。

_app.run('0.0.0.0', debug=debug, port=port, ssl_context=('./cert/server.crt', './cert/server.key'))

app.py を起動します。

python3 -B ./app.py

実施

https://www.example.com にアクセスし、[Sign In] ボタンを押すと Cognito のログイン画面が表示されます。ログインするとパスワードの変更を求められ、パスワード変更するとサンプル画面にログインすることができます。

認証の流れ

ログインする

OpenID Connect の認証フローでは、Authorization Code Flow や Implicit Flow などいくつかの処理フローが定義されていますが、最もよく利用される Authorization Code Flow の流れを下記に示します。下記の例では ID/パスワードによる認証の例を示していますが、認証は MFA を用いたりなど OP の実装にまかされます。

  1. User が RP にアクセスします。
    GET https://www.example.com
    
  2. RP は User にリダイレクトを指示します。scope には取得したい情報を指定します。response_type=code は Authorization Code Flow を意味します。client_id は OP に登録されているクライアントIDを指定します。state にはランダムな文字列を指定します。これは後ほどの検証のために用います。code_challenge には内部生成したランダムな検証コード(code_verifier)を code_challenge_method アルゴリズムで変換した値を指定します。S256 は SHA256 ハッシュを意味します。redirect_uri にはリダイレクトURIを、nonce にはランダム文字列を指定します。
    HTTP/1.1 302 Found
    Location: https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize
      ?scope=openid+profile+email
      &response_type=code
      &client_id=58mli5ubjefrok4j31glbkjovo
      &state=TH25QRQHJJC16YY7AKNS
      &code_challenge=Zfy2DA1iCm7v1bby0a_cs2bRVF6v-DMd0HNsaHbncR4
      &code_challenge_method=S256
      &redirect_uri=https://www.example.com/callback
      &nonce=097TREH1IZ5LRZ9F5MSN
    
  3. User は OP にリダイレクトします。
    GET https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize
      ?scope=openid+profile+email
      &response_type=code
      &client_id=58mli5ubjefrok4j31glbkjovo
      &state=TH25QRQHJJC16YY7AKNS
      &code_challenge=Zfy2DA1iCm7v1bby0a_cs2bRVF6v-DMd0HNsaHbncR4
      &code_challenge_method=S256
      &redirect_uri=https://www.example.com/callback
      &nonce=097TREH1IZ5LRZ9F5MSN
    
  4. OP は User にログイン画面へのリダイレクトを指示します。この部分の実装は OP によって様々です。
    HTTP/1.1 302 Found
    Location: https://example123.auth.ap-northeast-1.amazoncognito.com/login
      ?scope=openid+profile+email
      &response_type=code
      &client_id=58mli5ubjefrok4j31glbkjovo
      &state=TH25QRQHJJC16YY7AKNS
      &code_challenge=Zfy2DA1iCm7v1bby0a_cs2bRVF6v-DMd0HNsaHbncR4
      &code_challenge_method=S256
      &redirect_uri=https://www.example.com/callback
      &nonce=097TREH1IZ5LRZ9F5MSN
    

    User は OP のログイン画面を開きます。

    GET https://example123.auth.ap-northeast-1.amazoncognito.com/login
      ?scope=openid+profile+email
      &response_type=code
      &client_id=58mli5ubjefrok4j31glbkjovo
      &state=TH25QRQHJJC16YY7AKNS
      &code_challenge=Zfy2DA1iCm7v1bby0a_cs2bRVF6v-DMd0HNsaHbncR4
      &code_challenge_method=S256
      &redirect_uri=https://www.example.com/callback
      &nonce=097TREH1IZ5LRZ9F5MSN
    

    OP は User にログイン画面を返却します。

    HTTP/1.1 200 OK
    ※ログイン画面
    
  5. User はユーザが入力したユーザ名とパスワードを OP に送ります。
    POST https://example123.auth.ap-northeast-1.amazoncognito.com/login
      ?scope=openid+profile+email
      &response_type=code
      &client_id=58mli5ubjefrok4j31glbkjovo
      &state=TH25QRQHJJC16YY7AKNS
      &code_challenge=Zfy2DA1iCm7v1bby0a_cs2bRVF6v-DMd0HNsaHbncR4
      &code_challenge_method=S256
      &redirect_uri=https://www.example.com/callback
      &nonce=097TREH1IZ5LRZ9F5MSN
    username=yamada
    password=******
    
  6. OP は User に認証コードを返却し、RP のリダイレクトURIにリダイレクトすることを指示します。
    HTTP/1.1 302 Found
    Location: https://www.example.com/callback
      ?code=9979d435-50a9-43e2-b17f-e3be2e36eb8a
      &state=TH25QRQHJJC16YY7AKNS
    
  7. User は RP にリダイレクトします。RP は CSRF 攻撃を防ぐために state の値が自分が発行した値と合致しているかを検証します。
    GET https://www.example.com/callback
      ?code=9979d435-50a9-43e2-b17f-e3be2e36eb8a
      &state=TH25QRQHJJC16YY7AKNS
    
  8. RP は User から受け取った認証コード(code)を用いて OP にトークンを要求します。下記の例では client_id と client_secret を POST パラメータで渡していますが、client_id:client_secret を BASE64 エンコードしたものを Authorization ヘッダに Basic 認証で指定する方法などいくつかの認証方式があります。
    POST https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/token
    Content-Type: application/x-www-form-urlencoded
    
      client_id=58mli5ubjefrok4j31glbkjovo
      client_secret=ajvj5nn2vl59kn2il73cis5dafgh8rc32dp1rnc9gto0jidennq
      code=9979d435-50a9-43e2-b17f-e3be2e36eb8a
      code_verifier=OF6NR422P3P29J4DFV8G55F4HTFAIONTAR13BEUD72YDWFL0OPRJ0L6V5ZKCY9D3DFK6FY85MDIK9ZQVIRATGUU9EC01MYK2PVQA
      redirect_uri=https://www.example.com/callback
      grant_type=authorization_code
    
  9. OP は RP にトークンを返却します。IDトークン(id_token)はユーザが認証されたことを示すものでこの中にユーザーのメールアドレスなどの情報が含まれます。アクセストークン(access_token)はリソースにアクセスするために必要となるトークンです。リフレッシュトークン(refresh_token)はアクセストークンの有効期限(expires_in)が切れる前にトークンをリフレッシュするために使用します。
    {
      "id_token": "eyJraWQiOiIzUmFBWWZKRVwvY3NveXpmVDBlN1BtdTFmVjUzckxobVRsdXJiNEU4QmkrST0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiZTRzUWJ6aTgtTWNBb2pDV3VmbjNmdyIsInN1YiI6IjY3ODQxYWI4LWYwZTEtNzA2My03NmJmLWExNWI0MTExODNjMCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfblJlZHJWRVlNIiwiY29nbml0bzp1c2VybmFtZSI6InlhbWFkYSIsIm5vbmNlIjoiMDk3VFJFSDFJWjVMUlo5RjVNU04iLCJvcmlnaW5fanRpIjoiMTg5MGUzMWMtODY1NS00YTQ4LTllYTMtM2MxYTE1NjBhYzFmIiwiYXVkIjoiNThtbGk1dWJqZWZyb2s0ajMxZ2xia2pvdm8iLCJldmVudF9pZCI6ImQwNDg4NWZjLWJiZmMtNDIzZC05MzU0LWQ2NDc1ODY1ODJhZiIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNzI3NTk0NDUwLCJuYW1lIjoieWFtYWRhIiwiZXhwIjoxNzI3NTk4MDUwLCJpYXQiOjE3Mjc1OTQ0NTAsImp0aSI6ImNjZGM5M2FjLTY3YWQtNDBmYi05MDFmLTU3YTQ0NjRhYmFkYyIsImVtYWlsIjoieWFtYWRhQGV4YW1wbGUuY29tIn0.i2445XrLKkyW1drQHQXOh598-UEB6SgpRZCgJsfu1hW-UPVWZqOGskEq6CC4VY533kDhpppqJGo-ZAfE1-CzLhzdYdmi5aov46pEJHNCAahjfjffgnNRBXtXCIROWEPh3YxEuncS4kMBKCz4oo0BiAE8T6OV591gGwGqOgRIRADcMrnn7WDiZK3xRwO5F6pViBMrdu6wPykHNXvdgZ7C_46TJozEc5CFaEiucYL1d14_3qxKvJ4NIygjH-oD_IwiiVhKquMbjms35gIauVOLZEpXgy4z_FCmQI5LEEoR5tdt90TpCM2bmJcxHn9s3nRfqvrxWIEnJ1arWdp_D4gLVg",
      "access_token": "eyJraWQiOiJNVmRpcUcxc2Y2UmhoRnViYmg3QnB1R3pQVE1rSDhXYjJcL2JHaGRzN1V4cz0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2Nzg0MWFiOC1mMGUxLTcwNjMtNzZiZi1hMTViNDExMTgzYzAiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfblJlZHJWRVlNIiwidmVyc2lvbiI6MiwiY2xpZW50X2lkIjoiNThtbGk1dWJqZWZyb2s0ajMxZ2xia2pvdm8iLCJvcmlnaW5fanRpIjoiMTg5MGUzMWMtODY1NS00YTQ4LTllYTMtM2MxYTE1NjBhYzFmIiwiZXZlbnRfaWQiOiJkMDQ4ODVmYy1iYmZjLTQyM2QtOTM1NC1kNjQ3NTg2NTgyYWYiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiYXV0aF90aW1lIjoxNzI3NTk0NDUwLCJleHAiOjE3Mjc1OTgwNTAsImlhdCI6MTcyNzU5NDQ1MCwianRpIjoiMzg3YjQ2NDQtMGNhZC00OGVmLThmNGUtZWRhNThmYTg0M2M3IiwidXNlcm5hbWUiOiJ5YW1hZGEifQ.FsguU26nOyaGFQGf_2Fbb8WBFAfAZ8l1PMpR6avVQ5pCO0fU35VIYCaVoild3s9BBaPRz2xjF06xHPzUHIlZZ9y1_7t7Bsb407eTFUaMCO-ojOT_VEYfOzfAWD5cT4BuRqMUM-oTIr1oOFo_plDhI4RgPDjUUZiYHwcqOv9Aji1Gqh5KIOFM9uWWpSPFWqhVCzfALNw4Cj-bbAojEdEIIxTuAvpoxacCldPMsyZf6M0JeCXzdqkfYUSsGkwYMMmPNI0oQC4ykqdSXdE55voK10WWE85w0bQJ5MRrOwjYRsUi4NR3CK4BIU9zWNFz-5CRmd6QtKJ_77vpR3I5Ht1PbQ",
      "refresh_token": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.XLLiIZEJwk-EnoGT_Z-8j7QIPuHNPZoyerMqX1xOm3-IoYO6HZFKNIi6Icu4Pfr57rxewUyN2e56t9QlQG4-FdQucCecP7CbK3uxYU9sTQgJBnqGKNKSKritKYFKkG7YmI5azOjZe7Vaofiq20vraormt3x9T2g5_YyeikVz8IB0AX5xwKehtRH7Z1AnhkbaQATkwSFzU6SqIlZtzJ0YqaECPRGj7QMLnLtsQbamS9OD4Fph2Y5O0n9fa3gSYxPWtCdjXblUr4cbW13oOWzWJUcwLNrN3FBcyr4VNo1J7rCsxVXBTKicumbcVMo5HdMOCDReE-f_pI31nfcThKAR2g.3BKnbw0sCSvJ2uf9.tEx1VEntxVMFn7oXP2f7TcXGdSJImIov5vzqfRUOdty-ghVLtNKwSjSOc4eKoljxmrx9Bol0ArwiZxFoCLjWSviiPUueZMcOLYYlSOQ7BjAbdg7rEMPW1-GmijpPPWj9Xv7Lrc0iL7gBRWYV-heENqcBvyj7OdECak89dxNpocOlh0Y_WkyEwgn7nS8WwI6VuTgY8vpW4rF-eemA1rFQ9evjFGXDL554hFshNh5MIy25cJW8LWA-jA267IqVKVE4q8_JW9fgAkjSV5RoZfCvdeinOj5wLUnplxpjYACbJi8RdT184F_LtoFIUSWiZ2QU8XE-Is1Cdzw6qmzZoqwc4q3hp6Rhjc9EoyX0MKD6OqLji5ThWmSoTDQaqA7mwi3pXn2C9nRK5D4eQAEc6dbxiGIRvrqHLcmgx_P-u6ASznMsmdEEKzVTx3Dd1cACpVPTrX7hFBu-1YgxEAtdd_DVQl2FQqprlhol7jUZYq_RwTW1fiXG4lTqAvUUPE1WJB7_9IvGUhTWJS75nus5jVJ8OBjFAbXvVEZjmfv9Lwruu6j1e5Rcm8BZ38i5d7beKFcIt7p5pt2Zp93gHvGzzrcbktxcdPYLbqO2ZIs95Oo9ewdGMS2SUzH_x38caNnMlp6gSbZjCP8WCJspkIx7fPc-flMKGFD-PMRnd_LtGZgcC7gyeKGp54UT3Ly0_UcbK7FLB9dV4imanaonoYUtAjuQngZ1nAu1n6yV3-Nv7GJGfHVHi7Ia3KMLL6tEricaHwchO50cqedrDFzstx_d1tRgCw775qEPE_s1YdEGN6I6jYkTxhGJu9K7aP_EEsVMg-TjSz8VYtgNfYopDF7iGX6rioarh8qSSuc4cpfmJWjq_7mSHC2v9SE82v41qtADXKaiUHX3KEKiSCgQlPIHH4uvjYY50i8L3W9HixWu5oSctVgMyLlTAScdQbIfDIUX9dFv_0EFUFd9yNymkTpjeDOjAsWCmwLZiQ45HIqXrMQzV8yu4f-umjv_hHoNV_aS5MQPUFQqPD7ABYBzOgyrWU_MQnzkYVPAqZ9fWqn5AKcsjtaDfyp77V1INJUizkbmrQYyoBMj-NVGnfRNDVZ6dl_hDrCwJCv515dU8-2_Xxdaa9LmJHHn36qsHApmM5WMZyxUQGc7thKKtOwUwg6oeP0oKNnB8PE5dAEumVdfN28Ge7ATytbPhP_nM6UOVIXOcTC2jUxSezq6eKgfabnwxZqu9vlr87UcH0Ium-WXwmIxWMnYnO5xaIKOuEa2Bz6rgnexGKFZyJGixVQt7qssHQGwQj8taZkB.wHzd2G0DKpn6p3Kwv1WbCQ",
      "expires_in": 3600,
      "token_type": "Bearer"
    }
    
  10. example-python-openid-connect-client には実装されていませんが、このあと、RP は OP にユーザ情報を要求することができます。
    GET https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/userInfo
    Authorization: Bearer eyJraWQiOiJNVmRpcUcxc2Y2UmhoRnViYmg3QnB1R3pQVE1rSDhXYjJcL2JHaGRzN1V4cz0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2Nzg0MWFiOC1mMGUxLTcwNjMtNzZiZi1hMTViNDExMTgzYzAiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfblJlZHJWRVlNIiwidmVyc2lvbiI6MiwiY2xpZW50X2lkIjoiNThtbGk1dWJqZWZyb2s0ajMxZ2xia2pvdm8iLCJvcmlnaW5fanRpIjoiMTg5MGUzMWMtODY1NS00YTQ4LTllYTMtM2MxYTE1NjBhYzFmIiwiZXZlbnRfaWQiOiJkMDQ4ODVmYy1iYmZjLTQyM2QtOTM1NC1kNjQ3NTg2NTgyYWYiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiYXV0aF90aW1lIjoxNzI3NTk0NDUwLCJleHAiOjE3Mjc1OTgwNTAsImlhdCI6MTcyNzU5NDQ1MCwianRpIjoiMzg3YjQ2NDQtMGNhZC00OGVmLThmNGUtZWRhNThmYTg0M2M3IiwidXNlcm5hbWUiOiJ5YW1hZGEifQ.FsguU26nOyaGFQGf_2Fbb8WBFAfAZ8l1PMpR6avVQ5pCO0fU35VIYCaVoild3s9BBaPRz2xjF06xHPzUHIlZZ9y1_7t7Bsb407eTFUaMCO-ojOT_VEYfOzfAWD5cT4BuRqMUM-oTIr1oOFo_plDhI4RgPDjUUZiYHwcqOv9Aji1Gqh5KIOFM9uWWpSPFWqhVCzfALNw4Cj-bbAojEdEIIxTuAvpoxacCldPMsyZf6M0JeCXzdqkfYUSsGkwYMMmPNI0oQC4ykqdSXdE55voK10WWE85w0bQJ5MRrOwjYRsUi4NR3CK4BIU9zWNFz-5CRmd6QtKJ_77vpR3I5Ht1PbQ
    
  11. OP は RP にユーザ情報を返却します。
    HTTP/1.1 200 OK
    {
      "sub":"67841ab8-f0e1-7063-76bf-a15b411183c0",
      "email_verified":"true",
      "name":"yamada",
      "email":"yamada@example.com",
      "username":"yamada"
    }
    
  12. RP は User にログイン後の画面を返します。
    HTTP/1.1 200 OK
    ※ログイン後画面
    

トークンをリフレッシュする

IDトークンやアクセストークンは有効期限(3600秒)などを迎える前にリフレッシュする必要があります。RP は OP にリフレッシュ要求を行います。

GET https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/token
  grant_type=refresh_token
  client_id=58mli5ubjefrok4j31glbkjovo
  client_secret=ajvj5nn2vl59kn2il73cis5dafgh8rc32dp1rnc9gto0jidennq
  refresh_token=eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.PcvU5dLZDOrAuQQ3F9Zk5mHCEOVwejf7WYpGFm3Ml1-nMCcsuu-elAtmO6M6ipfpdsrUp4FlCnTNX9niT0rujsTf5nVgCIYpJEHkd4U0DQViA0r3KJAOzzZx9Z2w47IpyoBrVctxIVi1RDwyY8YYyn7E0TY5e7qrbKpM3Kn63t8FmRo8Lg9AGj890BRaqepfCc5PPOEmiaIJx_VAMaH4JSReEGsDENb1iSLkTr5fIGfWPVx48oTxchjCr1eArHwEOLcxav1VhkyxUc77VdaansHLVJERqq4WE_Kd_UMTjo7Vb0QPVfwtfErhcm4qoDik9p8Nz9wQV7Vn7-p7H0KtxA.Yhrx4pEMDCp8AuOj.nFtv_2_AhYQ8I737bS2jTUVI2_E1ohNnZZjumxpsFiFlCaVik2jYHrVSmfIzVf0XKYSbnq0RdDsSkNs2wSG-Xa9mpTbwR058xQaY9EGAKMtYQp1zWzJwhsV0UD8oCnO4JqDDbh2D8dcx0WjH32szY3h8mL5sQ_39WBCmumnYzXTZta5Dno5LyOmhqVzZhVi7-EePG6OQLMuLG9wma8EjB-vRAKGkuxoaqdS8aY0mnaTJPncMKTZJq4D63psXG3cAfmgKRZz8IhxemLINI6epB7p830CBFyRFpzeV7Zz9GNxgMLIKfk_sQKLWGPhQ4C9XdW4K8rQF0E8EDDp3mKoXA3rZJ45pEDs8U7kMeXxhqsU-F7I3MU6RZEMbPHujKwM5KLT5ozS1miUQbwNDMamapjKMFdCcGJcTVamCiszQ-n5Sq32w9m_0UnsBkJtGRHmWCrmrXNTG96uA59yFAywwgmTe0KyIidgo5sWsUYwYErK1KHmIzkQBw6AuScnG2dqKWfjFEB5EEXn52BN-xjSHsUdLBNW_Pu-acBm-p6WD_Y8YM9ffG4_h34IsG1kWCZhm0MeaDWO9y1s1wp9_GYFUnkFU7h_7t02QtKYQ-aDxtMbfOMQ79Cpkt9i_xWBIOK2YkqAUfxXXRzthF5cJKDlFfT_HlEkDmY8sa6ejNyIWLIh9FbmVraco-vGxQsYgXq4wYgwCjKKN0zkEKA6Uy2ws8Pucq0QD6cTz1x_SG5GdDxpwlcHiHdNZC7BWGoG8VGgxMQ5oS0yT_lXFltIqTuCp97d3ZtORM8uw-ngAEbUNxBzAr_TGWam9FOSZnPTMBGunYx014Lpxkj1DesAqtYNh4bMSLYg1dQKi68hfxs7NASLnlmvCLbSeJShx9ZuqBikLEFCLQqT2w2HhxfxXgJEqOH5_6Bkg88enOJTBAsA1CP9KKLedZOw15mSxcif9uWlSIGATQexJLmi65i4UP6zmkqjnYIQL8S8eNwtxV2uP8YhWNiAe-B4jYnjKpWHovUxikdnJkhgTICBSQuT6l8k6WCJFzkMcT4l0bVKcalPV2TqM_mnbdjLAtxA9hLQUlrY8GOpLbl4B3AeWymY0aWCZhNd2q7RfalIps9oKgMbTCeWg5axse6o-JEPg6qncQaoCk_6YwxBAQZmYFJaWPze9skapoc8sq2K1UazpMQwek0z10E0UvSBzn1ESxOFhSaqU2siXNz5TcuZiRnT0YGFcawFwAmvkG0tf6mm7miMVVK9h_AKNt89t0E3IkiFqAq2fO0txhTiSg4H3LWevMNHHmbAStmzr.75xRWGA6hbWmi0gn0BPB9w

OP は RP にリフレッシュしたトークンを返却します。

HTTP/1.1 200 OK
{
  "id_token": "eyJraWQiOiIzUmFBWWZKRVwvY3NveXpmVDBlN1BtdTFmVjUzckxobVRsdXJiNEU4QmkrST0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiSGJicUhFSUhCSDloWDdQT0N0Z0Y4ZyIsInN1YiI6IjY3ODQxYWI4LWYwZTEtNzA2My03NmJmLWExNWI0MTExODNjMCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfblJlZHJWRVlNIiwiY29nbml0bzp1c2VybmFtZSI6InlhbWFkYSIsIm9yaWdpbl9qdGkiOiI0MDI0Mzk4NC03NmFmLTQ3ODUtOGYwMi05YjY1NTIwYmM0YTkiLCJhdWQiOiI1OG1saTV1YmplZnJvazRqMzFnbGJram92byIsImV2ZW50X2lkIjoiOTExNTEwOGItODAxYi00YTEzLTg2YjctNmI2NDYxNWMwNjJiIiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE3Mjc2MDA3OTMsIm5hbWUiOiJ5YW1hZGEiLCJleHAiOjE3Mjc2MDQ0NzYsImlhdCI6MTcyNzYwMDg3NiwianRpIjoiNzJhNjkyYzktYjdkYS00MTVjLWJlYTQtMDk5NWVmMTNiY2EzIiwiZW1haWwiOiJ5YW1hZGFAZXhhbXBsZS5jb20ifQ.STr6anfpTYOFEd1CfWlLnQWxSbWdVstagNr_S5gmy7qrXVuwpncP9RA5PpieW6fwrZh3IswE7SMvTTICWexb7VG-e67RDL21CerAk0SIO4cVMsKJDviYu6HP4Lkqik4H6RyIeHKKXrb11tUnWaEuR8qHLjkjidDYXMILEhEM_YM3p_dQJWnWHb33USYaRiDlJ3V9BXll66A7M0vh3im5WzDasZn1-32f8MYNc25BTcSxFx-uxOoB3KDRJA9tbbuBpwcZtXpJnOpoKuhUvzZtAoGheS9cMrZNRxW7NgRTAA4F_juvxtFhFU0ndu-Wlw4gjocQY5YLaDsW_lWvpRFZgw",
  "access_token": "eyJraWQiOiJNVmRpcUcxc2Y2UmhoRnViYmg3QnB1R3pQVE1rSDhXYjJcL2JHaGRzN1V4cz0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2Nzg0MWFiOC1mMGUxLTcwNjMtNzZiZi1hMTViNDExMTgzYzAiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfblJlZHJWRVlNIiwidmVyc2lvbiI6MiwiY2xpZW50X2lkIjoiNThtbGk1dWJqZWZyb2s0ajMxZ2xia2pvdm8iLCJvcmlnaW5fanRpIjoiNDAyNDM5ODQtNzZhZi00Nzg1LThmMDItOWI2NTUyMGJjNGE5IiwiZXZlbnRfaWQiOiI5MTE1MTA4Yi04MDFiLTRhMTMtODZiNy02YjY0NjE1YzA2MmIiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiYXV0aF90aW1lIjoxNzI3NjAwNzkzLCJleHAiOjE3Mjc2MDQ0NzYsImlhdCI6MTcyNzYwMDg3NiwianRpIjoiNDk3ODFkOWEtZWIwZi00MDY4LTg2ODktY2E3Nzk4YmFiMzJhIiwidXNlcm5hbWUiOiJ5YW1hZGEifQ.qNR1fErYug-KTiOzsSBQcLocw8rCU0VNxPfqcKO3yA9VYwwMscQN2UlSK88UzmRcUNr2Z9EXLWOnNkfEo85IZL9IRsvOfEfZFerj6iRVESxwa0gOP7zRIOD8YueViamvS5gFC5QQPF_-KCPhG_Oph98qBMRzHCHcn5UnJvrGOsIH1CiQ8IU2wNlgvsgrgFCJQTGFePx3pSyx20p4jxHUd6h4y2ywed39T0FYVVyh_VA-cFD_xIKUa58xEwcokminESBEloLET3QN52vvGbyZ2xIBTtt9wBK_HaaCjqj4tJRDR26Ea0JK-3N1XZDvLMYc8hJz4j_xE8GH2xyuGA0n9g",
  "expires_in": 3600,
  "token_type": "Bearer"
}

トークンを失効させる

不要となったアクセストークンやリフレッシュトークンは失効(revoke)させます。

POST https://example123.auth.ap-northeast-1.amazoncognito.com/oauth2/revoke
Authorization: Basic {BASE64(client_id ":" client_secret)}
Content-Type: application/x-www-form-urlencoded

  client_id=58mli5ubjefrok4j31glbkjovo
  token=eyJraWQiOiJNV...

ログアウトする

  1. User は RP にログアウトを要求します。
    GET https://www.example.com/logout
    
  2. RP は User に end_session_endpoint へのリダイレクトを指示します。
    HTTP/1.1 302 Found
    Location: https://example123.auth.ap-northeast-1.amazoncognito.com/logout
      ?client_id=58mli5ubjefrok4j31glbkjovo
      &post_logout_redirect_uri=https://www.example.com
    

    AWS Cognito の場合は post_logout_redirect_uri ではなく logout_uri を送るようです。

    HTTP/1.1 302 Found
    Location: https://example123.auth.ap-northeast-1.amazoncognito.com/logout
      ?client_id=58mli5ubjefrok4j31glbkjovo
      &logout_uri=https://www.example.com
    
  3. User は OP の end_session_endpoint にリダイレクトします。
    GET https://example123.auth.ap-northeast-1.amazoncognito.com/logout
      ?client_id=58mli5ubjefrok4j31glbkjovo
      &post_logout_redirect_uri=https://www.example.com
    
  4. OP はログアウトを行い、RP のリダイレクトURIへのリダイレクトを指示します。
    HTTP/1.1 302 Found
    Location: https://www.example.com
    
  5. User は RP のリダイレクトURI を開きます。
    GET https://www.example.com
    
  6. RP はログアウト後の画面を返します。
    HTTP/1.1 200 OK
    ※ログアウト後画面
    

IDトークンの形式

IDトークンは下記の様なものです。

eyJraWQiOiIzUmFBWWZKRVwvY3NveXpmVDBlN1BtdTFmVjUzckxobVRsdXJiNEU4QmkrST0iLCJhbGciOiJSUzI1N
iJ9.eyJhdF9oYXNoIjoiZTRzUWJ6aTgtTWNBb2pDV3VmbjNmdyIsInN1YiI6IjY3ODQxYWI4LWYwZTEtNzA2My03N
mJmLWExNWI0MTExODNjMCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZH
AuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfblJlZHJWRVlNIiwiY29nbml0bzp
1c2VybmFtZSI6InlhbWFkYSIsIm5vbmNlIjoiMDk3VFJFSDFJWjVMUlo5RjVNU04iLCJvcmlnaW5fanRpIjoiMTg5
MGUzMWMtODY1NS00YTQ4LTllYTMtM2MxYTE1NjBhYzFmIiwiYXVkIjoiNThtbGk1dWJqZWZyb2s0ajMxZ2xia2pvd
m8iLCJldmVudF9pZCI6ImQwNDg4NWZjLWJiZmMtNDIzZC05MzU0LWQ2NDc1ODY1ODJhZiIsInRva2VuX3VzZSI6Im
lkIiwiYXV0aF90aW1lIjoxNzI3NTk0NDUwLCJuYW1lIjoieWFtYWRhIiwiZXhwIjoxNzI3NTk4MDUwLCJpYXQiOjE
3Mjc1OTQ0NTAsImp0aSI6ImNjZGM5M2FjLTY3YWQtNDBmYi05MDFmLTU3YTQ0NjRhYmFkYyIsImVtYWlsIjoieWFt
YWRhQGV4YW1wbGUuY29tIn0.i2445XrLKkyW1drQHQXOh598-UEB6SgpRZCgJsfu1hW-UPVWZqOGskEq6CC4VY533
kDhpppqJGo-ZAfE1-CzLhzdYdmi5aov46pEJHNCAahjfjffgnNRBXtXCIROWEPh3YxEuncS4kMBKCz4oo0BiAE8T6
OV591gGwGqOgRIRADcMrnn7WDiZK3xRwO5F6pViBMrdu6wPykHNXvdgZ7C_46TJozEc5CFaEiucYL1d14_3qxKvJ4
NIygjH-oD_IwiiVhKquMbjms35gIauVOLZEpXgy4z_FCmQI5LEEoR5tdt90TpCM2bmJcxHn9s3nRfqvrxWIEnJ1ar
Wdp_D4gLVg

途中にピリオド(.)が2カ所あり、ピリオドをデリミタとして3つのパートに分解できます。一つ目がヘッダー、二つ目はペイロード、三つ目は署名です。

--- ヘッダー ---
eyJraWQiOiIzUmFBWWZKRVwvY3NveXpmVDBlN1BtdTFmVjUzckxobVRsdXJiNEU4QmkrST0iLCJhbGciOiJSUzI1N
iJ9
--- ペイロード ---
    eyJhdF9oYXNoIjoiZTRzUWJ6aTgtTWNBb2pDV3VmbjNmdyIsInN1YiI6IjY3ODQxYWI4LWYwZTEtNzA2My03N
mJmLWExNWI0MTExODNjMCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZH
AuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfblJlZHJWRVlNIiwiY29nbml0bzp
1c2VybmFtZSI6InlhbWFkYSIsIm5vbmNlIjoiMDk3VFJFSDFJWjVMUlo5RjVNU04iLCJvcmlnaW5fanRpIjoiMTg5
MGUzMWMtODY1NS00YTQ4LTllYTMtM2MxYTE1NjBhYzFmIiwiYXVkIjoiNThtbGk1dWJqZWZyb2s0ajMxZ2xia2pvd
m8iLCJldmVudF9pZCI6ImQwNDg4NWZjLWJiZmMtNDIzZC05MzU0LWQ2NDc1ODY1ODJhZiIsInRva2VuX3VzZSI6Im
lkIiwiYXV0aF90aW1lIjoxNzI3NTk0NDUwLCJuYW1lIjoieWFtYWRhIiwiZXhwIjoxNzI3NTk4MDUwLCJpYXQiOjE
3Mjc1OTQ0NTAsImp0aSI6ImNjZGM5M2FjLTY3YWQtNDBmYi05MDFmLTU3YTQ0NjRhYmFkYyIsImVtYWlsIjoieWFt
YWRhQGV4YW1wbGUuY29tIn0
--- 署名 ---
                        i2445XrLKkyW1drQHQXOh598-UEB6SgpRZCgJsfu1hW-UPVWZqOGskEq6CC4VY533
kDhpppqJGo-ZAfE1-CzLhzdYdmi5aov46pEJHNCAahjfjffgnNRBXtXCIROWEPh3YxEuncS4kMBKCz4oo0BiAE8T6
OV591gGwGqOgRIRADcMrnn7WDiZK3xRwO5F6pViBMrdu6wPykHNXvdgZ7C_46TJozEc5CFaEiucYL1d14_3qxKvJ4
NIygjH-oD_IwiiVhKquMbjms35gIauVOLZEpXgy4z_FCmQI5LEEoR5tdt90TpCM2bmJcxHn9s3nRfqvrxWIEnJ1ar
Wdp_D4gLVg

ヘッダを BASE64 デコード(末尾に=が不足していれば追加)すると下記になります。kid はキーID、alg は署名アルゴリズムを意味します。

{
  "kid":"3RaAYfJE\/csoyzfT0e7Pmu1fV53rLhmTlurb4E8Bi+I=",
  "alg":"RS256"
}

ペイロードを BASE64 デコード(末尾に=が不足していれば追加)すると下記になります。

{
  "at_hash": "e4sQbzi8-McAojCWufn3fw",
  "sub": "67841ab8-f0e1-7063-76bf-a15b411183c0",
  "email_verified": true,
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_nRedrVEYM",
  "cognito:username": "yamada",
  "nonce": "097TREH1IZ5LRZ9F5MSN",
  "origin_jti": "1890e31c-8655-4a48-9ea3-3c1a1560ac1f",
  "aud": "58mli5ubjefrok4j31glbkjovo",
  "event_id": "d04885fc-bbfc-423d-9354-d647586582af",
  "token_use": "id",
  "auth_time": 1727594450,
  "name": "yamada",
  "exp": 1727598050,
  "iat": 1727594450,
  "jti": "ccdc93ac-67ad-40fb-901f-57a4464abadc",
  "email": "yamada@example.com"
}

署名情報は BASE64 デコードしてもバイナリデータとなります。この署名情報を用いてヘッダやペイロードが改竄されていないかを検証します。署名アルゴリズム(alg)の RS256 は RSASSA-PKCS1-v1_5 using SHA-256 を意味し、公開鍵暗号を意味します。検証には OP の公開鍵が必要ですが、JWK Set URI(jwks_uri) から取得することができます。

curl https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_nRedrVEYM/.well-known/jwks.json
{
  "keys": [
    {
      "alg": "RS256",
      "e": "AQAB",
      "kid": "3RaAYfJE/csoyzfT0e7Pmu1fV53rLhmTlurb4E8Bi+I=",
      "kty": "RSA",
      "n": "smcMiQ_J8uVhRCdR5-HYXvvWMJ_9gniAHcJ8RAcPaIEeErKxOLeQUZ8nN0oTerUZXB_DjwGnkrg1Atk7ZT6k4wFMUh9hnbcCgqtvBULS2kH6UiFlDNKEkcjuo1MHA4C-Hp7ixKbvfiOQZoV4GI05Pwjfy7Uq2AzjZFN7s8pu7YRb4TuhdzNdUVOM7woJW_na52KUPMaQpQoZdaedEJjwVw9ggVaUrGrm84HHN0KlviFHOWHzBfGuJvbDal19CNMQxSm9z6mIhtfM_0sea7hG-D-djyRSfERjHataBSo41FYLiP1C3AthRJH_ENLCZBmh-t6iYSUT5Eha55Wx4BDyEQ",
      "use": "sig"
    },
    {
      "alg": "RS256",
      "e": "AQAB",
      "kid": "MVdiqG1sf6RhhFubbh7BpuGzPTMkH8Wb2/bGhds7Uxs=",
      "kty": "RSA",
      "n": "7TKROn_BbfESd_6KDC-m9ihou-PmFbHLboS6Og1Ksix3dMU1CDSdXeAqFYEtHo0vNiwmBkmLKW2Z--9hVM2voosjA8m62n7ljynPW6pHvbwoBPzo0axcj6zv5El84A7lVpnJ8gl3SjATI9E_zZnsvXB3kRJudEWcCgmAclnawN5CXtVamj9LHLomyhsGrF_LqvibYfMI_DjtxYy-LQH7rcuLX8VI4CJfyq36R05kimEDT9B6nHFXQBylCQM_0Tkk8yuH36Ns9oxlq1N49ELFR0yWJjJaAvRM2CaVJO7W747L-TnOnv_uMEiPyIh5x5nKzro5izA95hDsx_NVk7KT7w",
      "use": "sig"
    }
  ]
}

サンプルコード

Python で AWS Cognito を IP(IdP) として動作する RP(Client) のサンプルコードを下記に示します。テストのため www.example.com の FQDN を使用します。

必要なパッケージをインストールします。

# -- RHEL9/AlmaLinux 9/Rocky Linux 9
$ sudo dnf -y install python3 python3-pip openssl
$ sudo pip3 install flask requests pyjwkest

# -- Ubuntu 24.04
$ sudo apt install -y python3-venv
$ sudo python3 -m venv /opt/oidc
$ source /opt/oidc/bin/activate
$ sudo pip3 install flask requests pyjwkest

HTTPS 通信を行う必要があるため、SSLの鍵と証明書を作成します。server's hostname には www.example.com を指定してください。他は空でも構いません。

$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
   :
Common Name (eg, your name or your server's hostname) []:www.example.com
   :
$ cat server.csr | openssl x509 -req -signkey server.key > server.crt

ブラウザを実行する Windows 端末に、管理者モードで \Windows\System32\drivers\etc\hosts を開き、ブラウザから www.example.com にアクセスする際のアドレスを www.example.com に書き込みます。下記に例を示しますが IPアドレスは適切に変更してください。

192.168.1.100 www.example.com

プログラムの config 情報を環境に合わせて設定します。最低限 XXXXXXXXXX の箇所の修正は必要です。

$ vi ./openid-connect-sample.py

準備ができたら下記を実行し、ブラウザから https://www.example.com にアクセスします。

$ python3 -B ./openid-connect-sample.py