Next.js で AWS Cognito のパスキー認証を行う
はじめに
マジックリンクやパスキーを使ったログイン(以下、パスワードレス認証)は、今までのパスワードを使った認証より安全であるといわれており、様々なサービスで使えるようになってきています。
AWS Cognito は既にパスワードレス認証に対応する方法がある程度確立されています。
サンプルも幾つか出ていて、ReactについてはAWS公式が出しているサンプルプロジェクト もあります。
このサンプルは
<Passwordless>
<App />
</Passwordless>
と Passwordless
コンポーネントでアプリ全体を囲うと、未ログイン時にパスワードレス認証のフォームが出てくるサンプルが紹介されています。
このコンポーネント内部の細かい解説がないため、自分で同じ機能を持つフォームを作ろうとしたとき、分かりにくいと感じました。
備忘録も兼ねて、各機能でバラしたサンプルを作りました。
作成したサンプルコード
これは何?
Cognito のパスワードレス認証を使うための Next.js でのサンプルコードです。
以下のことができます。
- パスワードレス認証に必要な AWS リソースの作成
- メールアドレスを用いたユーザー作成
- マジックリンク(メールにログイン用のURLが届く)でのログイン
- ログインしたユーザーでのパスキーの登録
- パスキーを使ったログイン
使い方
事前準備
詳しい説明はドキュメントを読んでください。
AWS CDK のインストール
AWS が用意したサンプルを使うため、CDK をインストールする必要があります。
また、CDKを使うためには AWS CLI のインストール および、アクセスキーとシークレットの設定を設定しておく必要があります。
余談ですが、AWS CLI2 から使えるようになった aws configure sso
による認証は記事を執筆している時点では、AWS CDK に未対応のようです。
(詳しくは検証してません。誤っている場合はご指摘ください)
Node.js と Yarn のインストール
Node.js はバージョン >= 16.x をインストールしてください。
筆者は 18 系を使って検証しています。
Yarn は必須ではありませんが、npmだとパッケージインストールに結構時間がかかるので、導入をお勧めします。
本記事では Yarn を前提に書いています。
バックエンドのデプロイ
バックエンドに関するファイルは
cognito-passwordless-next-sample/cdk
にあります。
これはAWSによるワークショップのコードをベースにしています。
手順
例は Ubuntu で説明しますが、他OSでも概ね同じです。
- ディレクトリに移動します。
$ cd cognito-passwordless-next-sample/cdk
- パッケージをインストールします
$ yarn
- .env ファイルを編集します。
$ code .env
lib/cdk-stack.ts
で使用するための変数を設定します。
各変数は以下のようになっています。
-
FRONTEND_HOST
,FRONTEND_URL
: Cognito にアクセスする Origin の ホストネームおよび URL を記述します- ローカル環境でデバッグする際は、localhost を指定してください
- パスキーを設定するには https である必要があるので、出来れば Vercel などのデプロイ先を指定してください
-
EMAIL_FROM_ADDRESS
: マジックリンクを送る際の from のメールアドレスを記述します- DNS が適切に設定されていないアドレスからのメールは迷惑メールに入りますので、検証の際は注意してください。詳しい説明は本記事の内容から逸れるので割愛します
- デプロイします
デプロイの準備。
ここで失敗した場合は、認証が正しくできているかを確認してみてください。
$ cdk bootstrap
⏳ Bootstrapping environment aws://xxxxx/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
CDKToolkit: creating CloudFormation changeset...
✅ Environment aws://xxxxx/ap-northeast-1 bootstrapped.
続いて、 デプロイを実行します。
途中、 Do you wish to deploy these changes (y/n)?
と訊かれるので、リソースを確認してから y
を押します。
$ cdk deploy --method direct
(中略)
✅ CdkStack
✨ Deployment time: 275.76s
Outputs:
CdkStack.ClientId = xxxxx
CdkStack.Fido2Url = https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/v1/
CdkStack.PasswordlessRestApiPasswordlessEndpointXXXXX = https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/v1/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxx:stack/CdkStack/xxx-xxx
✨ Total time: 278.6s
Outputs の箇所に作成したリソースのID等が出力されます。
この値は後で使うので、メモしておいてください。
もし、メモし忘れた場合は、AWS コンソールの CloudFormation -> CdkStackSample -> 出力 から確認することができます。
フロントエンドでの動作確認
フロントエンドに関するファイルは
cognito-passwordless-next-sample/front
にあります。
手順
- ライブラリのインストール
yarn install
-
.env
の設定
cognito-passwordless-next-sample/front/.env
に、AWSのライブラリで使用する各種変数を設定します。
これらの値は cognito-passwordless-next-sample/front/src/app/layout.tsx
で使用しています。
-
NEXT_PUBLIC_AWS_REGION
: AWSのリージョンを書きます。Tokyoであれば、ap-northeasat-1です -
NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID
: CDKのデプロイ時に出力された ClientId を書いてください -
NEXT_PUBLIC_FIDO2_API_URL
: CDKのデプロイ時に出力された Fido2Url を書いてください
- 実行する
Vercel 等にデプロイするか、$ yarn dev
で実行します。
- メールでのユーザー新規作成
このようなページが開くので、下にある「アカウントを作成」を押して
次の新規登録ページでメールアドレスを入力します。
すると、このようなメールが届きます。
メールに書かれているリンクをクリックすると、ログイン済みの状態になります。
- パスキーの作成
ログイン完了のページに遷移するので、
右上のメニューリンクから「Credentialの作成」をクリックします。
デバイス名を入力して、パスキーを登録します。
例えば、Windows であれば、次のようにWindows Hello のダイアログが出て、登録されます。
- パスキーでログイン
右上の「ログアウト」をクリックして、一度ログアウトした後で、「生体認証でログイン」をクリックすると、先ほど登録したパスキーでログインができます。
これで、基本的な操作は完了です。
補足
AWSサンプルリポジトリの見方
このサンプルにないことをやりたい時のために、例のサンプルリポジトリの見方もメモしておきます。
amazon-cognito-passwordless-authリポジトリの内部には、Cognitoをパスワードレス認証で使うためのドキュメントや、実際に AWS の API へアクセスしているコードが含まれているのですが、正直、見方が難しいです。
まず最初に読むべきはREADMEファイルなのですが、これも乱立しています。。
最上の README に各 README へのリンクがあり、それを頼りに見ていく必要があります。
今回のNext.jsで使ったのはWebに関するREADMEとReactに関するREADMEの2つです。
この2つを読むと、client/react/hooks.tsxに React用の hooks のコードが書かれていること、client/cognito-api.tsに Cognito を使うためのユーティリティ関数が書かれていることが分かります。
この中で export されている、外部から呼び出すことを想定された関数を見ていくと、何が出来るのかが大体わかります。
今回のサンプルでは、これらの情報を頼りに usePasswordless
フック PasswordlessContextProvider
フック、SignUp
API を使い、Passwordless
コンポーネントと同じ動きをするフォームを作りました。
(更に下位にある fido2.ts
や magic-link.ts
も目を通してみたのですが、認証の仕様に関するコードが多く、「ログインできるフォームが作れればいいんだ!」という段階であれば、ここまで理解しなくてよさそうです)
ログイン/未ログインによって要素の出し分けをしたい
ログイン状態は signInStatus
という Context に入っています。
import { usePasswordless } from 'amazon-cognito-passwordless-auth/react'
export default function Page (): JSX.Element {
const { signInStatus } = usePasswordless()
if (signInStatus === 'SIGNED_IN') {
// ログイン済みの時の処理
}
}
取りうる値は "SIGNING_OUT" | "SIGNED_IN" | "REFRESHING_SIGN_IN" | "SIGNING_IN" | "CHECKING" | "NOT_SIGNED_IN"
です。
なお、ページ遷移直後は CHECKING
になっているので、ログイン済みの時という条件で
if (signInStatus !== 'NOT_SIGNED_IN') {
// ログイン済みの時の処理
}
と書いてしまうと、遷移直後に一瞬だけ要素が表示されてしまいますので、注意してください。
(上記は仕様なので問題ないのですが、SIGNED_IN
に行く前に一瞬だけ NOT_SIGNED_IN
を経由するなど、まだライブラリとして未成熟さを感じさせる実装になっています)
マジックリンクの遷移先を変えたい
サンプルだとデフォルト値である /
にリダイレクトするようになっていますが、変更する場合はサーバーサイドとクライアントサイドの両方で設定が必要です。
サーバーサイド
セキュリティの問題で、リダイレクト先はCognitoのホワイトリストで管理されています。
そのリストにリダイレクトするパスを追加します。
cdk/lib/cdk-stack.ts
の 19行目以降を次のように編集します。
例えば、 /dashboard
に遷移させたい場合は次のように変更します。
const userPoolClient = userPool.addClient("passwordlessClient", {
oAuth: {
callbackUrls: [
`${process.env.FRONTEND_URL!}/`,
`${process.env.FRONTEND_URL!}/dashboard` // この行を追加
],
logoutUrls: [
`${process.env.FRONTEND_URL!}/`
],
}
});
そして、再度デプロイします。
今度は差分だけなので、すぐに終わります。
$ cdk deploy --method direct
ちなみに、同じことを Web コンソール経由でも行うことができます。
AWS コンソールで、Cognito -> (作成したユーザープール) -> アプリケーションの統合タブ -> アプリケーションクライアントのリスト-> (作成したアプリケーションクライアント)を開き、
「ホストされたUI」の項目にある「許可されているコールバック URL」を編集します。
フロントエンド
サインイン、サインアップのページファイルは
cognito-passwordless-next-sample/front/src/app/sign[in/up]/page.tsx
にあります。
遷移先は requestSignInLink()
関数の redirectURL
オプションで指定します。
例えば、サインアップ時のメールで遷移する先を変更したい場合は
const result = requestSignInLink({
username: email,
redirectUri: new URL('/dashboard', window.location.origin).href
})
とします。
これをデプロイすると、リダイレクト先が変更されます。
Cognito のユーザープールを複数作りたい
検証と本番環境のように、複数のCognito環境を作成したい場合です。
cdk/bin/cdk.ts
は最初こうなっています。
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkStackSample } from '../lib/cdk-stack';
const app = new cdk.App();
new CdkStackSample(app, 'CdkStackSample', {
});
CdkStackSample
クラスは cdk.Stack
クラスを継承しています。
第二引数でスタック名を指定できます。
もし、同じ構成で2つのスタックを作成する場合は、単純に new する処理を増やします。
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkStackSample } from '../lib/cdk-stack';
const app = new cdk.App();
new CdkStackSample(app, 'CdkStackSample1', {
});
const app = new cdk.App();
new CdkStackSample(app, 'CdkStackSample2', {
});
また、スタックごとに設定を変えたい場合は、適宜 CdkStackSample クラスのコンストラクターを拡張してください。
Cognitoでのユーザー作成について
Cognitoでユーザーを作成する場合、執筆時点ではパスワードを未設定にすることはできません。
(間違っていたらご指摘ください)
本サンプルでは、複雑で十分長いパスワードを自動生成し、設定しています。
具体的な実装は front/src/libs/Utils.ts
をご覧ください。
参考
-
amazon-cognito-passwordless-auth
- AWS による Cognito を用いた Passwordless 認証のサンプルコード
-
Implement Passwordless authentication with Amazon Cognito and WebAuthn
- 上記リポジトリを用いた公式ワークショップ
Discussion