🧇

パスキーの仕組みを、AWS上に構築したアプリから学ぶ

2024/07/25に公開

はじめに

先日、AWSが提供されているサンプルを使ってAmplify Gen2上にパスキー認証を実装してみました。
https://zenn.dev/ncdc/articles/5aa4e337c626a6

今回は、実装されたAWSリソースを見ながら、パスキーによる認証処理を学んでいきたいと思います。

そもそも、パスキーとパスワードは何が違うか?

パスキーとはパスワードに変わる認証方法と言われています。しかし、そもそもパスワードと根本的に異なる点は何処でしょうか?
AWS上でのパスキーの実装を見る前に、スマホでパスワードで認証する時とスマホで指紋認証するときの違いを考えてみましょう。

パスワードで認証する時は、以下の手順になります。

  1. スマホにパスワードを入力する
  2. スマホからサーバーにパスワードを送信する
  3. サーバーでパスワードを認証する

まぁ普通ですね。

これに対して、指紋認証はどうなるでしょうか?

  1. スマホで指紋認証する
  2. スマホから認証結果(?)をサーバーに送信する
  3. サーバーで認証結果(?)を認証する

はい。意味不明ですね。
サーバーに何を送っているのか?サーバーは何をもってユーザーを認証しているのか?全く分かりません。

何故こういう事になっているかと言うと、

  • パスワードはサーバーで認証している
  • 指紋はスマホで認証している

という違いがあり、指紋認証は本来スマホで完結しておりサーバーは不要です。

ということですので、パスワードとパスキーの違いは生体認証かどうかとかではなくサーバーで認証しているか手元の端末で認証しているということになります。そして、手元の端末で認証した情報でいかに安全にサーバーにログインするかがパスキーにおける重要なポイントとなります。

AWS上に実装されたパスキーログインの仕組みを見てみよう

それでは、前回の記事でAWS上にデプロイしたアプリの挙動を追うことで、パスキーにおけるログインの仕組みを確認してみましょう。

AWSの構成

認証のシステムですので、AWSに詳しい方ならCognitoを中心とした構成となっているのは想像できるかと思います。実際その通りではありますが、AWS上に作られたリソースを見るとLambdaやDynamoDBも作られている事がわかります。


Amplify Gen2を使ったのでデプロイされたリソースの一覧が見れて便利ですね。Lambdaの下2つはAmplifyの管理用?ですので、今回のアプリが作ったのはLambdaが6本とDynamoDBが2テーブルになります。

より細かく確認すると下図のような構成になっていました。

パスキー認証処理の動きを見る

それでは、実際にアプリを動かしてパスキー認証の動きを見てみます。実装されている主要な処理は主に以下の4個がありますが、この記事ではパスキーによるログイン処理のみを説明します。

  1. メールに送ったマジックリンクからログインする
  2. パスキーを登録する
  3. 登録されたパスキーでログインする
  4. 登録済のパスキーを管理する

他の処理は需要がありそうなら追って記事にするかもしれません。

最初にユーザー名を指定するパターンの認証処理

今回、AWS上の処理を追っていて知ったのですが、実はパスキーの認証には2種類あるみたいです。「最初にユーザー名を指定するパターン」と「ユーザーを指定せずに処理を開始し、認証中にパスキーを選択するパターン」です。ユーザーとしては後者のほうが簡単ですが、アプリの挙動は前者のほうが分かりやすいので、今回はユーザー名指定パターンを見てみます。

では早速、実際にデプロイしたアプリでこの画面の上のほうのボタンを押下したときの挙動を確認します。

ブラウザのネットワークタグで通信を確認しながら動きを見てみました。

最後にAPI Gateway上のAPIを呼び出していますが、これはログイン後の情報取得でありログイン処理とは関係ありません。
ログイン処理としてはCognitoの呼び出しが2回ということになります。より細かく見ると指紋認証の前後で1回ずつCognitoを呼び出していおり、次の図のようなフローになっていました。

この動きは、Cognitoのカスタム認証に沿った流れになっています。

カスタム認証を、ものすごくざっくり書くと、

  • CreateAuthChallengeのLambdaでユーザーに対して問題を出題
  • アプリのフロントエンドで問題を解く
  • VerifyAuthChallengeのLambdaで答え合わせ

をすることで認証します。先ほどから出てきているチャレンジとはユーザーに対して出題する問題のことですね。いわゆるチャレンジ&レスポンス方式の認証ということになります。

パスキー認証の中身を知るためにこれらの処理をもう少し詳しく見てみましょう。

CreateAuthChallenge

まずは、CreateAuthChallenge、つまり、Cognitoからユーザーに問題を出す処理です。
ソースを追っても良いのですが、まずは手っ取り早くブラウザ上のdevツールでレスポンスを確認してみます。レスポンスそのままだと見づらいので、ざっくりと整形すると以下のようになっていました。

{
  "ChallengeName": "CUSTOM_CHALLENGE",
  "ChallengeParameters": {
    "USERNAME": "(Cognitoのユーザー名)",
    "challenge": "PROVIDE_AUTH_PARAMETERS",
    "fido2options": {
      "challenge": "(ランダムっぽい文字列)",
      "credentials": [
        {
          "id": "(パスキーのID。DynamoDBに入っている)",
          "transports": ["hybrid", "internal"]
        }
      ],
      "timeout": 120000,
      "userVerification": "required"
    }
  },
  "Session": "(Cognitoのセッション)"
}

この中で重要なのはfido2optionsの部分です。
中身はランダムな値であるchallengeとDynamoDBから取得したパスキーのID等の値が入っています。パスキー登録処理を説明していないので分かりにくいですが、登録処理時にパスキーのID等の必要なデータをDynamoDBに保存しています。
実は、このfido2optionsはパスキー認証時にブラウザ側(つまりフロントエンド)で実行する時に呼び出すnavigator.credentials.get()の引数(PublicKeyCredentialRequestOptions)にほぼ一致します。
このあたりを深堀りしたい場合は、MDNの該当ページを参考にしてください。
https://developer.mozilla.org/ja/docs/Web/API/Web_Authentication_API#認証

CreateAuthChallengeの処理をMDNのページ内にある下図に当てはめると①の部分に相当する処理ということになります。

より具体的な実装を知りたい場合は、以下のソースを追いましょう。
https://github.com/aws-samples/amazon-cognito-passwordless-auth/blob/main/cdk/custom-auth/create-auth-challenge.ts

フロントエンドでの処理

CreateAuthChallengeの後は、フロントエンドアプリで先程のMDNのドキュメントに載っているnavigator.credentials.get()を実行します。
するとブラウザとスマホがいい感じに動いて、指紋認証→指紋に紐づいた秘密鍵でChallengeに電子署名→署名されたメッセージをフロントエンドに返す。という処理を実行してくれます。
先程のMDNの図で②③④の部分になります。

実際の実装を確認したい場合は、以下のソースで追うことができます。
https://github.com/aws-samples/amazon-cognito-passwordless-auth/blob/main/client/fido2.ts#L354

なお、唐突に秘密鍵が出てきましたが、これはパスキー登録時にスマホ内部で生成してこっそり保管されているものになります。

VerifyAuthChallenge

フロントエンドで電子署名をしたら、サーバーサイドつまりCognitoに紐づくLambdaがその署名を検証することで、パスキーの認証処理が完了します。

より具体的な実装を知りたい場合は、以下のソースを追いましょう。
https://github.com/aws-samples/amazon-cognito-passwordless-auth/blob/main/cdk/custom-auth/verify-auth-challenge-response.ts

署名の検証には指紋に紐づいた公開鍵が必要になります。この公開鍵もパスキー登録時にスマホ内部で作成されてDynamoDBに保存されています。

パスキー認証処理まとめ

パスキー認証の処理をまとめると下図のような処理をしているということになります。

また、最初に書いた手元の端末で認証した情報でいかに安全にサーバーにログインするかの答えとしては、登録時に指紋に紐づく秘密鍵と公開鍵を発行しておき電子署名で認証するということになります。

パスキーに関する補足

実はAWS上に実装したアプリを含めてここまでの説明は、パスキーという言葉が出る前から存在していたものです。今回使用したAWSのライブラリも履歴を追えば分かりますがパスキー登場以前から開発されています。

ここで記事が終わるとパスキーの話を何もしてないじゃんと思われそうなので、こういった認証がパスキーと呼ばれるようになった時に何が変わったのかを補足しておきます。

スマホで指紋によるログインをする場合、スマホの内部に指紋と紐づいた認証情報を保管する必要があります。ですので、スマホとPCとでは認証情報を共有ができませんでした。例えばPCから顔認証でログインしたい場合は、PCからも別途認証情報と登録してサーバーに登録する必要がありました。面倒くさいです。
その面倒くさい認証情報を、AppleとかGoogleとかMicrosoftがいい感じに共有してくれるようになったのがパスキーです。

非常に便利ですが、異なるデバイスに認証情報を共有しているので、もちろんセキュリティリスクがあります。情報の重要度と便利さを秤にかけて十分に気をつけて利用しましょう。

参考

参考にしました
https://blog.mmmcorp.co.jp/2024/02/14/cognito-with-passkey-2/

NCDCエンジニアブログ

Discussion