Amazon Web Services ブログ

デジタル認証アプリサービス API を AWS と連携させてみよう

はじめに

こんにちは、パブリックセクターのお客様向けにプロトタイピングの支援をしている、SA の鈴木です。
早速ですが、みなさま、デジタル庁が提供している、デジタル認証アプリはご存知でしょうか?「デジタル認証アプリ」は、マイナンバーカードを使った本人確認を、安全に・簡単にするためのアプリです。

令和6年(2024年)4月時点で、マイナンバーカードの保有率は70%を越えており、マイナンバーカードの利用シーンが広がっています。「デジタル認証アプリ」は、マイナンバーカードを使った認証や署名を、安全に・簡単にするための、デジタル庁が提供するアプリです。
行政機関や民間事業者は、デジタル庁が提供するデジタル認証アプリと連携する API(デジタル認証アプリサービス API)を活用することで、マイナンバーカードを使った本人確認・認証や電子申請書類への署名機能を簡単に組み込むことができます。
引用元:デジタル認証アプリ – https://services.digital.go.jp/auth-and-sign/business/

デジタル認証アプリと連携する、デジタル認証アプリサービス API を利用すると、EC サイト・ネットバンキングの利用開始時に発生する本人確認や、イベント等での酒類購入時の年齢確認、地域に住んでいる方の住所確認、といったことをマイナンバーカードを用いて行うことができます。
これまでこのような本人確認は、申請書類と共に免許証や健康保険証のコピーを同封し、それら書類を申請先で確認、問題なければ申請が受理される、といった流れで進み、申請が受理されるまで、数日から1週間近くかかっていました。これは郵送による長いリードタイム、手作業による確認作業、とコストが高いものでした。しかし、デジタル認証アプリを利用することで、オンラインで本人確認を済ませることができ、より短時間での申請や、申請確認作業の効率化が可能になります。

本記事のゴール

本記事では、サービスサイトが示す、次の「認証 API 利用の流れ」を、Cognito と連携し、実現したいと思います。
本人確認が必要なサービスを企画・開発されている方々向けに、デジタル認証アプリサービス API と、AWS の Customer Identity and Access Management (CIAM) のサービスである Amazon Cognito の連携の一例としてぜひご確認いただければと思います。

認証 API 利用の流れ(引用元:【民間事業者向け情報】マイナンバーカードで本人の確認を簡単に – https://services.digital.go.jp/auth-and-sign/business/

Cognito は ソーシャルなIdentity Provider (IdP) や、Open ID Connect (OIDC) 、Security Assertion Markup Language (SAML) といったサードパーティの IdP を経由した認証が可能です。そのため、Cognito を利用し、OIDC に対応しているデジタル認証アプリサービス API とも連携することが理論上可能です。しかし、デジタル認証アプリサービス API で求められている実装のガイドラインを満たすには、private key jwt 認証を利用する必要があります。2025年2月現在、Cognito は、private key jwt 認証に非対応です。そのため、今回は client secret の代わりに、private key jwt 認証を Cognito で利用できるようにしたサンプル実装である、cognito-external-idp-proxy をカスタマイズして利用します。本サンプルは、ブログも公開されていますので、まずこちらをご一読いただくことをお勧めします。
目を通していただいた、という前提の上で、先に進めたいと思います。次に示す構成図が cognito-external-idp-proxy を利用した場合の全体構成となります。

また、この構成のシーケンス図は以下のようになります。

※上記は、デジタル庁 開発者サイトのシーケンス図:認証cognito-external-idp-proxy のドキュメントを元に作成しました。

デジタル認証アプリサービス API の利用にあたって

デジタル認証アプリサービス API は、利用にあたり事前に申請が必要です。詳しくは、デジタル庁が公開しているサービスサイトをご一読いただき、申請してください。申請の際には、申請する企業の情報などが必要になり、特に、システム面で事前に注意すべき項目としては、次に示すものがあります。

  • private key jwt 認証用の秘密鍵と公開鍵のペア
    • cognito-external-idp-proxy の README に作成手順が記載されています。
    • ここで作成した公開鍵をデジタル認証アプリサービス API の利用申請時に提出します。ペアとなる秘密鍵は大事に保管してください。
  • コールバック URL
    • cognito-external-idp-proxy をデプロイ後に提供される、コールバック関数と接続されている API Gateway のエンドポイントを申請時に提出することになります。
    • カスタムドメインを利用しない場合、ドメイン名はデプロイ後に決定されます。先んじてデプロイを進めておきましょう。
  • テストカード/テストカード代替機能の準備
    • J-LIS に別途申請する必要があります。貸与または購入から選択できます。
    • 今回はテストカードを J-LIS から貸与していただき、検証を行いましたが、テストカード代替機能を用いた検証も可能になったようです。テストカードの管理が煩雑な場合や急ぎ検証したい場合は、この機能の利用も検討ください。

デジタル認証アプリサービス API の実装ガイドラインとサンプルの比較

デジタル認証アプリサービス API を利用するために必要な実装は、実装のガイドライン(本記事では、2025年3月27日時点を参照) にまとめられています。これを整理し、cognito-external-idp-proxy の実装状況と比較すると、次のような表にまとめることができます。(横長になってしまい、読みづらいですがご容赦ください)

要件 要件詳細 項目分類 項目とその概要 サンプルで実装済みかどうか 本記事で追加・変更するか 実装箇所または、追加実装方針の概要
リダイレクト URI の設定 以下の説明を確認のうえ、デジタル認証アプリサービスへの申込時に、各 RP アプリへのリダイレクト URI を指定してください。
RP がネイティブアプリの場合
iOSは Universal Link、Androidは App Link を指定してください。
悪意のある他アプリによる乗っ取り攻撃の危険があるため、Custom URL Schema は指定しないでください。
RP が WEB アプリの場合
リダイレクト URI に WEB アプリの URI を指定することで、WEB アプリ上で認可レスポンスを取得できます。
RP がブラウザベースアプリの場合
トークンを安全に取り扱うため、BFF アーキテクチャ(Backend For Frontend)を採用してください。
リダイレクト URI に BFF の URI を指定してください。
リダイレクト URI の設定 RP が WEB アプリの場合
リダイレクト URI に WEB アプリの URI を指定することで、WEB アプリ上で認可レスポンスを取得できます。
実装済み いいえ Cognito + cognito-external-idp-proxy の部分を RP として扱うことで、RP が WEB アプリの場合と同義として捉えています。
ID トークンの検証 認可コードフローのトークンレスポンスには、ID トークンが含まれています。
※署名プロセスにおいてクライアントクレデンシャルズフローが扱われる過程がありますが、クライアントクレデンシャルズフローにおいては ID トークンは含まれません。
RP アプリは、利用者認証情報を含む改ざん検知用の署名付きトークンである ID トークンについて、トークンレスポンス取得後に必ず正当性を検証してください。
デジタル認証アプリサービスの JWT の署名アルゴリズムは ES256 です。
JWT はピリオド(”.”)区切りのヘッダ部、ペイロード部、シグネチャー部から構成され、各部位は Base64URL エンコードされています。
検証準備 取得した ID トークンを Base64URL デコードし、ヘッダ部、ペイロード部、シグネチャー部を(”.”)で分割します。
デジタル認証アプリサービスの JWK Set 公開エンドポイントを用いて、デジタル認証アプリサービス内の一連の公開鍵情報を取得します。公開鍵は定期的に更新されるため、ID トークン検証の度に取得してください。
実装済み いいえ Cognito で検証しています。
該当部分の引用です。

ユーザープールは、IdP 設定の発行者 URLs から IdP jwks_uri エンドポイントへのパスを決定し、JSON ウェブキーセット (JWKS) エンドポイントからトークン署名キーをリクエストします。IdP は JWKS エンドポイントから署名キーを返します。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html

JWT ヘッダ部の検証 ID トークンヘッダ部の kid が検証準備で取得した公開鍵の kid と一致することを検証してください。
署名のアルゴリズム である alg と、検証準備で取得した公開鍵のアルゴリズムがともに「ES256」になっていることを検証してください。
未実装 はい cognito-external-idp-proxy の token Lambda 関数で実装します。
PyJWT で提供されている関数を用いて、ヘッダ部の検証を行います。
署名の検証 RP アプリでご使用の開発言語や、使用されるライブラリ等によって異なります。
検証の概要を以下に示します。
Base64URL エンコード状態のヘッダ部 + “.” + ペイロード部をデータ部として保持 ・・・変数①
Base64URL デコードしたシグネチャー部を保持 ・・・変数②
検証準備で取得した公開鍵を保持 ・・・変数③
アルゴリズムを ES256 として保持 ・・・変数④
変数①~④を使用し、開発言語または使用されるライブラリで署名を検証
検証結果の正常・異常を判断
実装済み いいえ Cognito で検証しています。
該当部分の引用です。

ID トークンの署名を、プロバイダーのメタデータに基づいて想定される署名と比較します。https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html

ペイロード部の検証 ID トークンの発行者を示す Issuer Identifierクレーム(iss)が OpenID Provider メタデータの issuer の値と一致することを検証してください。 実装済み いいえ Cognito で検証しています。
該当部分の引用です。

iss クレームを IdP に設定された OIDC 発行者と比較します。https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html

ID トークンの audience クレーム(aud)に、RP アプリのクライアント ID が含まれていることを検証してください。 実装済み いいえ Cognito で検証しています。
該当部分の引用です。

aud クレームが IdP で設定されているクライアント ID と一致するか、または aud クレームに複数の値がある場合は設定されたクライアント ID が含まれているかを比較します。https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html

ID トークンの有効期限を示す expiration time クレーム(exp)が、検証時の時間より未来であることを検証してください。 実装済み いいえ Cognito で検証しています。
該当部分の引用です。

exp クレームのタイムスタンプが現在の時刻より前でないことをチェックします。https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html

ID トークンの発行日時を示す Issued at クレーム(iat)が、 検証時の UNIX タイムスタンプ値 - 所要時間(秒) の値以上であることを検証してください。
所要時間は、RP アプリで決める必要があります。
nonce 保存から RP アプリが ID トークン受け取るまでの時間が目安です。
指定時間を経過していた場合は必要に応じて、RP アプリ側にて再認証を実施するなど、追加処理の実装を検討してください。
未実装 はい cognito-external-idp-proxy の token Lambda 関数で実装します。
PyJWT で提供されている decode 関数で iat クレームを検証しているため、これを利用します。
ID トークンの at_hash クレームについて、以下のとおり検証してください。
アクセストークンを、ID トークンヘッダ部の algorithm クレーム(alg)と同じハッシュアルゴリズムを用いてハッシュ化し、ハッシュ化した結果の左半分のビット群を Base64URL にてエンコードしたうえで、ID トークンの at_hash と値が一致することを確認してください。
未実装 はい cognito-external-idp-proxy の token Lambda 関数で実装します。
PyJWT で提供されている decode 関数から返される ID トークンから at_hash を取得し、アクセストークンから生成した at_hash と比較することで、検証します。
検証失敗時 タイムスタンプの検証で失敗する場合は、有効期限切れや認証時刻が過ぎているため、認証処理を中断し、トークンリフレッシュにて ID トークンの再発行を行ってください。 未実装 いいえ 本記事では実装しませんが、exp, iat, nbf クレームの検証後、有効期限切れであれば、トークンリフレッシュを行うように実装します。
その他の検証で失敗する場合は、ID トークンが改ざんされている可能性があるため、認証処理を中断し、エラー処理等を行ってください。 未実装 はい exp, iat, nbf クレームのタイムスタンプに関する例外以外は、認証失敗でレスポンスを返すよう実装します。
logout トークンの検証 デジタル認証アプリサービスから RP アプリへ back-channel logout リクエストを送信する際、logout トークンを POST パラメータへ含めます。
RP アプリは、logout トークンについて正当性の検証を含めて、利用者とのセッション終了処理を実施してください。
logout トークンの検証 同左 未実装 いいえ 設定が任意のため、今回は省略しますが、本来は RP 側でエンドポイントを実装し、そのエンドポイントのロジックで logout トークンについての正当性の検証とセッション終了処理を実施する必要があります。
ID トークン署名検証用公開鍵のローテーションを考慮した準備 署名を検証するための公開鍵は、暗号漏えいが疑われる場合を想定し、有効期限内であってもローテーションする可能性があります。RP アプリに保持する公開鍵について、キー変更を自動的に処理するように RP アプリを作成してください。 公開鍵のローテーション JWK Set 公開エンドポイント
https://auth-and-sign.go.jp/api/realms/main/protocol/openid-connect/certs
OpenID Provider Configuration エンドポイント
https://auth-and-sign.go.jp/api/realms/main/.well-known/openid-configuration
未実装 はい ID トークンの検証を token Lambda 関数で実装する必要があるため、公開鍵のローテーションについても考慮する必要があります。
PyJWT ライブラリを用いて、リクエストごとに公開鍵を取得します。
※本記事では、公開鍵のキャッシュは未実装ですが、実際に利用される場合は、公開鍵をキャッシュする仕組みの実装をお勧めいたします。また、Cognito における検証は以下のようになっています。
該当部分の引用です。

ユーザープールは、IdP 設定の発行者 URLs から IdP jwks_uri エンドポイントへのパスを決定し、JSON ウェブキーセット (JWKS) エンドポイントからトークン署名キーをリクエストします。
IdP は JWKS エンドポイントから署名キーを返します。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html

Web アプリケーションの脆弱性悪用への対策 悪意のあるリクエストを送信させることを目的とするクロスサイトリクエストフォージェリ(CSRF)への対策をしてください。
検証結果が一致しない場合は CSRF による攻撃の可能性があるため、認証処理を中断し、エラー処理等を行ってください。
RP アプリの対応方法(CSRF) RP アプリは、state というランダムな値を生成して、認可リクエストに設定し、利用者のセッションにもその値を保存してください。
state に設定する値については、別紙の API リファレンスをご参照ください。
実装済み いいえ state は以下のコードで保持するよう実装されています。
https://github.com/aws-samples/cognito-external-idp-proxy/blob/d85fc2bed25627170fe5989b3b8d344d9b1ab860/lambda/authorize/authorize_flow.py#L82
必ず、認可リクエストごとに新しい state 値を生成してください。 実装済み いいえ Cognito がサードパーティー ID プロバイダーへ認可リクエストを送る際に、state を生成しています。
デジタル認証アプリサーバは、認可コードを含むリダイレクトをレスポンスする際に、state の値も RP へレスポンスします。
RP アプリが生成した state と、デジタル認証アプリサーバからリダイレクトで送られた state が一致することを検証してください。
実装済み いいえ https://github.com/aws-samples/cognito-external-idp-proxy/blob/d85fc2bed25627170fe5989b3b8d344d9b1ab860/lambda/callback/callback_flow.py#L65
上記処理でチェックしています。
リプレイ攻撃への対策 利用者のログインセッション確立の際に、攻撃者が ID トークンを置き換えて不正ログインするリプレイ攻撃への対策をしてください。
検証結果が一致しない場合はリプレイ攻撃の可能性があるため、認証処理を中断し、エラー処理等を行ってください。
RP アプリの対応方法(nonce) RP アプリは、nonce というランダムな値を生成して、認可リクエストに設定し、利用者のセッションにもその値を保存してください。
nonce に設定する値については、別紙の API リファレンスをご参照ください。
必ず、認可リクエストごとに新しい nonce 値を生成してください。Client セッションと ID Token を紐づける文字列。リプレイアタック対策に用いられる。
nonce 値は、case-sensitive (大文字と小文字を区別する) である点に留意すること。(OpenID Connect)
リクエスト毎にナンス値を生成し設定する。
code_verifier と同様に、256 ビットのエントロピーを推奨。
未実装 はい cognito-external-idp-proxy の authorize Lambda 関数で、nonce を生成し、DynamoDB で保持するよう実装する必要があります。
合わせて、nonce のレコードに TTL を設定し、nonce が自動的に削除されるようにします。
デジタル認証アプリサーバは、アクセストークン発行レスポンスを返却する際、RP アプリが生成した nonce を含む ID トークンを返却します。
RP アプリは、返却された nonce と自身が生成した nonce が一致することを検証してください。
未実装 はい Cognito は、認可リクエストに nonce パラメータを渡すと ID トークンの nonce パラメータを検証できます。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/authorization-endpoint.html
Lambda で認可リクエストを送る際に、nonce パラメータを設定することで検証します。
認可コード横取り攻撃への対策 認可レスポンス受信時のリダイレクト処理における悪意のあるアプリへのなりすまし対策をしてください。 RP アプリの対応方法(PKCE) RP アプリは、認可リクエストに code_challenge、code_challenge_method パラメータを設定してください。
必ず、認可リクエストごとに新しい code_challenge 値を生成してください。
code_challenge の生成方法となる code_challenge_method の値については、必ず「S256」を指定してください。
code_challenge および code_challenge_method に設定する値の詳細については、 別紙の API リファレンスをご参照ください。
デジタル認証アプリサービスでは、渡された code_challenge、code_challenge_method のほかに、code_verifier を用いて検証を行い、正規のリクエストに対してのみアクセストークンを発行します。
実装済み いいえ https://github.com/aws-samples/cognito-external-idp-proxy/blob/d85fc2bed25627170fe5989b3b8d344d9b1ab860/lambda/authorize/authorize_flow.py#L66上記から、トランザクションごとに code_verifier を発行していることがわかります。method も S256 になっています。
アクセストークン置き換え攻撃への対策 攻撃者がアクセストークンを置き換えて利用者になりすます攻撃の対策をしてください。
ID トークンのペイロードに含まれる at_hash を用いて、アクセストークンの置き換え対策のための検証を行ってください。
詳細な検証方法については以下をご参照ください。
参考:ペイロード部の検証
アクセストークン置き換え攻撃への対策 同左 未実装 はい cognito-external-idp-proxy の token Lambda 関数で実装する必要があります。
ログの保持 デジタル認証アプリサービスでは、API 実行ログの提供は行いません。
問題が発生した際の原因や影響範囲の調査を行えるよう、API 実行ログは RP アプリ内で一定期間保存してください。
内部ストレージからの漏洩対策のため、後述の保護対象のデータに記載されている情報は、ログの保存対象にしないでください。
ログの保持 同左 実装済み いいえ APIGateway と Lambda のログは取得されています。
ただし、既存設定は Amazon CloudWatch Logs のログの保持期間が5日間であるため、自社のガバナンスポリシーに合わせた修正が必要になる可能性があります。
プラットフォーム事業者に必要とされる証明書暗号対応 民間事業者が、デジタル認証アプリの署名 API を利用する場合、公的個人認証法に基づく大臣認定事業者(プラットフォーム事業者)と連携して電子署名の検証及び電子証明書の有効性確認を行う必要があります。
電子証明書の保持及び電子証明書の発行番号の取得・保持は、プラットフォーム事業者には認められていますが、プラットフォーム事業者に電子署名の検証等を委託する事業者(サービス提供事業者:SP 事業者)には認められていないため、署名APIの利用申込みを行う SP 事業者、連携するプラットフォーム事業者に以下の対応をお願い申し上げます。
民間事業者が署名 API を利用する場合、この対応は必須となります。
利用までの流れ SP 事業者は、事前準備契約の申込書に対象となるサービスと連携するプラットフォーム事業者を記入します。(自社がプラットフォーム事業者であり、連携する事業者の役割を兼ねる場合は自社名を記入してください。プラットフォーム事業者に電子署名の検証等を委託する場合は委託先のプラットフォーム事業者名を記入してください。)
※暗号化用公開鍵の登録はサービス単位で行います。テスト環境と本番環境は別の鍵をご用意ください。
テスト環境・本番環境の設定にあたり、デジタル庁から対象のプラットフォーム事業者に対して暗号化に用いる公開鍵の登録を依頼します。
プラットフォーム事業者はサービスごとに鍵を生成しサービス名と公開鍵をデジタル庁に登録します。
デジタル庁は SP 事業者に対象となるサービスにおける公開鍵登録完了を連絡します。
未実装 いいえ
署名用電子証明書と署名値の暗号化について ECDH-ES の暗号化方式で署名用電子証明書と署名値を暗号化してデジタル認証サーバからプラットフォーム事業者側へ返却します。以下にデジタル認証サーバでの暗号化処理の概要を示します。
署名用電子証明書と署名値を暗号化するための鍵ペア(秘密鍵、公開鍵)を生成します。
生成した秘密鍵と取得したプラットフォーム事業者公開鍵で共有鍵を生成します。
署名用電子証明書と署名値を Base64 エンコードします。
署名用電子証明書と署名値を共有鍵により暗号化し、JWE Compact Serialization 形式にシリアライズします。
生成した公開鍵、暗号化した署名用電子証明書および署名値を返却します。
未実装 いいえ
データの保護 クロスサイトスクリプティング(XSS)やクロスサイトリクエストフォージェリ(CSRF)を受けて、RP アプリのキャッシュや内部ストレージ等から直接アクセストークンや利用者のプライバシー情報が窃取された場合を想定する必要があります。
アクセストークン・利用者のプライバシー情報をRPアプリで安全に保持するよう暗号化を行い、セキュアなストレージへ保管し、利用する際に復号する等のデータ漏洩対策をしてください。
トークン関連 private_key_jwt 認証方式における秘密鍵 実装済み はい AWS Secrets Manager で保管しています。
アクセストークン 実装済み はい Cognito や Lambda のメモリで保持されます。
リフレッシュトークン 実装済み はい Cognito や Lambda のメモリで保持されます。
IDトークン 実装済み はい Cognito や Lambda のメモリで保持されます。
認可エンドポイント関連 認可コード 実装済み はい サンプルが作成する DynmaoDB で保管します。TTL をつけているため、有効期限が来ると、5分後に自動的に削除されます。(5分はサンプルで定義しているデフォルト値です。必要に応じて変更してください。)
署名トランザクション結果取得エンドポイント関連 署名用電子証明書情報 未実装 いいえ
UserInfo エンドポイント関連 利用者識別子 実装済み はい Cognito の属性マッピングで連携され、Cognito のユーザープールで保管されます。
氏名 実装済み はい Cognito の属性マッピングで連携され、Cognito のユーザープールで保管されます。
住所 実装済み はい Cognito の属性マッピングで連携され、Cognito のユーザープールで保管されます。
生年月日 実装済み はい Cognito の属性マッピングで連携され、Cognito のユーザープールで保管されます。
性別 実装済み はい Cognito の属性マッピングで連携され、Cognito のユーザープールで保管されます。

それでは、次から、各要件ごとに、どのようにカスタマイズすべきか、みていきましょう。

準備:利用するライブラリの変更

サンプルで利用している python-jwt ライブラリは、ヘッダ部の kid の検証や iat クレームの検証が実装されていないため、PyJWT に変更します。変更したことによって、private key jwt 認証の処理に関する部分を修正する必要があります。今回は PyJWT を利用しましたが、読者の方々が実装される際は、ご自身の環境に合わせてライブラリをお選びください。また、ライブラリを変更する場合は、Layer の更新手順に従い、 Layer を忘れずに更新してください。

ID トークンの検証(JWT ヘッダ部の検証)

ID トークンの検証は、 ID トークンを取得する処理が記述されている、token Lambda 関数を更新します。
PyJWT では、ID トークン署名検証用公開鍵を取得するための関数が用意されており、取得した ID トークンに含まれる kid の ID に最適な公開鍵を取得します。kid の ID に一致する公開鍵を取得する、ということは、JWT ヘッダ部の検証の要件である、「ID トークンヘッダ部の kid が検証準備で取得した公開鍵の kid と一致することを検証してください。」 を満たすことがわかります。実際のコードは、cognito-external-idp-proxy/lambda/token/token_flow.py186 行目の直後に、以下のように追記します。

print("+++ VERIFYING ID TOKEN +++")
id_token = json.loads(data)["id_token"] 

print("+++ GET JWKS +++")
jwks_client = jwt.PyJWKClient(config["jwks_uri"])
signing_key = jwks_client.get_signing_key_from_jwt(id_token)

ID トークンの検証(JWT ヘッダ部の検証とペイロード部の検証)

ここでは、ヘッダ部の検証の要件である、「署名のアルゴリズム である alg と、検証準備で取得した公開鍵のアルゴリズムがともに「ES256」になっていることを検証してください。」と、
ペイロード部の検証の要件である、「iat クレーム」「at_hash クレーム」の検証を実装します。
まず、iat クレームについてです。
PyJWT は、ID トークンのデコード関数で iat クレームの検証をデフォルトで行います。ただし、要件に「所要時間は、RP アプリで決める必要があります。」とあるように、所要時間をパラメータとして渡す必要があります。トークンが発行されてから検証を行うまでの所要時間を定義すれば良いので、今回は 60 秒程度とします。ここはそれぞれの環境で適した時間を設定ください。
実際のコードは、以下のようになります。JWT ヘッダ部の kid の検証で取得した公開鍵 (signing_key) も一緒に渡します。このコードでは、念の為、iss クレームと aud クレームも検証するように、必要なパラメータを渡しています。
cognito-external-idp-proxy/lambda/token/token_flow.pysigning_key = jwks_client.get_signing_key_from_jwt(id_token) の直後に以下を記述します。

try:
    decoded_id_token = jwt.decode(
        id_token, 
        signing_key, # kid の検証時に取得した公開鍵
        leeway=60, # 所要時間に 60 秒を設定
        issuer=config["idp_issuer_url"], # iss クレームの検証
        audience=config["client_id"], # aud クレームの検証
        algorithms=["ES256"])

続いて、at_hash クレームの検証を実装します。
at_hash クレームは、要件にある通り、「アクセストークンを、ID トークンヘッダ部の algorithm クレーム(alg)と同じハッシュアルゴリズムを用いてハッシュ化し、ハッシュ化した結果の左半分のビット群を Base64URL にてエンコードした値」です。
繰り返しになりますが、まず、(①)トークンエンドポイントから取得したデータから、アクセストークンを取得・検証し、(②)sha256 でハッシュ化します。続いて、(③)ハッシュ化した値の左半分のビット群を Base64URL でエンコードし、at_hash を生成します。最後に、(④)生成した at_hash とデコードした ID トークンに含まれる at_hash を比較し、同じ値であることを確認します。
cognito-external-idp-proxy/lambda/token/token_flow.py の 上記デコード処理の直後に以下を記述します。

    access_token = json.loads(data)["access_token"] # ①アクセストークンの取得
    jwt.decode(
                access_token,
                signing_key,
                algorithms=["ES256"]) # アクセストークンの検証
    hashed_access_token = hashlib.sha256(access_token.encode()).digest() # ②
    calculated_at_hash = base64.urlsafe_b64encode(hashed_access_token[:len(hashed_access_token)//2]).decode('utf-8').rstrip('=') # ③
    # ④
    if decoded_id_token['at_hash'] != calculated_at_hash:
        print("at_hash mismatch")
        return { "statusCode": 400 }

リプレイ攻撃への対策

要件では、リプレイ攻撃への対策として nonce の利用が求められています。
今回は、authorize Lambda 関数でリクエストごとに nonce を生成し、認可リクエストに設定し、DynamoDB の state テーブルと code テーブルで nonce を一定期間保持し、token Lambda 関数で検証します。
まず、authorize Lambda 関数で nonce を生成する処理の実装です。ここでは、Secrets Manager のクライアントを用いて、ランダムパスワードを生成し、その乱数を nonce のフォーマットに合うように変換します。
以下のコードを cognito-external-idp-proxy/lambda/authorize/authorize_flow.py77 行目あたりに追加します。

nonce_random = SM_CLIENT.get_random_password(
    PasswordLength=64,
    ExcludePunctuation=True,
    IncludeSpace=False
)
nonce_random = nonce_random["RandomPassword"]
nonce = hashlib.sha256(nonce_random.encode("utf-8")).digest()
nonce = base64.urlsafe_b64encode(nonce).decode('utf-8')
nonce = nonce.replace('=', '')

RP で生成した nonce を state テーブルで管理するため、state や code_verifier を格納する処理で合わせて state テーブルに格納するように、cognito-external-idp-proxy/lambda/authorize/authorize_flow.py82 行目のコードを書き換えます。

DYNAMODB_CLIENT.put_item(
    TableName = config["state_table"],
    Item = {
        "state": {
            "S": str(hashed_state)
        },
        "code_verifier": {
            "S": code_verifier
        },
        "ttl": {
            "N": str(state_ttl)
        },
         # Add
        "nonce": {
             "S": nonce
        }
    }
 )

そして、デジタル認証アプリサービス API の認可エンドポイントのパラメータとして設定します。
ここでは、cognito-external-idp-proxy の実装には存在しないパラメータである、acr_values も合わせて設定します。(参照:認可エンドポイントのリクエストパラメータ、https://developers.digital.go.jp/documents/auth-and-sign/authserver/
cognito-external-idp-proxy/lambda/authorize/authorize_flow.py100 行目あたりに以下のコードを追加します。

params["nonce"] = nonce
params["acr_values"] = "aal3 crl"

デジタル認証アプリサービス API からのレスポンスに、nonce は含まれていても、state は含まれていません。このままでは state テーブルから、RP が生成した nonce を取得できず、レスポンスに含まれる nonce を検証できません。そこで、callback Lambda 関数で、state テーブルの nonce を、code テーブルに格納し、token Lambda 関数で、code をキーに、nonce を取得するため、cognito-external-idp-proxy/lambda/callback/callback_flow.py81 行目の code テーブルへの追加処理を以下のように変更します。

DYNAMODB_CLIENT.put_item(
    TableName = config["auth_code_table"],
    Item = {
        "auth_code": {
            "S": config["auth_code"]
        },
        "code_verifier": {
            "S": code_verifier
        },
        # Add
        "nonce": {
            "S": state_result["Item"]["nonce"]["S"]
        },
        "ttl": {
            "N": str(code_ttl)
        }
    }
)

これで RP が生成した nonce を token Lambda 関数で取得する準備が整いました。
最後に、cognito-external-idp-proxy/lambda/token/token_flow.py122 行目の直後に、以下のコードを追加し、code テーブルから取得した nonce を変数に格納します。

nonce = code_result["Item"]["nonce"]["S"]

デコードした ID トークンの検証後( if decoded_id_token['at_hash'] != calculated_at_hash: での検証後)に、以下のコードを追加し、変数に格納した nonce の値( RP で生成したもの)と ID トークンに含まれる nonce の値(デジタル認証アプリサービスのレスポンス)が等しいかを検証します。

    if decoded_id_token["nonce"] != nonce:
        print("Nonce mismatch")
        return { "statusCode": 400 }
# トークンの検証時に例外が発生したら、認証失敗とするための例外処理
except Exception as e:
    return { "statusCode": 400 }

アクセストークン置き換え攻撃への対策

この要件は、ID トークンのペイロード検証時に、 at_hash クレームの検証を行なっているため、対策済みとなります。

SPA やバックエンドで必要な実装

これまでの実装で、デジタル認証アプリサービス API を通じて、認証を行うことができるようになりました。実際のサービスでは、この認証後に発行される Cognito のトークンを用いて、サービスの API に対する認可処理を行い、さまざまな処理を実行することになります。
今回のデモでは、Cognito のユーザ属性から生年月日を確認し、20歳以上かどうか判断するという一連の流れを、React と Amazon API Gateway と COGNITO_USER_POOLS タイプのオーソライザー(オーソライザー)を利用して実装したいと思います。React からは Cognito のアクセストークンを送信し、オーソライザーでアクセストークンに含まれるスコープ(custom/read:age)を検証し、API の利用可否を判断(認可)します。
このアクセストークンに含まれるスコープは、独自で Cognito に設定した、デモ用に作成した API のためのスコープです。具体な実装は、この後の「CDK の実装例」で示しています。

SPA の実装

今回は、Cognito のコンソールで紹介されているサンプル実装を利用します。利用するライブラリは、oidc-client-ts ライブラリと react-oidc-context ライブラリです。SPA における OIDC の利用で論点になるのは、トークンのハンドリング方法ですが、これは各サービスのポリシーに合わせ、BFF や、Custom Storage などの採用をご検討ください。
本実装はあくまでサンプルです。InMemoryWebStorage を利用することでページリロード時のユーザ体験が損なわれてしまう点や、環境変数やバックエンドとの接続設定や、API 呼び出しの処理など、皆様がご利用される際には、本番環境向けに、適切に実装してください。

実装例 – main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { AuthProvider } from "react-oidc-context";
import "./index.css";
import { InMemoryWebStorage, WebStorageStateStore } from "oidc-client-ts";

const cognitoAuthConfig = {
  authority: "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXXX",
  client_id: "xxxxxxxxxxxxxxxxxx",
  redirect_uri: "http://localhost:5173", // デモアプリのため、localhostで実装しています。
  response_type: "code",
  scope: "custom/read:age openid",
  automaticSilentRenew: true,
  silentRedirectUri: "http://localhost:5173", // デモアプリのため、localhostで実装しています。
  onSigninCallback: () => {
    window.history.replaceState({}, document.title, window.location.pathname);
  },
  userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() }),
};

const root = ReactDOM.createRoot(document.getElementById("root")!);

root.render(
  <React.StrictMode>
    <AuthProvider {...cognitoAuthConfig}>
      <App />
    </AuthProvider>
  </React.StrictMode>
);

実装例 – App.tsx

今回は、RFC6750 に従い、アクセストークンを指定し、オーソライザーでスコープによるアクセス制御を実現します。
参考:リソースサーバーを使用したスコープ、M2M、および API

import { useCallback, useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";
import AppIcon from "../public/App-Icon_White.svg";

function App() {
  const auth = useAuth();
  const [isLoading, setIsLoading] = useState(true);
  const [isOver20, setIsOver20] = useState(false);

  const signOutRedirect = () => {
    const clientId = "xxxxxxxxxxxxxxxxxxxxxxxx";
    const logoutUri = "http://localhost:5173";
    const cognitoDomain = "https://xxxxxxxxxx.auth.ap-northeast-1.amazoncognito.com";
    window.location.href = `${cognitoDomain}/logout?client_id=${clientId}&logout_uri=${encodeURIComponent(logoutUri)}`;
  };

  const ageCheck = useCallback(async() => {
    try {
      const res = await fetch("https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/api/age-check", {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${auth.user?.access_token}`, // アクセストークンを指定する
        },
      });
      if (res.ok) {
        const data = await res.json();
        setIsOver20(Boolean(data.isOver20));
        setIsLoading(false);
      }
    } catch(error) {
      console.error("Error was happend:", error);
    }
  }, [auth.user?.access_token])

  useEffect(() => {
    if (auth.isAuthenticated) {
      ageCheck();
    }
  }, [auth.isAuthenticated, ageCheck])

  if (auth.isLoading) {
    return <div>Loading...</div>;
  }

  if (auth.error) {
    return <div>Encountering error... {auth.error.message}</div>;
  }

  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <h1 className="text-2xl mb-20">デジタル認証アプリ デモ on AWS</h1>
      <>
      {auth.isAuthenticated ? (
        <>
          <pre> {auth.user?.profile.name} さんは、 {isLoading ? "読み込み中..." : isOver20 ? "20歳以上です。" : "20歳未満です。"} </pre>
          <button 
            className="mt-4 flex justify-center items-center bg-digital-agency-blue-default text-white px-4 py-2 rounded-md hover:bg-digital-agency-blue-hover active:bg-digital-agency-blue-active focus:outline-none focus:ring-2 focus:ring-[#0017C1] focus:ring-opacity-50"
            onClick={() => signOutRedirect()}>
            ログアウト
          </button>
        </>
      ) : (
        <>
          <div className="text-center">本人確認のため、デジタル庁が提供するデジタル認証アプリを利用します。</div>
          <button 
            className="mt-4 flex justify-center items-center bg-digital-agency-blue-default text-white px-4 py-2 rounded-md hover:bg-digital-agency-blue-hover active:bg-digital-agency-blue-active focus:outline-none focus:ring-2 focus:ring-[#0017C1] focus:ring-opacity-50"
            onClick={() => auth.signinRedirect()}>
            <img src={AppIcon} alt="App Icon" className="mr-4 w-6 h-6 fill-white" />
            マイナンバーカードでログイン
          </button>
        </>
      )}
      </>
      <footer className="absolute right-0 left-0 bottom-0 bg-gray-100 w-full">
          <div className="flex justify-center space-x-4">
            <pre>サービスについて</pre>
            <pre>利用規約</pre>
            <pre>ポリシー</pre>
          </div>
      </footer>
    </div>
  );
}

export default App

アプリのデザインについて

デジタル認証アプリを利用するアプリケーションには、デザインガイドラインも示されています。フロントエンドアプリケーションは、こちらに従うようにしてください。
https://developers.digital.go.jp/documents/auth-and-sign/design-guideline/

バックエンドの実装

前述の通り、API Gateway と オーソライザー、Lambda で API とその認可処理を実装します。今回実装したサンプルのバックエンドでは、トークンベースでアクセス制御、つまりトークン所有者による代理アクセスとして、バックエンドが保持するユーザー情報を特定、照合することで、データにアクセスしているユーザーが、本当にそのデータのユーザーであるかを検証しています。

CDK の実装例

バックエンド用のコンストラクトを次のように作成します。このコンストラクトを PjwtWithPkceStack から呼び出してください。
construct の実装例(ファイルは、cognito-external-idp-proxy/cdk/construct/backend.py のように追加します)は次のようになります。

from aws_cdk import (
    aws_apigateway as _apigw,
    aws_lambda as _lambda,
    aws_cognito as _cognito,
    aws_iam as _iam,
    Duration,
)
from constructs import Construct
from cdk_nag import NagSuppressions

class Backend(Construct):
    def __init__(self, scope: Construct, id: str, user_pool: _cognito.UserPool, oauth_scopes: list[str], **kwargs):
        super().__init__(scope, id)

        # Create API Gateway with CORS
        self.api = _apigw.RestApi(
            self,
            "BackendApi",
            rest_api_name="Backend API",
            description="API Gateway with COGNITO_USER_POOLS type authorizer and custom scopes",
            default_cors_preflight_options=_apigw.CorsOptions(
                allow_origins=["http://localhost:5173"],
                allow_methods=["GET", "OPTIONS"],
                allow_headers=["Content-Type", "X-Amz-Date", "Authorization", "X-Api-Key",
                             "X-Amz-Security-Token", "X-Amz-User-Agent"],
                max_age=Duration.days(1)
            )
        )

        # Create an authorizer of the type of COGNITO_USER_POOLS
        auth = _apigw.CognitoUserPoolsAuthorizer(
            self,
            "CognitoUserPoolsAuthorizer",
            cognito_user_pools=[user_pool]
        )

        # Create age_check Lambda function
        age_check_lambda = _lambda.Function(
            self,
            "AgeCheckFunction",
            runtime=_lambda.Runtime.PYTHON_3_10,
            handler="age_check.handler",
            code=_lambda.Code.from_asset("./lambda/age_check"),
            timeout=Duration.seconds(30),
            environment={
                "USER_POOL_ID": user_pool.user_pool_id
            }
        )
        age_check_lambda.add_to_role_policy(
            _iam.PolicyStatement(
                effect=_iam.Effect.ALLOW,
                actions=["cognito-idp:AdminGetUser"],
                resources=[user_pool.user_pool_arn]
            )
        )

        # Create API resources and methods
        api_resource = self.api.root.add_resource("api")
        age_check_resource = api_resource.add_resource("age-check")

        # Add method with authorization
        age_check_get_method = age_check_resource.add_method(
            "GET",
            _apigw.LambdaIntegration(age_check_lambda),
            authorizer=auth,
            authorization_type=_apigw.AuthorizationType.COGNITO,
            authorization_scopes=["custom/read:age"]  # Custom scope for age check
        )

        # Grant Lambda permissions to be invoked by API Gateway
        age_check_lambda.add_permission(
            "ApiGatewayInvoke",
            principal=_iam.ServicePrincipal("apigateway.amazonaws.com"),
            source_arn=age_check_get_method.method_arn
        )

        # Create custom resource scope
        age_check_scope = _cognito.ResourceServerScope(
            scope_name="read:age",
            scope_description="Permission to access age check endpoint"
        )

        # Create resource server for custom scopes
        resource_server = user_pool.add_resource_server(
            "ResourceServer",
            identifier="custom",
            scopes=[age_check_scope]
        )

        # Add custom scope to OAuth scopes
        oauth_scopes.append(_cognito.OAuthScope.resource_server(resource_server, age_check_scope)) 

        """
        SUPPRESSION RULES FOR CDK_NAG
        """

        NagSuppressions.add_resource_suppressions(
            self.api, [
                { "id": "AwsSolutions-APIG2", "reason": "This is demo api. There is no request parameters for this API."}
            ]
        )

        NagSuppressions.add_resource_suppressions(
            self.api.deployment_stage, [
                { "id": "AwsSolutions-APIG1", "reason": "This is demo api. Logging of APIGW is not necessary. Instead, Lambda turned on logging."},
                { "id": "AwsSolutions-APIG3", "reason": "This is demo api. This is protected by COGNITO_USER_POOLS authorizer. It's enough for dev env. If you go to production, you have to attach WAF."},
                { "id": "AwsSolutions-APIG6", "reason": "This is demo api. Logging of APIGW is not necessary. Instead, Lambda turned on logging."}
            ]
        )

        NagSuppressions.add_resource_suppressions(
            age_check_lambda.role, [
                { "id": "AwsSolutions-IAM4", "reason": "Managed policies were used by CDK automatically. Managed policies make CDK simple."}
            ]
        )

        NagSuppressions.add_resource_suppressions(
            age_check_lambda, [
                { "id": "AwsSolutions-L1", "reason": "Runtime version followed cognito-external-idp-proxy's runtime version."}
            ]
        )

Lambda の実装例

import boto3
import json
from datetime import datetime
import os

headers = {
    'Access-Control-Allow-Origin': 'http://localhost:5173',
    'Access-Control-Allow-Headers': 'Content-Type',
    'Access-Control-Allow-Methods': 'OPTIONS,GET'
}

def get_birthdate(username):
    """
    Get custom:birth attribute by cognito AdminGetUser api
    """
    try:
        cognito = boto3.client('cognito-idp')
        response = cognito.admin_get_user(
            UserPoolId = os.environ.get('USER_POOL_ID'),
            Username = username 
        )
        # Get value when match UserAttributes Name is custom:birth
        birthdate:str = next((attr['Value'] for attr in response['UserAttributes'] if attr['Name'] == 'custom:birth'), None)
        return birthdate
    except Exception as e:
        raise ValueError(f"Error getting birthdate: {str(e)}")


def calculate_age(birthdate_str):
    """
    Calculate age from birthdate string in YYYYMMDD format
    """
    try:
        birthdate = datetime.strptime(birthdate_str, '%Y%m%d')
        today = datetime.now()
        age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day))
        return age
    except ValueError as e:
        raise ValueError(f"Invalid birthdate format. Expected YYYY-MM-DD, got: {birthdate_str}")

def handler(event, context):
    """
    Lambda function to check age from claims in the JWT token and verify if user is over 20
    """
    try:
        print("+++ event +++")
        print(event)
        # Get the claims from the authorizer context
        claims = event['requestContext']['authorizer']['claims']
        
        # Check if sub claim exists
        if 'username' not in claims:
            return {
                'statusCode': 400,
                'headers': headers,
                'body': json.dumps({
                    'message': 'sub claim not found in token'
                })
            }
        
        # Calculate age and check if over 20
        birthdate = get_birthdate(claims['username'])
        if birthdate is None:
            return {
                'statusCode': 400,
                'headers': headers,
                'body': json.dumps({
                    'message': 'This user is not found in userpool.'
                })
            }
        age = calculate_age(birthdate)
        is_over_20 = age >= 20
        
        return {
            'statusCode': 200,
            'headers': headers,
            'body': json.dumps({
                'isOver20': is_over_20,
            })
        }

    except Exception as e:
        return {
            'statusCode': 500,
            'headers': headers,
            'body': json.dumps({
                'message': f'Error processing request: {str(e)}'
            })
        }

属性マッピングの修正

認証後にデジタル認証アプリサービス API から取得できる券面情報(基本4情報)をマッピングしたい場合、name, gender, addressは Cognito の標準の属性が利用できますが、birthdate は 10 桁の YYYY-mm-dd フォーマットである必要があるため、この API で提供されるデータ形式と異なり、エラーとなってしまいます。そこで、UserPool 作成時に、カスタム属性として custom:birthdate を追加し、ログイン時にこのカスタム属性に券面情報の生年月日を連携します。これを行うには、以下のように CDK のソースコードを修正します。

UserPool へのカスタム属性の追加

生年月日をマッピングしたい場合だけ、追加してください。cognito-external-idp-proxy/cdk/pjwt_with_pkce_stack.py346行目を以下のように更新してください。

# Cognito User Pool base configuration
cognito_user_pool = _cognito.UserPool(self, "UserPool",
    custom_attributes={"birth": _cognito.StringAttribute(mutable=True, max_len=8, min_len=8)}
)

カスタム属性のマッピング

また、基本 4 情報を全てマッピングする場合は、以下のように修正してください。(不要な属性はマッピング対象から除外してください。)cognito-external-idp-proxy/cdk/pjwt_with_pkce_stack.py350行目を以下のように更新してください。

cognito_user_pool_idp_oidc = _cognito.UserPoolIdentityProviderOidc(
    self, "IdentityProvider",
    client_id = self.node.try_get_context("idp_client_id"),
    client_secret = self.node.try_get_context("idp_client_secret"),
    issuer_url = idp_issuer_url,
    user_pool = cognito_user_pool,
    attribute_request_method = _cognito.OidcAttributeRequestMethod.GET,
    attribute_mapping = _cognito.AttributeMapping(
       address = _cognito.ProviderAttribute.other("address"),
        fullname = _cognito.ProviderAttribute.other('name'),
        gender = _cognito.ProviderAttribute.other("gender"),
        custom = {
            "custom:birth": _cognito.ProviderAttribute.other("birthdate"),
        }
    ),
    endpoints = _cognito.OidcEndpoints(
        authorization = apigw_proxy_route_auth_uri,
        jwks_uri = idp_issuer_url + self.node.try_get_context("idp_keys_path"),
        token = apigw_proxy_route_token_uri,
        user_info = idp_issuer_url + self.node.try_get_context("idp_attributes_path")
    ),
    name = self.node.try_get_context("idp_name"),
    scopes = self.node.try_get_context("idp_scopes").split()
)

Tips
Cognito はカスタム属性をキーに検索できないため、カスタム属性をキーに検索したい要件があれば、外部のデータストア(Amazon DynamoDB など)に券面情報を保存することをご検討ください。

デプロイ方法

ここまで実装お疲れ様でした。この後はデプロイを行い、動作を確認していきます。
cognito-external-idp-proxy のカスタマイズが完了し、デジタル認証アプリのサンドボックス環境の準備が整っていることを確認し、以下のようにパラメータを設定の上、cognito-external-idp-proxy の README の手順に沿ってデプロイします。

{
  "api_version": "v1",
  "api_authn_route": "/oauth2/authorize",
  "api_callback_route": "/callback",
  "api_token_route": "/oauth2/token",
  "idp_attributes_path": "/protocol/openid-connect/userinfo",
  "idp_auth_path": "/protocol/openid-connect/auth",
  "idp_keys_path": "/protocol/openid-connect/certs",
  "idp_token_path": "/api/realms/main/protocol/openid-connect/token",
  "idp_client_id": "xxxxxxxxxxxxxxxxxxxxxxxx",
  "idp_client_secret": "secret",
  "idp_issuer_url": "https://sb-auth-and-sign.go.jp/api/realms/main",
  "idp_name": "digital-auth-and-sign",
  "idp_scopes": "openid name address birthdate gender",
  "pkce": true,
  "stack_name": "CognitoExternalIdpProxyStack",
  "userpool_allowed_callback_url": "http://localhost:5173"
}

無事デプロイが終われば、J-LIS より貸与された、または購入したテストカードを利用し、動作確認を行います。

動作確認

それでは、デジタル認証アプリでのログインと券面情報(ユーザの情報、基本4情報)が取れるか、取得したアクセストークンを用いて年齢確認 API にリクエストを送り、20 歳以上かどうか判定するデモを、動画でご覧ください。

SPA に表示されている、「マイナンバーカードでログイン」をクリックすると、デジタル認証アプリが提供するサービスページに遷移し、デジタル認証アプリのログインシーケンスが始まります。
QR コードが表示されますので、これをスマートフォンで読み取ると、スマートフォンにインストールされたデジタル認証アプリが起動し、マイナンバーカードによるログインを求められます。
ログインが成功すると、先ほどまでデジタル認証アプリのサービスページが表示されていたタブは、リダイレクトし SPA のログイン後画面に遷移します。券面情報は、Cognito の属性マッピングにより、Cognito へ連携されているため、Cognito の API から取得でき、SPA 上に表示することができます。

Cognito へ属性マッピングされた券面情報は、引き続き、自社のサービスで利用することができます。今回は、アクセストークンの sub クレームから取得したユーザー ID をキーに、Cognito に登録された生年月日を GetAdminUser API で取得し、ログインしたユーザーが 20 歳以上か判定する処理を実装しました。
このようにデジタル認証アプリサービス API を利用し、自社のサービスをより高度に・高価値にしていくことができることでしょう。

終わりに

いかがでしたでしょうか。本記事では、デジタル認証アプリサービス API を利用し、Cognito と連携するための実装方法を紹介しました。
皆様が実装を行う際には、利用予定のユーザの認証リクエストの予測数から、AWS 側の API のクォータ(本サンプルでは、Cognito の GetAdminUser API や、Secrets Manager の GetRandomPassword API を利用します)を確認し、問題なくサービス提供できるよう設計ください。
本人認証や本人確認は、サービスを提供する上でなくてはならないものであり、一連の体験は、お客様の満足度向上に大きく寄与する部分です。JPKI を利用した認証により、サービスの利便性を向上させることはこのデジタル社会では不可欠ですので、ぜひご活用いただければと思います。

著者

Yozo Suzuki

鈴木 陽三 (Yozo Suzuki)
アマゾン ウェブ サービス ジャパン合同会社
プロトタイプ ソリューション アーキテクト
好きなAWSのサービスは、AWS CDK と AWS IAM Identity Center
趣味は、マンガを読むことと、カメラで娘の写真を撮ること