😊

AWS Cognitoを使ってみる

2022/02/28に公開

概要

AWS Cognito を使ってログイン機能を構築してみます。
構築方法として、ログイン画面と会員登録画面について、Cognito 側で用意された UI(Hosted UI)を使用するパターンと、SDK 等を用いて完全に独自にログイン画面や会員登録画面を構成するパターンがあります。
まずは、Cognito 側で用意された UI(Hosted UI)を使用するパターンについて記載します。

手順

Cognito ユーザプール設定

  1. AWS コンソールからcognito-ユーザープールの管理-ユーザープールを作成するを押下します。
  2. プール名に任意のプール名を設定します。(例):testuserpool
  3. ステップに従って設定するを押下します。
  4. 属性の設定を行います。ここでの設定は後から変更できないため、先に要件を決めておく必要があります。ユーザ名でのサインインとするか、Eメールアドレス電話番号のいずれかあるいはその両方とするか、といった点を選択します。今回は、Eメールアドレスおよび電話番号Eメールアドレスを許可とし、E メールでの認証のみとします。
  5. 標準属性を設定すると、その属性はサインアップのために必要になります。また、`カスタム属性という形で任意の追加の属性を持たせることができます。cognito はデータベースのように使用するのは望ましくないため、一般的には、認証系の情報以外は持たせないほうが良いのではないかと思われます。今回は、標準属性は設定しません。
  6. ポリシーを設定します。
    1. パスワードの強度:最小文字数や要求する文字種類(数字、特殊文字、大文字、小文字)を設定します。今回はデフォルトのままとします。
    2. ユーザに自己サインアップを許可するかを設定します。管理者のみ許可するか、ユーザに自己サインアップを許可するかを選択します。今回はユーザに自己サインアップを許可します。
    3. 管理者が設定した一時パスワードの有効期限を設定します。デフォルト7日間となっています。今回はデフォルトのままとします。
  7. 多要素認証(MFA)を有効にするかを設定します。設定すると、SMS テキストメッセージまたは時間ベースのワンタイムパスワード (TOTP) をサインイン時に要求します。今回はオフとします。
  8. ユーザのアカウントの回復方法を設定します。E メールと電話、MFA に関する利用対象と関係性の設定を行います。今回はEメールのみを選択します。
  9. 属性の検証(属性のアクティブ確認)について設定します。Eメール電話番号Eメールまたは電話番号検証無しから設定します。今回は、Eメールを選択します。なお、SMS メッセージを送信する場合、SMS に関するロール設定が必要になります。
  10. ユーザに対して送信する E メールに関する設定を行います。
    1. SESリージョン:メールの送信元となるリージョンを選択できます。現時点は米国東部(バージニア)米国西部(オレゴン)欧州(アイルランド)からのみ選択できます。今回は米国東部(バージニア)のままにします。
    2. FROM E メールアドレス ARN:送信元となる E-mail アドレスを設定できます。この設定は、Amazon SES で設定/検証した後のみ選択できるので、FROM アドレスを独自アドレスにする場合、事前に設定とアドレスの検証を行っておく必要があります。今回はデフォルトのままにします。
    3. FROM Eメールアドレス:(未検証のアドレスで)任意の値を設定することもできるようですが今回は未設定とします。
    4. REPPLY-TO Eメールアドレス:ユーザが返信操作を行った場合の送信先アドレスを設定します。今回は未設定とします。
  11. Amazon SES 設定を通じて E-mail 送信をするかを選択します。はいを選ぶ場合は、FROM EメールアドレスARN(検証された FROM アドレスの設定)が必須になります。いいえの場合、Cognito が送信します。今回はいいえ(デフォルト)を選択します。
  12. Eメール検証メッセージのカスタマイズ設定を行います。検証タイプにはコードリンクの 2 種類があります。またEメールの件名Eメールのメッセージのテンプレートが用意されており、内容をカスタマイズすることができます。(コード,URL 部分のみ置換文字列があります)
  13. ユーザ招待メッセージのカスタマイズ設定を行います。Eメールの件名Eメールのメッセージのテンプレートが用意されており、内容をカスタマイズすることができます。(ユーザ名、仮パスワード部分のみ置換文字列があります)
  14. また、以下の内容をオプション項目で設定できます。
    1. タグ:ユーザープールに対してタグキータグ値を設定することができます。
    2. デバイス:ユーザのデバイスを記憶するかを設定します。(デフォルト:いいえ
    3. アプリクライアント:このユーザプールにアクセスにくるアプリクライアント(サービスやアプリ)を追加する場合に設定します。作成後から追加できます。
      1. 任意のアプリクライアント名を設定します。(例:testapp
      2. トークン、アクセストークン、ID トークンの有効期限を設定します。
      3. クライアントシークレットを作成するかを選択します。クライアントシークレットを持たせない構成の場合チェックを外します。今回はチェックを入れたままにします。
      4. 認証フローで利用可能な認証方法を設定します。以下のような内容から選択が可能です。
        1. 認証用の管理 API のユーザー名パスワード認証を有効にする (ALLOW_ADMIN_USER_PASSWORD_AUTH):サーバ側の管理 API で認証を行う場合はチェックを入れます。今回はチェック無しとします。(デフォルトチェック無し)
        2. Lambda トリガーベースのカスタム認証を有効にする (ALLOW_CUSTOM_AUTH):認証イベントに応じて Lambda でトリガーを作ってカスタム認証をする場合設定を入れます。今回はチェックを入れたままにします。(デフォルトチェックあり)
        3. ユーザー名パスワードベースの認証を有効にする (ALLOW_USER_PASSWORD_AUTH):SRP を使用せずパスワードベースでの認証を可能とする場合設定します。サーバサイド側から認証を実行する場合、必要になるかと思います。今回は今回はチェック無しとします。(デフォルトチェックなし)
        4. SRP (セキュアリモートパスワード) プロトコルベースの認証を有効にする (ALLOW_USER_SRP_AUTH):クライアントからの SRP プロトコルでの認証を実行する場合選択します。今回はチェックを入れたままにします。(デフォルトチェックあり)
        5. 更新トークンベースの認証を有効にする (ALLOW_REFRESH_TOKEN_AUTH):デフォルトオンで変更ができません。
      5. セキュリティ設定で、ユーザ存在エラーを防ぐかを設定します。デフォルトは有効(推奨)となっています。そのままとします。
      6. 高度なトークン設定でトークンの取り消しを有効化するかを設定します。デフォルトはチェックありです。そのままとします。
      7. 属性の読み込みおよび書き込みアクセス権限で、必要に応じて、属性単位で読み取り/書き込みの制御を設定します。
      8. アプリクライアントを作成します。
      9. アプリクライアントIDクライアントシークレットが表示されるので控えておきます。
    4. トリガーで AWS Lambda 関数を使用してカスタマイズをする場合の設定ができます。次のケースで呼び出す Lambda 関数を設定できます。(管理画面の説明そのままですが何が設定できるかを示すために書いておきます)
      1. サインアップ前:このトリガーは、ユーザーがサインアップのための情報を送信すると呼び出され、サインアップのリクエストを承諾または拒否するカスタム検証を実行できるようにします。
      2. 認証前:このトリガーは、ユーザーが認証のための情報を送信すると呼び出され、サインインのリクエストを承諾または拒否するカスタム検証を実行できるようにします
      3. カスタムメッセージ:このトリガーは、検証メッセージまたは MFA メッセージの送信前に呼び出され、メッセージを動的にカスタマイズできるようにします。静的なカスタムメッセージは、検証パネルで編集できます。
      4. 認証後:このトリガーは、ユーザーの認証後に呼び出され、カスタムロジック (分析用など) を追加できるようにします。
      5. 確認後:このトリガーは、ユーザーの確認後に呼び出され、カスタムメッセージの送信やカスタムロジック (分析用など) の追加ができるようにします。
      6. 認証チャレンジの定義:このトリガーは、カスタム認証フローを開始するために呼び出されます。
      7. 認証チャレンジの作成:このトリガーは、認証チャレンジの定義]トリガーの一部としてカスタムチャレンジが指定されている場合に、認証チャレンジの定義 の後で呼び出されます。
      8. 認証チャレンジレスポンスの確認:このトリガーは、カスタム認証チャレンジに対するエンドユーザーからのレスポンスが有効かどうかを確認するために呼び出され
      9. ユーザーの移行:このトリガーはサインインオペレーションおよびパスワードを忘れた場合のオペレーション中に呼び出され、ユーザーを既存のディレクトリからこのユーザープールに移行します
      10. トークン生成前:このトリガーは、トークンの生成前に呼び出され、ID トークンのクレームをカスタマイズできるようにします。
  15. 確認画面で、プールの作成を押下します。
  16. アプリの統合-アプリクライアントの設定を押下します。
    1. 有効なIDプロバイダからCognito User Poolを選択します。
    2. サインインとサインアウトのURLで、作成するサイトのコールバックURLに認証後の戻り先の URL を設定します。またログアウトした際の遷移先 URL をサインアウトURLに設定します。カンマ区切りで複数設定できます。
    3. OAuth 2.0設定で許可されているOAuthフローを選択します。ここでは、Authorization code grantを選択します。
      1. Authorization code grant:認可コードを発行して、その検証を行うフローを組む場合、こちらを選択します。
      2. Implicit grant:認証後すぐアクセストークンを発行して、そのままアプリに渡す場合こちらを選択します。
      3. Client credentials:クライアントアプリとの直接のやり取りで、アプリ自体のためのアクセストークンを発行します。このフローはユーザに対する認証としては使用しません。
    4. 許可されているOAuthスコープを選択します。phoneemailopenidaws.cognito.signin.user.adminprofileから選択できます。今回はemailopenidaws.cognito.signin.user.adminを選択します。なおaws.cognito.signin.user.admin スコープは、UpdateUserAttributes および VerifyUserAttribute などのアクセストークンを必要とする Amazon Cognito ユーザープール API 操作へのアクセス権を付与します。
    5. ここまで設定するとホストされたUIを起動が選べるようになり、自動生成されたログイン画面を使用してみることができます。(独自画面は後述します)
  17. アプリの統合-ドメイン名を選択します。
    1. Amazon Cognito ドメインか、自分が所有するドメインを設定できます。今回は自分が所有するドメインを使用します。(Amazon Cognito ドメインを使用する場合、ドメインのプレフィックスを設定します。この値はグローバルで一意である必要があります)なお、ここで注意事項があり、ACM の SSL 証明書は、リージョンがバージニア北部である必要があります。
    2. 自分が所有するドメインドメインの使用ボタンを押下します。
    3. ドメイン名に使用するドメインを設定します。(例:auth.example.com
    4. ACMマネージド証明書バージニア北部リージョンで登録した ACM 証明書から選択します。
    5. 変更の保存を押下します。
    6. エイリアスターゲットが表示されるので、Route53ホストゾーン設定に進みます。
      1. 対象のドメインを選択し、レコードを作成を押下します。
      2. レコード名に入力したサブドメイン部(例:auth)を設定します。
      3. レコードタイプはAとします。
      4. トラフィックのルーティング先エイリアスを選択します。
      5. CloudFrontディストリビューションへのエイリアスを選択します。
      6. 表示されたエイリアスターゲットの値を設定します。
      7. レコードを作成を押下します。
    7. ドメインが有効になるまで 15 分ほどかかります。
    8. アプリの統合-ドメイン名画面をリロードして、ドメインのステータスAvtiveになれば成功です。

ログイン・会員登録画面(Hosted UI)を呼び出す実装例

Cognito によって生成された画面を呼び出すために、呼び出し元画面(ログイン前画面)と、コールバック画面(完了画面)を作成します。以下は、Next.js(TypeScript)で作成した例です。実際にはstateの発行や検証を行ったほうが良いです。

呼び出し元画面(ログイン前画面)
const Start = () => {
  const congitoDomain = "<Your Domain>";
  const redirectUri = "<Redirect Uri>";
  const clientId = "<Client Id>";

  async function startLogin() {
    location.href = `https://${congitoDomain}/oauth2/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}`;
  }

  return (
    <>
      <h1>This is start page</h1>
      <div>
        <button onClick={startLogin}>Start login</button>
      </div>
    </>
  );
};

export default Start;

コールバック画面(完了画面)
import { GetServerSideProps } from "next";

type Props = {
  access_token?: string;
  id_token?: string;
  refresh_token?: string;
  error?: string;
};

const LoginComplete = (props: Props) => {
  return (
    <>
      <h1>This is callback page</h1>
      <div>access_token: {props.access_token}</div>
      <div>id_token: {props.id_token}</div>
      <div>refresh_token: {props.refresh_token}</div>
      <div>error: {props.error}</div>
    </>
  );
};
export default LoginComplete;

export const getServerSideProps: GetServerSideProps = async (context) => {
  const congitoDomain = "<Your Domain>";
  const redirectUri = "<Redirect Uri>";
  const clientId = "<Client Id>";
  const clientSecret = "<Client Secret>";

  const code = context.query.code;

  let props: Props = {};
  if (typeof code === "string") {
    const headers: HeadersInit = {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization:
        "Basic " +
        Buffer.from(`${clientId}:${clientSecret}`).toString("base64"),
    };
    const request: RequestInit = {
      method: "POST",
      headers: headers,
      body:
        `grant_type=authorization_code&client_id=${clientId}&redirect_uri=${redirectUri}&code=` +
        code,
    };
    const response = await fetch(
      `https://${congitoDomain}/oauth2/token`,
      request
    );
    try {
      const json = await response.json();
      console.log(json);
      if ("error" in json) {
        props = {
          error: json.error,
        };
      } else {
        props = {
          access_token: json.access_token,
          id_token: json.id_token,
          refresh_token: json.refresh_token,
        };
      }
    } catch (e) {
      console.log(e);
    }
  }

  return {
    props: props,
  };
};

<Your Domain>:Cognito で発行されたドメイン(xxx.auth.ap-northeast-1.amazoncognito.com)または、cognito に設定した独自ドメインを設定します。
<Redirect Uri>:コールバック画面(完了画面)の URL を設定します。ここは、Cognito 側の設定とそろっていれば、http://localhost:3000/等でも問題ありません。Cognito 側で行った設定とそろっていないとエラーになります。
<Client Id>:Cognito 管理画面でひかえたクライアント ID を設定します。
<Client Secret>:Cognito 管理画面でひかえたクライアントシークレットを設定します。

結果及びまとめ

呼び出し元画面(ログイン前画面)のほうで、Start loginボタンを押下すると、Cognito 側の HostedUI のログイン画面に遷移し、会員登録やログインを行うと、コールバック画面(完了画面)画面に遷移して取得したアクセストークンなどが表示されます。(実際には、安全に保管・管理してください)

なお、今回は認証フローがAuthorization code grantかつクライアントシークレットありの構成としています。
この構成は基本的に、完了画面側で、サーバサイド側からトークンエンドポイントをコールすることで最終的にアプリが認証状態となる構成です。(サーバサイド実装が存在することが前提となります)

クライアントシークレットが保持できない場合には、Authorization code grantクライアントシークレットなしの構成とするか、そもそも Cognito 側の画面で認証が完了したら、直接アクセストークンなどをパラメータで受け取る、Implicit grantという構成をとる必要が出てきます。(SPA 構成の場合は、サーバ機能(サーバサイド処理をできる場所)がない場合、こちらの構成を取る必要が出てくるかと思います)

Authorization code grantはサーバ側で、公開されないクライアントシークレットを隠蔽していることで、セキュリティ強度が高くなります。
Implicit grantの場合は、コールバック画面(完了画面)の URL の Get パラメータに直接 TOKEN が出力され、それをそのまま(トークンエンドポイントはコールせずに)受け取る構成になります。そのため、URL の GET パラメータからトークンを漏洩させないよう慎重に実装する必要があります。

また、アクセストークンなどを、グローバルスコープからアクセス可能な場所に保管しないといった考慮が必要になってきます。なお、Implicit grantの場合は、refresh_tokenは払い出されません。

Cognito を導入する場合、途中から変えるのが難しい内容も多いため、まず先に、どのようなキー項目でユーザを認証するのかおよび、認証フローに関する方針を明確にしておく必要があります。

独自 UI を実装する場合

Hosted UI は CSS などは多少変えられますが、言語は英語のみであったり、ユーザに提供するには少し UI/UX 上のハードルが高いです。独自 UI を実装する場合、SDK を使って実装していくことになります。

https://github.com/aws-amplify/amplify-js/tree/main/packages/amazon-cognito-identity-js

SDK を用いての実装配下を参考にしてください。

https://zenn.dev/ttani/articles/aws-cognito-sdk

Discussion