あるSEのつぶやき・改

ITやシステム開発などの技術に関する話題を、取り上げたりしています。

React+AmplifyのCognito認証画面に独自カスタマイズした画面を使用する方法

はじめに

Amplify + Cognito のサインイン画面は堅牢でよくできているのですが、いかんせんデザインの自由度が低いです。

f:id:fnyablog:20190921213130p:plain:w480

この記事では、 Amplify の API を直接叩いてサインインする方法をご紹介します。

準備

Amplify のプロジェクトの作成方法と、プロジェクトに Auth 機能を追加する方法は、下記記事を参考にしてください。

www.aruse.net

出来上がりの画面

出来上がりの画面は、サインイン、リセット、サインアウト、ユーザー名の取得機能を持っているものとします。

f:id:fnyablog:20190921213713p:plain:w320

ソースコード

全体のソースコードは以下のようになります。

import Amplify, { Auth } from 'aws-amplify';
import React, { useState } from 'react';
import * as aws_exports from './aws-exports';
import './App.css';

Amplify.configure(aws_exports.default);
// Amplify.Logger.LOG_LEVEL = 'DEBUG';

const App: React.FC = () => {
  const [username, setUserName] = useState('');
  const [password, setPassword] = useState('');
  const [message, setMessage] = useState('');

  const onSignOut = (e: React.MouseEvent) => {
    // フォーム送信を抑止
    e.preventDefault();
    
    Auth.signOut();
    setMessage('');
  }

  const getUserName = async(e: React.MouseEvent) =>{
    // フォーム送信を抑止
    e.preventDefault();

    try { 
      const user = await Auth.currentAuthenticatedUser();
      setMessage(`ユーザー名は${user.username}です`);
    } catch(err) {
      if (err === 'not authenticated') {
        setMessage('サインインしていません');
      } else {
        setMessage('予期しないエラーが発生しました');
        console.error(err);
      }
    }
  }

  const resetForm = (e: React.MouseEvent)  => {
    // フォーム送信を抑止
    e.preventDefault();

    // Form要素を取得
    const form:HTMLFormElement | null = document.getElementById('signInForm') as HTMLFormElement;
    form!.reset(); // nullを無視してリセット

    // stateもクリア
    setUserName('');
    setPassword('');
    setMessage('');
  }

  const onSignIn = async (e: React.MouseEvent) => {
    // フォーム送信を抑止
    // Without below, Auth.signIn returns NetworkError.
    e.preventDefault();

    if (username === '') {
      setMessage('ユーザー名が入力されていません');
      return;

    // [Fix]UnexpectedLambdaException:null invocation failed due to configuration.
    } else if (password === '') {
      setMessage('パスワードが入力されていません');
      return;
    } else {
      setMessage('');
    }

    try {
      const user = await Auth.signIn(username, password);

      // ref. https://aws-amplify.github.io/docs/js/authentication#customize-your-own-components
      if (user.challengeName === 'SMS_MFA' ||
        user.challengeName === 'SOFTWARE_TOKEN_MFA') {
        // Do something...
      } else if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
        // Do something...
      } else if (user.challengeName === 'MFA_SETUP') {
          // Do something...
      } else {
          setMessage('ログインしました');
          console.log(user);
      }
    } catch (err) {
        if (err.code === 'UserNotConfirmedException') {
            setMessage('ユーザー登録の途中です');
            console.error('UserNotConfirmedException');
        } else if (err.code === 'PasswordResetRequiredException') {
            setMessage('パスワードを変更する必要があります');
            console.error('PasswordResetRequiredException');
        } else if (err.code === 'NotAuthorizedException') {
            setMessage('ユーザー名またはパスワードが異なります');
            console.error('NotAuthorizedException');
        } else if (err.code === 'UserNotFoundException') {
            setMessage('ユーザー名またはパスワードが異なります');
            console.error('UserNotFoundException');
        } else {
            setMessage('予期しないエラーが発生しました');
            console.error(err);
            console.error('Unknowned error');
        }
    }
  }

  return (
    <div>
      <h3>{message}</h3>
      <form id="signInForm">
        UserName:<input type="text" onChange={(e) => setUserName(e.target.value)}/><br/>
        Password:<input type="password" onChange={(e) => setPassword(e.target.value)} /><br/>
        <button onClick={(e) => onSignIn(e)}>Sign In</button>
        <button onClick={(e) => resetForm(e)}>Reset</button>
        <button onClick={(e) => onSignOut(e)}>Sign Out</button>
        <button onClick={(e) => getUserName(e)}>Get UserName</button>
      </form>
    </div>
  );
}

export default App;

解説

ハマりどころはたくさんあるのですが、一番最初にハマるのはフォームデータの受け渡しに React Hooks を使っているところでしょうか。

最近の react-create-app のデフォルトでは、クラスコンポーネントではなく関数コンポーネントで App.tsx が作成されるようになっています。そのため、そのまま開発しようとしたら値の引き回しに React Hooks という仕組みを使用する必要があります。

ユーザー名に着目して、React Hooks を見てみましょう。

  const [username, setUserName] = useState('');

const[変数名, 変数に値をセットする関数] を定義して、useStateの引数で変数の初期値を設定します。変数の値を変更する場合は、この場合は必ずsetUserNameを使用する必要があります。

変数を使用する場合は、直接変数名を記述できます。

      const user = await Auth.signIn(username, password);

次にフォームの値を取得する方法ですが、React.MouseEventのオブジェクトから値を取得します。

具体的には、フォームのHTML(JSX)で以下のように記述します。

        UserName:<input type="text" onChange={(e) => setUserName(e.target.value)}/><br/>

onChangeイベントで、イベントのオブジェクトを引数(e)にして、フォームの値をe.target.valueで取得しています。

onSignInメソッドの最初に以下の記述がありますが、これは<form>タグに囲まれている<button>タグにイベントを記述しているため、イベントの実行後にフォームの送信が行われることを防いでいます。

    // フォーム送信を抑止
    // Without below, Auth.signIn returns NetworkError.
    e.preventDefault();

resetFormメソッドでdocument#getElementByIdを使用していますが、TypeScript だとそのままでは使えずHTMLFormElementにキャストする必要があります。また、null である可能性があるので、form.reset()はコンパイルエラーになり、form!.reset()として null でも強制的に reset メソッドを実行しています。

   // Form要素を取得
    const form:HTMLFormElement | null = document.getElementById('signInForm') as HTMLFormElement;
    form!.reset(); // nullを無視してリセット

ここまでが React のお話で、これからが Amplify のお話になります。

なお、Amplify の API を呼び出す方法の詳細については、下記の公式ドキュメントを参照してください。

aws-amplify.github.io

onSignInメソッドのpasswordを必須で聞いているのは、Amplify というか Cognito がユーザー名だけでサインインを受け付けるものの、不具合がありUnexpectedLambdaExceptionの例外が返ってくるためです。このためだけに独自カスタマイズした画面を採用してもよいくらいの不具合だと思っています。

 if (username === '') {
      setMessage('ユーザー名が入力されていません');
      return;

    // [Fix]UnexpectedLambdaException:null invocation failed due to configuration.
    } else if (password === '') {
      setMessage('パスワードが入力されていません');
      return;
    } else {
      setMessage('');
    }

onSignInメソッドのAuth.signInが失敗した場合は例外が返ってくるので、例外に合わせて処理を記述する必要があります。今回のサンプルでは、メッセージを表示後エラーログを出力しています。

    try {
      const user = await Auth.signIn(username, password);
      // 中略

    } catch (err) {
        if (err.code === 'UserNotConfirmedException') {
            setMessage('ユーザー登録の途中です');
            console.error('UserNotConfirmedException');
        } else if (err.code === 'PasswordResetRequiredException') {
            setMessage('パスワードを変更する必要があります');
            console.error('PasswordResetRequiredException');
        } else if (err.code === 'NotAuthorizedException') {
            setMessage('ユーザー名またはパスワードが異なります');
            console.error('NotAuthorizedException');
        } else if (err.code === 'UserNotFoundException') {
            setMessage('ユーザー名またはパスワードが異なります');
            console.error('UserNotFoundException');
        } else {
            setMessage('予期しないエラーが発生しました');
            console.error(err);
            console.error('Unknowned error');
        }
    }

説明はだいたい以上で終わりですね。

あとはソースコードのコメントや公式ドキュメントをあたってみてください。

おわりに

React + Amplify の Cognit 認証画面で独自カスタマイズした画面を使用する方法を調べるには、かなり苦戦しました。

まずは、React Hooks の洗礼です。💦

その次は、Amplify の意味不明なエラーがなかなか解決できませんでした。

ネットの情報もほぼないに等しいので、公式ドキュメントだけが頼りです。

ですが、とっかかりはつかんだので、他の画面の作成も今回ほど苦労はしなさそうですね。