Dev Study
AWSサービスの内部原理 コース

129. パスワードを送らずに証明する仕組みがある — SRPというゼロ知識に近い認証と、OAuthの認可コードフロー

「ログインする」という一言の裏で、パスワードそのものは本当にネットワークを流れているのか——SRPという「送らずに証明する」認証と、OAuthの「コードを引換券にする」認可コードフローという2つの経路を、リクエストのパラメータを1つずつ追いながら図で見ていきます。

① 2つの分岐点 — パスワードを送るか、証明だけを送るか

公式が投げかける問い(Authentication flows)
「どうログインさせるか」の設計判断公式が管理者に問う分岐点
公式の問いかけがそのまま2択になる
パスワードを送るALLOW_USER_PASSWORD_AUTH
素朴・実装が軽い
ハッシュとソルトで交渉するALLOW_USER_SRP_AUTH
SRP・多重の防御
  • 公式の問い(原文): 「Could my authentication requests for username-password authentication be intercepted? Do I want my application to transmit passwords, or to negotiate authentication using hashes and salts?」——「傍受されうるか。パスワードを送るのか、それともハッシュとソルトで認証を交渉するのか」がこの2方式の分かれ道。
  • どちらもアプリクライアントの Authentication flows で有効化する設定(ExplicitAuthFlows)。プレーンパスワード方式は ALLOW_USER_PASSWORD_AUTH、SRP方式は ALLOW_USER_SRP_AUTH。これらはコンソール/CreateUserPoolClient・UpdateUserPoolClientのExplicitAuthFlowsで設定する名称であり、実際にInitiateAuthへ渡すAuthFlowの値(USER_PASSWORD_AUTH / USER_SRP_AUTH)とは別の名前空間である点に注意。
  • 根拠: amazon-cognito-user-pools-authentication-flow-methods.html(Sign-in with persistent passwords / …and secure payload)

どちらの経路も「ユーザー名とパスワードでログインする」点は同じ。違いは、パスワードという秘密をネットワークに乗せるか、乗せずに「知っている証明」だけを送るか——この一点にある。

② プレーンパスワード方式 — TLSの中を平文が通る

ALLOW_USER_PASSWORD_AUTH の中身
アプリUSERNAME + PASSWORD を組み立てる
InitiateAuth(AuthFlow: USER_PASSWORD_AUTH)
リクエスト本文PASSWORD: 「ユーザーのパスワードそのもの」
TLSで暗号化はされる
検証
ユーザープール受け取った値を照合してトークンを返す
  • AuthParameters に USERNAME と PASSWORD をそのまま入れる(公式のリクエスト例より。'Client-based sign-in with a password'節、AuthFlow: USER_PASSWORD_AUTH)。値はTLSで保護されるが、パスワード本体はサーバーに到達している。
  • 公式は『User migration authentication flow』の節で関連する言い回しを使っている: 「This flow sends your users' passwords to the service over an encrypted SSL connection during authentication.」——これは移行用のUSER_MIGRATION Lambdaトリガーを使い、かつUSER_PASSWORD_AUTHを選んだ場合の記述だが、暗号化された接続の内側をパスワード本体が通るという構造自体は通常のUSER_PASSWORD_AUTHでも同じ。
  • 同じ節が続けて言う: 「When you have migrated all your users, switch flows to the more secure SRP flow. The SRP flow doesn't send any passwords over the network.」——これも移行完了後の推奨だが、「SRPはパスワードをネットワークに一切流さない」という対比の骨子は本編の主旨と一致する。
  • 根拠: amazon-cognito-user-pools-authentication-flow-methods.html(Sign-in with persistent passwords / User migration authentication flow)

素朴な方式では、TLSという1枚の膜の内側をパスワード本体が通り、サーバーに届く。TLSが守ってくれるとはいえ、防御はその1枚に依存している。公式自身が(ユーザー移行の文脈で)「より安全なSRPに切り替えよ」と促している。

③ SRP方式 — 送るのは g^a % N という計算結果だけ

ALLOW_USER_SRP_AUTH の第1往復
クライアント秘密の乱数 a を選ぶ(外に出さない)
g^a % N を計算して SRP_A を作る
SRP_A を送るInitiateAuth(AuthFlow: USER_SRP_AUTH) / AuthParameters: { USERNAME, SRP_A }
パスワードはここに無い
ユーザープールが応答
PASSWORD_VERIFIER チャレンジ「では証明してみせよ」と問い返す
  • 公式の定義: 「This option sends proof of knowledge of a password—a password hash and a salt—that your user pool can verify. With no readable secret information in the request to Amazon Cognito, your application is the only entity that processes the passwords that users enter.」——リクエストに読み取れる秘密情報は一切なく、パスワードを扱うのはアプリだけ。
  • SRP_A の正体: 公式のリクエスト例で SRP_A の値は {{[g^a % N]}} と注記される。本文いわく「The client generates SRP_A from a generator modulo N g raised to the power of a secret random integer a.」——生成元 g を秘密の乱数 a 乗して N で割った余り。a は外に出ないので、SRP_A から a は逆算できない。
  • 設定名(ALLOW_USER_SRP_AUTH)とAPIのAuthFlow値(USER_SRP_AUTH)は別物——設定はアプリクライアントの許可フラグ、AuthFlowは実際にInitiateAuthへ渡す値。
  • 根拠: amazon-cognito-user-pools-authentication-flow-methods.html(secure payload / Client-based sign-in with SRP)

最初に送るのは SRP_A ——秘密の乱数 a を g^a % N という一方向の計算に通した結果だけ。パスワードも a も乗っていない。サーバーは「その値が本物なら、続きの証明もできるはずだ」と問い返す(PASSWORD_VERIFIER チャレンジ)。

④ SRPの第2往復 — 秘密を明かさず「知っている」を証明する

PASSWORD_VERIFIER への応答(チャレンジレスポンス)
PASSWORD_VERIFIERサーバーからのチャレンジ
RespondToAuthChallenge で3つの値を返す
PASSWORD_CLAIM_SIGNATUREパスワードを知る者だけが作れる署名
PASSWORD_CLAIM_SECRET_BLOCKサーバー由来の照合材料
TIMESTAMP計算のタイムスタンプ
一致を確認できたら
トークン発行ID / アクセス / リフレッシュ(またはMFAへ)
  • 第2往復で返すのは PASSWORD_CLAIM_SIGNATURE / PASSWORD_CLAIM_SECRET_BLOCK / TIMESTAMP の3つ(公式のチャレンジレスポンス例より、ChallengeName: PASSWORD_VERIFIER)。パスワード本体はここでも送らない——送るのは「パスワードを知っている者にしか作れない署名」。
  • つまり全体は 初期化(SRP_A)→ チャレンジ(PASSWORD_VERIFIER)→ 応答(署名)という往復型。サーバーもクライアントも、相手の秘密そのものは一度も受け取らずに一致を確認する。公式: 「On a successful PASSWORD_VERIFIER challenge response, Amazon Cognito issues tokens or another required challenge like multi-factor authentication (MFA).」
  • 既習との接続: 認証暗号編のSigV4は、シークレットアクセスキーを送らずHMACで「鍵を持っている」を証明していた。SRPはその認証版——「秘密を明かさず、知っていることだけを証明する」チャレンジレスポンスという同じ設計思想(ゼロ知識証明に近い発想)。
  • 根拠: amazon-cognito-user-pools-authentication-flow-methods.html

2往復目でも渡すのは署名であって秘密そのものではない。SigV4が「鍵を送らず所持を証明」したのと同じ発想を、認証に持ち込んだのがSRP。パスワードは一度もネットワークに乗らない。

⑤ もう一つの経路 — OAuthの認可コードフロー(Hosted UI / managed login)

authorization code grant(response_type=code)
アプリ自前のログイン画面を作らない
/oauth2/authorize?response_type=code&client_id=…&redirect_uri=… へリダイレクト
managed login / Hosted UICognitoがホストするログインページ
OAuth 2.0 / OIDC の認可サーバー
ユーザーがサインイン → redirect_uri に返す
?code=AUTHORIZATION_CODE&state=STATE認可コードがクエリ文字列で返る
アプリが裏で交換(サーバー間通信)
/oauth2/tokenコードを ID/アクセス/リフレッシュ トークンに引き換える
  • managed login とは何か(公式): ドメインを設定したユーザープールが得る「An authorization server that acts as an identity provider (IdP) to applications that work with OAuth 2.0 and OpenID Connect (OIDC)」+「A ready-to-use user interface (UI) for authentication operations」。旧称の hosted UI(classic)は「a first-generation version of the managed login services」——同じ系譜の第1世代。
  • コードが返る経路の正確な内訳: アプリは `/oauth2/authorize` にリダイレクトする。認可サーバーは(identity_providerを指定しない場合)内部でさらに `/login` エンドポイントへリダイレクトしてログイン画面を出す。ユーザーがサインインすると、最終的にこの `/login` エンドポイントが redirect_uri に応答を返す——「The server must return the code and state in the query string parameters and not in the fragment.」→ Location: https://YOUR_APP/redirect_uri?code=AUTHORIZATION_CODE&state=STATE。コードはURLの ?(クエリ)に返り、#(フラグメント)には返さない。
  • トークン交換(公式): POST /oauth2/token に grant_type=authorization_code, client_id, code, redirect_uri を送ると、'access_token' / 'id_token' / 'refresh_token' を含むJSONが返る。トークンへの引き換えはこの別のPOSTリクエスト(通常はサーバー間)で行うため、URLに code が漏れても単体では危険度が低い。
  • 根拠: cognito-user-pools-managed-login.html / login-endpoint.html / token-endpoint.html

認可コードフローでは、ログイン画面はCognitoがホストし、アプリにはまず「code」という引換券だけが返る。トークンへの引き換えはブラウザの外(/oauth2/token へのサーバー間通信)で行うので、URLにcodeが漏れても、それ単体では鍵にならない。

⑥ implicit grant との対比 — トークンがURLに直接返る旧式

response_type=code と response_type=token
認可コードフローresponse_type=code(推奨)
implicit grantresponse_type=token(旧式)
返るのは code?code=… (クエリ文字列)
引換券。単体では無力
返るのは token そのもの#id_token=…&access_token=… (フラグメント)
URLに本物が乗る
別途 /oauth2/token で交換秘密はブラウザの外で扱う
交換ステップなしアドレスバーにトークンが現れる
  • implicit の公式例: 「response_type=token」で「After a successful sign-in, Amazon Cognito returns user pool tokens to your web browser's address bar.」応答は https://…/#id_token={{…}}&access_token={{…}}&expires_in=3600&token_type=Bearer ——ID/アクセストークンが # に直接付く。
  • 対して認可コードフローは code という間接参照を1枚挟む。「本物(トークン)を晒さず、引換券(コード)を短命な仲介として渡す」構図。
  • 既習との接続: STSの一時認証情報や署名付きURLと同じ「間接参照で直接の秘密を晒さない」原則。コードは短命な引換券として振る舞い、実際の鍵(トークン)はサーバー間の交換でしか手に入らない——認可コードフローが implicit より推奨されるのはこの一枚の分離ゆえ。
  • 根拠: login-endpoint.html(implicit grantの例はauthentication-flow-methods.htmlの'View login pages'節にも実例あり) / cognito-user-pools-managed-login.html

implicit grant はトークンをURLフラグメントに直接返す旧式。認可コードフローは「コード(引換券)」と「トークン(本物)」を分離し、本物はブラウザの外でしか手に入らないようにする——STSの一時認証情報や署名付きURLで見た「間接参照で秘密を晒さない」原則の、認証版の実装。

公式ドキュメントで詳しく ↗