🤒

Amplify SDK のAuthライブラリ解説

2022/02/23に公開約8,000字

Authまわり知見貯まるまでは結構むずいので僕のわかる範囲で解説していきます。

公式リファレンス

https://aws-amplify.github.io/amplify-js/api/classes/authclass.html

Authライブラリはamazon-cognito-identity-jsのラッパーライブラリ

Authライブラリはamazon-cognito-identity-jsのラッパーライブラリです。こちらの機能をamplify向けにインターフェース変えたものです。と解説しようと思ったのですが、GitHubのレポジトリはamplify-jsに統合したみたいですね...

https://www.npmjs.com/package/amazon-cognito-identity-js

フロントエンド用のAPIとバックエンド用のAPI

amplifyの他にAWS SDKにはCognitoライブラリが提供されています。以下のドキュメントと先ほどあげた公式リファレンスを比べてみてください。

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-cognito-identity-provider/index.html

同じ内容が書いてあることに気づきましたでしょうか?違いといえばAWS SDKのCognitoライブラリのほうには、admin???というメソッドが追加されていることですね。

admin???というメソッドは、アドミン向けの機能で強制的にその処理を実行したり、パスワードを書き換えるなどの危険な処理を実行可能です。

この違いがあるのは、amplifyのAuthライブラリはフロントエンド向けに作られており、AWS SDKのCognitoライブラリはバックエンド向けに作られているからです。フロントエンドは基本的にクライアントサイドでプログラムを実行するためAWSのクレデンシャルキーを置くのは危険です。危険な処理はバックエンドを通して行うようにしましょう。

Authライブラリ解説

ユースケース

  • ログインユーザー情報(セッション)確認
  • サインアップ
  • サインアップ確認
  • ログイン
  • 強制パスワード変更
  • ログアウト
  • メールアドレス変更
  • メールアドレス検証
  • パスワード変更
  • パスワード検証

ログインユーザー情報(セッション)確認 ~userSession~

currentAuthenticatedUserで現在のユーザー情報を取得し、userSessionでセッションを取得します。セッションのisValid()を呼べば、セッションが有効かどうかがわかります。

import { Auth, Logger, I18n } from 'aws-amplify';

const currentAuthUser = await Auth.currentAuthenticatedUser();

const session = await Auth.userSession(currentAuthUser);

if (!session?.isValid()) {
  console.error('セッションが無効です!')
};
}

サインアップ ~signUp~

フロントエンド(amplify auth)で行うかバックエンド(aws-sdk cognito)で処理がわかれます。

仮パスワードを作って次回ログインしたときに強制変更してもらう場合はフロントエンドで行いましょう。パスワードも最初に決めたい場合はバックエンドで行いましょう。

フロントエンド

[Auth.signUp](https://aws-amplify.github.io/amplify-js/api/classes/authclass.html#signup)を使います。2段階認証を有効にしていると処理がややこしくなります。

const signUp = ({ username, password }) => {
  Auth.signUp(username, password)
    .then((data) => {

      // 2段階認証が必要かどうか。
      if (
        typeof data.userConfirmed !== 'undefined' &&
        data.userConfirmed === false
      ) {
        // 2段階認証が必要。
      } else {
        // サインアップ成功! ログイン画面へリダイレクトなど。
      }
    })
    .catch((err) => {
      logger.error('Auth.signUp() err:', err);
    });
}

バックエンド

まずは、axiosなどで自分のバックエンドのAPIを叩きます。
バックエンド内の処理はこんな感じ。

import AWS from 'aws-sdk';

AWS.config.update({
  region: process.env.AWS_REGION,
});
const cognito = new AWS.CognitoIdentityServiceProvider({
  apiVersion: process.env.COGNITO_API_VERSION,
});

type SignUpUser = { email: string; password: string };

const signUp = (user: SignUpUser): Promise<void> => {
  return new Promise((resolve, reject) => {
    const userName = user.email;
    const attributes = [];
    attributes.push({ Name: 'email', Value: user.email });

    return cognito
      .signUp({
        ClientId: process.env.COGNITO_CLIENT_ID || '',
        Username: userName,
        UserAttributes: attributes,
        Password: user.password || 'password',
      })
      .promise()
      .then(() => {
        logger.debug('SignUp success');
        resolve();
      })
      .catch((err) => {
        logger.error(
          `登録エラーが発生しました。詳細: ${err.message}`,
        );
        reject(
          new Error(
            `登録エラーが発生しました。詳細: ${err.message}`,
          ),
        );
      });
  });
};

サインアップ確認 ~signUpConfirm~

フロントエンド(amplify auth)で行うかバックエンド(aws-sdk cognito)で処理がわかれます。

メールアドレスをcognitoだけで管理している場合はフロントエンドで行いましょう。バックエンドでも管理している場合は、バックエンドでサインアップ確認処理を行いメールアドレスを更新するか、もしくはフロントエンドでサインアップ確認処理を行いCognitoのカスタムトリガーでDBのメールアドレスを非同期更新しましょう。

フロントエンド

const confirmSignUp = ({ userName, code }) => {
  return (dispatch) => {
    // confirmSignUp (cognito)
    Auth.confirmSignUp(userName, code)
      .then((data) => {
        // 成功処理
      })
      .catch((err) => {
        logger.error(
          'Auth.confirmSignUp() err:',
          err,
        );

        // エラー処理
      });
  };
};

バックエンド

まずは、axiosなどで自分のバックエンドのAPIを叩きます。
バックエンド内の処理はこんな感じ。

import AWS from 'aws-sdk';

AWS.config.update({
  region: process.env.AWS_REGION,
});
const cognito = new AWS.CognitoIdentityServiceProvider({
  apiVersion: process.env.COGNITO_API_VERSION,
});

const signUpConfirm = (email: string, code: string): Promise<void> => {
  return new Promise((resolve, reject) => {
    const userName = user.email;
    const attributes = [];
    attributes.push({ Name: 'email', Value: user.email });

    return cognito.confirmSignUp(
      {
        ClientId: process.env.COGNITO_CLIENT_ID || '',
        Username: email,
        ConfirmationCode: code,
      },
      .promise()
      .then(() => {
        logger.debug('SignUp success');
        resolve();
      })
      .catch((err) => {
        logger.error(
          `登録エラーが発生しました。詳細: ${err.message}`,
        );
        reject(
          new Error(
            `登録エラーが発生しました。詳細: ${err.message}`,
          ),
        );
      });
  });
};

ログイン ~singIn~

2段階認証を有効にしているとchallengeNameで2段階認証が要求されているかどうかチェックする必要があります。
仮パスワードを有効にしていると、challengeNameで新規パスワードが要求されているかどうかチェックする必要があります。ここで引っかかった場合は後の強制パスワード変更処理が必要です。

const login = ({ username, password }) => {
    // signIn (cognito)
  Auth.signIn(username, password)
    .then((data) => {

      // data.challengeNameをチェックする
      // MFA_REQUIRED or SMS_MFA 2段階認証が必要
      // NEW_PASSWORD_REQUIRED 強制パスワード変更
      // その他 ログイン成功!
      if (data.challengeName === 'NEW_PASSWORD_REQUIRED') {
	// 強制パスワード変更処理へ
      } else if (
        data.challengeName === 'MFA_REQUIRED' ||
           data.challengeName === 'SMS_MFA'
      ) {
        // 2段階認証処理
      } else {
        // ログイン処理
      }
    })
    .catch((err) => {
      logger.error('Auth.signIn() err:', err);
    });
};

強制パスワード変更 ~completeNewPassword~

2段階認証を有効にしているとややこしくなります。

const setNewPassword = async ({
  userName,
  password,
  newPassword,
} => {
  const user = await Auth.signIn(userName, password);

  // completeNewPasswordの返り値
  // 1. MFA認証が必要な場合 SMS_MFA or MFA_REQUIRED
  // 2. その他 認証済みユーザー
  const data = await Auth.completeNewPassword(user, newPassword, {});

  if (
    data.challengeName === 'MFA_REQUIRED' ||
    data.challengeName === 'SMS_MFA'
  ) {
    // 2段階認証処理
  }
  
  // 成功処理
}

ログアウト ~signOut~

一番簡単かも

const logout = () => {
  return Auth.signOut();
}

メールアドレス変更 ~currentAuthenticatedUser~

専用のAPIはなくupdateUserAttributesでemailを変更します。すると変更コード番号が変更先メールに届きます。

途中でいろいろコメントしてありますが、cognitoは結構おおきな欠陥があって、メールアドレス変更リクエストを送った時点で、検証成功もしていないのに、勝手にメアドが書き換わってしまいます。恐ろしいですね。。

const changeEmail = async ({ email }) => {
  const user = await Auth.currentAuthenticatedUser();
  // NOTE: デフォルトの挙動だと検証する前にメールアドレスを変更してしまうので、
  // その対応としてcustome:verified_emailに古いメアドを指定する。詳しくは以下のリンクを参照。
  // https://zenn.dev/dove/articles/78ecf08b51ee0c
  await Auth.updateUserAttributes(user, {
    email,
    'custom:verified_email': user.attributes.email, // 上記対応をしなければ不要
  })
}

メールアドレス検証 ~verifyUserAttributeSubmit~

const verifyEmail = async ({
  code,
  email,
}) => {
  const user = await Auth.currentAuthenticatedUser();
  const result = await Auth.verifyUserAttributeSubmit(user, 'email', code);
  if (result === 'SUCCESS') {
    // NOTE: デフォルトの挙動だと検証する前にメールアドレスを変更してしまうので、
    // その対応としてcustome:verified_emailに古いメアドを指定する。詳しくは以下のリンクを参照。
    // https://zenn.dev/dove/articles/78ecf08b51ee0c

    // 上記対応をしないのであればこの処理は不要
    await Auth.updateUserAttributes(user, {
      email, // ここと
      'custom:verified_email': email, // ここに新しいメールアドレスをいれる。
    });

  } else {
    throw new Error('メールアドレス検証に失敗しました!');
  }
}

パスワード変更 ~forgotPassword~

こちらも簡単ですね。userNameはメールアドレスでのログインをしているのであれば、メールアドレうが入ります。そのメアド宛にコード番号が届くので、後のパスワード検証で、新しいパスワードと共にこのコードを渡してあげます。

const forgotPassword = async (userName: string) => {
  await Auth.forgotPassword(userName);
}

パスワード検証 ~forgotPasswordSubmit~

const confirmForgotPassword = async ({
  userName,
  code,
  newPassword,
}) =>  {
  await Auth.forgotPasswordSubmit(userName, code, newPassword);
}

Discussion

ログインするとコメントできます