🧸

Amplifyを使わずにCognitoを利用する

2023/05/17に公開

AWSのAmplifyを使用せずにCognitoの認証を使用する記事が意外となかったので投稿します。
UIコンポーネントも日本語化が柔軟にできないので個別画面にCognito認証を実装する想定です。

公式Docで言うOption 2: Call Authentication APIs manuallyですね。

amplify-jsの公式Docに実装方法を参考にしています。
詳しくはそちらを参考にしてください。

(フロントエンド初心者なので誤っているところは指摘していただけると嬉しいです)

1. amplifyのインストール

公式Docを参考にインストールします。

npm install -g @aws-amplify/cli

公式ドキュメント(amplify-jsのインストール)
https://docs.amplify.aws/cli/start/install/

2. JavaScriptへの適用

amplify-jsはNode.jsの実装があるので、JavaScriptで利用する場合はいくつか対応が必要です。

2-1 global is not definedへの対応

"global"はJavaScriptでは定義されていないので、"global"に"window"を対応させます。

vite.config.js
export default defineConfig({
  // 省略
  define: {
    global: 'window',
  },
})

2-2 "request" is not exportedへの対応

以下の設定も追加します。

vite.config.js
export default defineConfig({
  // 省略
  resolve: {
    alias: {
      "./runtimeConfig": "./runtimeConfig.browser",
    },
  },
})

3. cognito.jsの実装

普通に各メソッドをexportしてもいいですが、Amplify.configureが何度も呼ばれるのも嫌な感じなのでクラスにしてます。

十分にテストしていないので気が付いたら直す予定です。忘れていなければ。たぶん。

ちなみに、デフォルトの設定ではトークン情報をLocalStorageに書き込んでるそうなので、わざわざシングルトンにする必要はないです。

https://qiita.com/ikegam1/items/620b9d359ccf60350b1d

※これはViteの環境変数なので適宜変更してください。
import.meta.env.VITE_XXXX

cognito.js
import { Amplify, Auth, Hub } from 'aws-amplify';

class Cognito {
  constructor() {
    Amplify.configure({
      Auth: {
        // REQUIRED only for Federated Authentication - Amazon Cognito Identity Pool ID
        identityPoolId: import.meta.env.VITE_COGNITO_USER_POOL_ID,

        // REQUIRED - Amazon Cognito Region
        region: import.meta.env.VITE_COGNITO_REGION,
    
        // OPTIONAL - Amazon Cognito User Pool ID
        userPoolId: import.meta.env.VITE_COGNITO_USER_POOL_ID,
    
        // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string)
        userPoolWebClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,

        // OPTIONAL - This is used when autoSignIn is enabled for Auth.signUp
        // 'code' is used for Auth.confirmSignUp, 'link' is used for email link verification
        signUpVerificationMethod: "code", // 'code' | 'link'
    
        // OPTIONAL - Hosted UI configuration
        oauth: {
          domain: "your_cognito_domain",
          scope: [
            "email",
            "openid",
            "aws.cognito.signin.user.admin",
          ],
          redirectSignIn: "https://xxxxxxxx.jp/",  //サインイン後のリダイレクト先
          redirectSignOut: "https://xxxxxxxx.jp/",  //サインアウト後のリダイレクト先
          responseType: "code", // or 'token', note that REFRESH token will only be generated when the responseType is code
        },
      },
    });
  }

  public async signUp({username, password, email}) {
    try {
      const { user, userConfirmed, userSub } = await Auth.signUp({
        username,
        password,
        attributes: {
          email,          // optional
        },
        autoSignIn: { // optional - enables auto sign in after user is confirmed
          enabled: true,
        }
      });
      return user;
    } catch (error) {
      error.message_jp = this.__getJpErrMessage(error, "新規登録");
      throw error;
    }
  }

  public async resendConfirmationCode({username}) {
    try {
      await Auth.resendSignUp(username);
      console.log('code resent successfully');
    } catch (error) {
      error.message_jp = this.__getJpErrMessage(error, "検証コード再送");
      throw error;
    }
  }

  public async confirmSignUp({username, code}) {
    try {
      await Auth.confirmSignUp(username, code);
    } catch (error) {
      error.message_jp = this.__getJpErrMessage(error, "仮登録認証");
      throw error;
    }
  }

  public listenToAutoSignInEvent() {
    Hub.listen('auth', ({ payload }) => {
      const { event } = payload;
      if (event === 'autoSignIn') {
        const user = payload.data;
        // assign user
      } else if (event === 'autoSignIn_failure') {
        // redirect to sign in page
      }
    })
  }

  public async signIn({username, password}) {
    try {
      const user = await Auth.signIn(username, password);
      return user;
    } catch (error) {
      error.message_jp = this.__getJpErrMessage(error, "ログイン");
      throw error;
    }
  }

  public async isSignIn(): Promise<boolean> {
    try {
      const user = await Auth.currentAuthenticatedUser();
      return true;
    } catch (error) {
      return false;
    }
  }

  public async signOut() {
    try {
      await Auth.signOut({ global: false });
    } catch (error) {
      console.log('error signing out: ', error);
      error.message_jp = this.__getJpErrMessage(error, "ログアウト");
      throw error;
    }
  }

  public async getAccessToken() {
    try {
      const user = await Auth.currentAuthenticatedUser();
      const accessToken = user.signInUserSession.accessToken.jwtToken;
      return accessToken;
    } catch (error) {
      console.log('Failed to get the access token', error);
      error.message_jp = this.__getJpErrMessage(error, "ログイン情報取得");
      throw error;
    }
  }

  public async changePassword({oldPassword, newPassword}) {
    try {
      const user = await Auth.currentAuthenticatedUser();
      const data = await Auth.changePassword(user, oldPassword, newPassword);
    } catch (error) {
      console.log('Failed to change the password', error);
      error.message_jp = this.__getJpErrMessage(error, "パスワード変更");
      throw error;
    }
  }

  public async sendForgotPassword({username}) {
    try {
      await Auth.forgotPassword(username);
    } catch (error) {
      console.log('Failed to send the ForgotPassword Mail', error);
      error.message_jp = this.__getJpErrMessage(error, "リセットメール送信");
      throw error;
    }
  }

  public async validForgotPasswordCode({username, code, new_password}) {
    try {
      await Auth.forgotPasswordSubmit(username, code, new_password);
    } catch (error) {
      console.log('Failed to valid the ForgotPassword Code', error);
      error.message_jp = this.__getJpErrMessage(error, "コード検証");
      throw error;
    }
  }

  private __getJpErrMessage (error, procName) {
    switch (error.code) {
        case 'UserNotConfirmedException':
            // ユーザのステータスが UNCONFIRMED の場合に起こる。
            // SignUp用のコードを再送し、ステータスを CONFIRMED にする必要がある。
            // 検証コードの再送は 1.3節の ResendConfirmationCode() を参照。
            return 'メールアドレスの検証が完了していません。';
        case 'PasswordResetRequiredException':
            // Cognito コンソールでパスワードをリセット(ユーザープールにユーザをインポートする場合も含む)した場合に起こる。
            // パスワードをリセットする必要がある。
            // パスワードのリセットは 3.1節の SendForgotPasswordCode() 参照。
            return 'パスワードをリセットする必要があります。';
        case 'NotAuthorizedException':
            // 誤ったパスワードを入力した場合に起こる。
            // 注) パスワードを間違え続けた場合にも起こり、 error.message が 'Password attempts exceeded' になる。
            if (error.message == 'Password attempts exceeded') {
                return 'パスワードの試行回数が上限を超えたため、一時的に認証できなくなりました。';
            } else {
                return 'パスワードが誤っています。';
            }
        case 'UserNotFoundException':
            // PASSWORD_VERIFIER は通るものの username が Cognito ユーザープールに存在しない場合に起こる。
            return 'パスワードが誤っています。';
        case 'InvalidPasswordException':
            return 'パスワードは8文字以上の英数字を入力ください。大文字の英字、小文字の英字、数値を含める必要があります。';
        case 'InvalidParameterException':
            return 'パスワードを入力してください。';
        case 'LimitExceededException':
            return '試行回数が所定の回数を超えました。時間を置いて再度送信してください。';
        case 'CodeMismatchException':
            return '検証コードが誤っています。または有効期限が切れました。';
        case 'NetworkError':
          return 'インターネット接続に失敗しました。';
        default:
          // その他のエラー
          return procName + '処理でエラーが発生しました。(' + error.code + ':' + error.message + ')';
    }
}

}

const cognitoAuth = new CognitoAuth();
export default cognitoAuth;

参考にした公式Doc

https://docs.amplify.aws/lib/auth/getting-started/q/platform/js/

Discussion