🧇

AWS Amplify Gen2でパスキー認証を実装してみる

2024/06/24に公開

はじめに

AWS公式サンプルのAmazon Cognito Passwordless Authを使ってApmlify Gen2上にパスキー認証を実装してみました。
https://github.com/aws-samples/amazon-cognito-passwordless-auth/

なお、フロントエンドはRemixのSPAモードにしてみました。(深い意味はなく使ったことないのでお試ししてみた感じです。)

Amplify Gen2で作ってみた経緯

パスキーは以前から試してみたいなと思っていて調べていたら上記のサンプルを見つけたのですが、載っている構成図がこんな感じなわけですね。

SPAのフロントエンドにCognitoで認証の時点でAmplifyと相性がよさそうですが、バックエンドはCDKのコンストラクタが提供されているとなればAmplify Gen2がハマりそうだなと思ってやってみた感じです。

作ったもの

レポジトリ

作ったコードは公開しておきます。
https://github.com/k-ibaraki/amplify-gen2-passwordless-remix

事前準備(SESのIDを検証済みにしておく)

今回はパスキーによる認証とメールでマジックリンクを送信する認証の2種類が使えます。しかし、パスキーは1度登録しないと使えません。つまり少なくとも初回ログインではメールを受け取る必要があります。
逆に言うとアプリからメールを送る必要がありますので、Amazon SESにそのための設定が必要です。

  • SES経由でユーザーにメールを送る必要があるため、送信元メールアドレスのSESのIDを検証済みにしておきます。
    • アカウントがサンドボックス状態の場合は、送信先メールアドレスも検証済みにする必要があります。AWSアカウントのデフォルトはサンドボックスですのでご注意ください。

使い方(sandbox環境+ローカルで動かす場合)

バックエンドのサンドボックスを作る

  • レポジトリをクローンしたら、必要なパッケージをインストールします。
npm i
  • 必要な環境変数を用意します。.envでも良いです。
    • フロントエンドのURLとホスト名、認証用のメールを送るメールアドレスが必要になります
.env
AUTH_FRONTEND_URL=http://localhost:5173
AUTH_FRONTEND_HOST=localhost
AUTH_EMAIL_FROM_ADDRESS=(送信元メールアドレス)
  • Amplify Gen2のサンドボックスに、バックエンドをデプロイします。
npx ampx sandbox --profile sandbox

しばらく待てば、AWS環境に必要なリソースが用意されます。

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

今回はユーザー作成機能を作り込んでいないので、AWSのコンソールからユーザーを作成してください。
Cognitoのユーザープールはできているはずです。

ユーザーのステータスがパスワードを強制的に変更になってしまいますが、今回はパスワードを使わないので気にしないことにします。

フロントエンドを起動する

以下のコマンドでフロントエンドを起動します。

npm run dev

パスキーを登録する

初回はパスキーが登録されていないのでメールでログインしてください。メールアドレスを入力するとマジックリンクが送られます。
(メールが届かないときは、SESの設定をミスっているか、バックエンド構築時の環境変数をミスっている可能性が高いです。)

ログインすると右上からボタンが出てくるので、そこからパスキーを登録できます。

登録したら、ログアウトしたり他のブラウザから接続すれば、パスキーでログインできます。

使い方(Amplifyにホスティングする場合)

Githubのレポジトリを適当のforkして、Amplify Gen2に繋げばOKです。
注意点としてデプロイ時の環境変数にフロントエンドのホスト名とURLが必要ですが、1度デプロイしないと分からないので、環境変数設定後に再デプロイしてください。

作り方

以下は構築方法を簡単に説明します。
と言っても、前述のAWS公式のサンプルをAmplify Gen2上のアプリに適用しただけではあります。

フロントエンドのベースを作る

まずは、RemixのSPAモードをテンプレで構築しました。

npx create-remix@latest --template remix-run/remix/templates/spa

とりあえず、起動してみました。

npm run dev

Amplify Gen2で認証機能を作る

Amplify Gen2を初期化します。

npm create amplify@latest

AmplifyフォルダとAuthとdataが作られました。

今回はdataは扱わないので消してしまいました。(Authは残す。)

amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
- import { data } from './data/resource';

defineBackend({
  auth,
-  data,
});

ここまでで一旦Amplifyにdeployしておきました。

  • GitHubにpush
  • ブラウザでAWS Amplifyのコンソールから画面に従って設定
    • 基本デフォルトで行けたが出力ディレクトリだけbuild/clientに変更

デプロイできました。

一応amplify.ymlを作って、レポジトリ直下においておきます。
https://github.com/k-ibaraki/amplify-gen2-passwordless-remix/blob/main/amplify.yml

ベースが作れたので、次はパスワードレス認証をを実装していきます。

amazon-cognito-passwordless-authを導入

cognitoでパスワードレス認証(パスキーを含む)を実現できるamazon-cognito-passwordless-authを導入します。
単純にインストールしようとしたら依存関係でエラーになってしまったので、こちらのissueを参考にreactのバージョンを指定した上でインストールしました。
https://github.com/aws-samples/amazon-cognito-passwordless-auth/issues/167

npm i react@18.2.0 react-dom@18.2.0
npm i amazon-cognito-passwordless-auth

バックエンド実装

ライブラリのドキュメントAmplify Gen2のドキュメントを見ながらamplify/backend.tsにcdkのコードを書きました。
https://github.com/k-ibaraki/amplify-gen2-passwordless-remix/blob/main/amplify/backend.ts

なお、書き方に自信がないのが、以下の部分です。

// ユーザープールとクライアントを取得
const userPool = backend.auth.resources.userPool as cdk.aws_cognito.UserPool;
const userPoolClient = backend.auth.resources.userPoolClient as cdk.aws_cognito.UserPoolClient;
// AuthのCDK Stackを取得
const authStack = Stack.of(userPool);

UserPoolUserPoolClientはそのままだと型が合わなかったので、型アサーションで書換えたのですがスマートではない感じがします。また、StackをStack.of()で取ってくるのは無理矢理な感じがします。きれいな書き方があれば教えて欲しいです。

ともあれ、この状態でデプロイすれば、パスワードレス認証に必要なCognito,Lambda,DynamoDB,Api Gateway等を一通り作ってくれます。

フロントエンド実装

続いて、ライブラリのReactのドキュメントを見ながら、フロントエンドの実装をします。

app/entry.client.tsx

まずは、entry.client.tsxに初期設定を追加します。
https://github.com/k-ibaraki/amplify-gen2-passwordless-remix/blob/main/app/entry.client.tsx
Amplifyの初期設定もしていますが後で見返したら今回は特に使ってませんでした。Passwordless.configure()が重要です。

app/root.tsx

続いてroot.tsxで認証機能を実装します。
https://github.com/k-ibaraki/amplify-gen2-passwordless-remix/blob/main/app/root.tsx
ここで使っているPasswordlessは、さきほどentry.client.tsx内で使ったPasswordlessと別物なので注意が必要です。(同一ライブラリ内で別な物に同じ名前をつけるってどうなの?)

<Passwordless>で囲むことで認証が通ったユーザーのみがコンテンツにアクセスできるようになります。また<Fido2Toast />を配置することで、ログイン後にパスキー登録するためのToastが表示されます。

app/routes/_index.tsx

前述の部分で、最低限のパスキー認証ができる実装にはなっていますが、ついでなのでusePasswordless()で使える機能を一部実装してみました。

  • ログアウトする
  • 認証情報を消す
  • アクセストークンやIDトークンを取得する

usePasswordlessの詳細はドキュメントをご確認ください。

https://github.com/k-ibaraki/amplify-gen2-passwordless-remix/blob/main/app/routes/_index.tsx

まぁログアウトや認証情報削除がないと動作確認が面倒くさすぎたというだけではあります。

最後に

パスキーやFido2の認証は普通に作るとかなり大変なので、比較的簡単に作れるライブラリをAWSが出してくれているのはありがたいです。
対象としている構成がReact+Cognito+CDKだったのでAmplify Gen2に良さそうと思って試してみたのですが、かなりよいと思いました。

参考文献

AWS公式ドキュメント類の他にこちらの記事を参考にさせていただきました。
https://zenn.dev/kaibutsu/articles/40b3bfb3261b7f

NCDCエンジニアブログ

Discussion