🔑

Next.js で AWS Cognito のパスキー認証を行う

2024/01/25に公開

はじめに

マジックリンクやパスキーを使ったログイン(以下、パスワードレス認証)は、今までのパスワードを使った認証より安全であるといわれており、様々なサービスで使えるようになってきています。

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でも概ね同じです。

  1. ディレクトリに移動します。
$ cd cognito-passwordless-next-sample/cdk
  1. パッケージをインストールします
$ yarn
  1. .env ファイルを編集します。
$ code .env

lib/cdk-stack.ts で使用するための変数を設定します。

各変数は以下のようになっています。

  • FRONTEND_HOST, FRONTEND_URL : Cognito にアクセスする Origin の ホストネームおよび URL を記述します
    • ローカル環境でデバッグする際は、localhost を指定してください
    • パスキーを設定するには https である必要があるので、出来れば Vercel などのデプロイ先を指定してください
  • EMAIL_FROM_ADDRESS : マジックリンクを送る際の from のメールアドレスを記述します
    • DNS が適切に設定されていないアドレスからのメールは迷惑メールに入りますので、検証の際は注意してください。詳しい説明は本記事の内容から逸れるので割愛します
  1. デプロイします

デプロイの準備。
ここで失敗した場合は、認証が正しくできているかを確認してみてください。

$ 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

にあります。

手順

  1. ライブラリのインストール
yarn install
  1. .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 を書いてください
  1. 実行する

Vercel 等にデプロイするか、$ yarn dev で実行します。

  1. メールでのユーザー新規作成

このようなページが開くので、下にある「アカウントを作成」を押して
次の新規登録ページでメールアドレスを入力します。

すると、このようなメールが届きます。
メールに書かれているリンクをクリックすると、ログイン済みの状態になります。

  1. パスキーの作成

ログイン完了のページに遷移するので、
右上のメニューリンクから「Credentialの作成」をクリックします。

デバイス名を入力して、パスキーを登録します。
例えば、Windows であれば、次のようにWindows Hello のダイアログが出て、登録されます。

  1. パスキーでログイン

右上の「ログアウト」をクリックして、一度ログアウトした後で、「生体認証でログイン」をクリックすると、先ほど登録したパスキーでログインができます。

これで、基本的な操作は完了です。

補足

AWSサンプルリポジトリの見方

このサンプルにないことをやりたい時のために、例のサンプルリポジトリの見方もメモしておきます。

amazon-cognito-passwordless-authリポジトリの内部には、Cognitoをパスワードレス認証で使うためのドキュメントや、実際に AWS の API へアクセスしているコードが含まれているのですが、正直、見方が難しいです。

まず最初に読むべきはREADMEファイルなのですが、これも乱立しています。。

最上の README に各 README へのリンクがあり、それを頼りに見ていく必要があります。
今回のNext.jsで使ったのはWebに関するREADMEReactに関するREADMEの2つです。

この2つを読むと、client/react/hooks.tsxに React用の hooks のコードが書かれていること、client/cognito-api.tsに Cognito を使うためのユーティリティ関数が書かれていることが分かります。

この中で export されている、外部から呼び出すことを想定された関数を見ていくと、何が出来るのかが大体わかります。

今回のサンプルでは、これらの情報を頼りに usePasswordless フック PasswordlessContextProvider フック、SignUp API を使い、Passwordless コンポーネントと同じ動きをするフォームを作りました。

(更に下位にある fido2.tsmagic-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 をご覧ください。

参考

Discussion