AWS Amplify Reactのサインアップ画面をカスタマイズしてみる

簡単にReactアプリに認証機能を追加することがでるAWS AmplifyとAWS Amplify React 認証画面を自分好みにカスタマイズする方法について調べてみました。
2018.06.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

サーバーレス開発部@大阪の岩田です。 下記のエントリでも紹介されているようにAWS AmplifyとAWS Amplify Reactを使用すると、簡単にReactアプリに認証機能を追加することができます。

Meguro.dev #2 参加レポート AWS Amplify をモブプロで試す #meguro_dev

しかし、実業務で認証機能を利用する際は、AWS Amplify Reactが提供する標準画面だけでは要件を満たせない場合がほとんどだと思います。 そこで、AWS Amplify Reactのサインアップ機能を自分好みにカスタマイズする方法について調べてみました。

環境

下記の環境で検証を行いました。

  • node:v8.10.0
  • create-react-app:1.5.2
  • aws-amplify:0.4.5
  • aws-amplify-react:0.1.51

前準備

まずはConito User Poolを作成します。 下記のテンプレートからCloud Formationで作成します。

AWSTemplateFormatVersion: '2010-09-09'
Description: Create Cognito User Pool
Resources:
  CognitoUserPoolMyUserPool:
    Type: "AWS::Cognito::UserPool"
    Properties:
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
        UnusedAccountValidityDays: 7
      AutoVerifiedAttributes:
        - email
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: false
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: false
      Schema:
        - AttributeDataType: "String"
          DeveloperOnlyAttribute: false
          Mutable: true
          Name: "email"
          StringAttributeConstraints:
            MaxLength: "2048"
            MinLength: "0"
          Required: true
        - AttributeDataType: "String"
          DeveloperOnlyAttribute: false
          Mutable: true
          Name: "address"
          StringAttributeConstraints:
            MaxLength: "2048"
            MinLength: "0"
          Required: true   
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: 
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - UserPoolClient
      GenerateSecret: false
      RefreshTokenValidity: 7
      UserPoolId:
        Ref: CognitoUserPoolMyUserPool
  MyIdentifyPool:
    Type: "AWS::Cognito::IdentityPool"
    Properties:
      IdentityPoolName: MyIdentifyPool
      AllowUnauthenticatedIdentities: true
      CognitoIdentityProviders: 
        - ClientId:
            Ref: UserPoolClient
          ProviderName: 
            Fn::Join:
            - ""
            - - cognito-idp.
              - Ref: "AWS::Region"
              - .amazonaws.com/
              - Ref: CognitoUserPoolMyUserPool
          ServerSideTokenCheck: false
Outputs:
  UserPoolId:
    Description: "User Poll ID"
    Value: 
      Ref: CognitoUserPoolMyUserPool
  UserPoolClient:
    Description: 'User Pool Client ID'
    Value: 
      Ref: UserPoolClient    
  MyIdentifyPool:
    Description: 'Identify Pool ID'
    Value: 
      Ref: MyIdentifyPool

ポイントとしてメールアドレス以外に、住所を必須入力としています。 このテンプレートを使用してAWS CliでCloud Formationのスタック作成を実行します。

aws cloudformation create-stack --stack-name cognito-idp --template-body file://template.yml

これで必要なバックエンドのサービスは準備完了です。

チュートリアルに沿って実装

最初にaws-amplify-reactのチュートリアルに沿って認証機能付きのアプリを作っていきます。 まずはcreate-react-appでアプリのひな形を作成し、amplifyのライブラリをインストールします。

create-react-app amplify-sample
cd amplify-sample
npm install aws-amplify aws-amplify-react

次に、create-react-appで作成した雛形アプリに認証処理を追加します。 index.jsに下記のコードを追記します。

index.js

// ...略
import Amplify from 'aws-amplify';
//先程のCloud FormationのテンプレートからOutputされた値を設定
Amplify.configure({
  Auth: {
      identityPoolId: 'xxxxx',
      region: 'ap-northeast-1',
      userPoolId: 'ap-northeast-1_xxxxxxx',
      userPoolWebClientId: 'xxxxxxxxxxx',
  }
});

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

App.jsを修正し,認証機能を追加します。

import { withAuthenticator } from 'aws-amplify-react';

class App extends Component {
//...略
}
// export default App;
export default withAuthenticator(App);

ここまでできたらnpm startして動作確認してみます。

簡単に認証周りの機能が実装されました。 が、、、住所を必須に設定したCognitoUserPoolをバックに指定しているので、この状態だと一生サインアップできません。

カスタマイズ

ここからが本題です。 Amplify-Reactデフォルトのサインアップ画面をカスタマイズし、入力項目に住所を追加していきます。

まずサインアップ画面のコンポーネントを作成します。 MySignup.jsというファイルに下記のように実装しました。

import React from 'react';

import { Auth, I18n } from 'aws-amplify';
import {
    SignUp,
    FormSection,
    SectionHeader,
    SectionBody,
    SectionFooter,
    InputRow,
    ButtonRow,
    Link,
} from 'aws-amplify-react';


export default class MySignUp extends SignUp {

    signUp() {
        const { username, password, email, phone_number, address } = this.inputs;
        const param = {
            username: username,
            password: password,
            attributes:{
                email: email,
                phone_number: phone_number,
                address: address
            }
        }
        Auth.signUp(param)
            .then(() => this.changeState('confirmSignUp', username))
            .catch(err => this.error(err));
    }

    showComponent(theme) {
        const { hide } = this.props;
        if (hide && hide.includes(SignUp)) { return null; }

        return (
            <FormSection theme={theme}>
                <SectionHeader theme={theme}>{I18n.get('Sign Up Account')}</SectionHeader>
                <SectionBody theme={theme}>
                <InputRow
                        autoFocus
                        placeholder={I18n.get('Username')}
                        theme={theme}
                        key="username"
                        name="username"
                        onChange={this.handleInputChange}
                    />
                    <InputRow
                        placeholder={I18n.get('Password')}
                        theme={theme}
                        type="password"
                        key="password"
                        name="password"
                        onChange={this.handleInputChange}
                    />
                    <InputRow
                        placeholder={I18n.get('Email')}
                        theme={theme}
                        key="email"
                        name="email"
                        onChange={this.handleInputChange}
                    />
                    <InputRow
                        placeholder={I18n.get('Phone Number')}
                        theme={theme}
                        key="phone_number"
                        name="phone_number"
                        onChange={this.handleInputChange}
                    />
                    <InputRow
                        placeholder={I18n.get('address')}
                        theme={theme}
                        key="address"
                        name="address"
                        onChange={this.handleInputChange}
                    />
                    <ButtonRow onClick={this.signUp} theme={theme}>
                        {I18n.get('Sign Up')}
                    </ButtonRow>
                </SectionBody>
                <SectionFooter theme={theme}>
                    <div style={theme.col6}>
                        <Link theme={theme} onClick={() => this.changeState('confirmSignUp')}>
                            {I18n.get('Confirm a Code')}
                        </Link>
                    </div>
                    <div style={Object.assign({textAlign: 'right'}, theme.col6)}>
                        <Link theme={theme} onClick={() => this.changeState('signIn')}>
                            {I18n.get('Sign In')}
                        </Link>
                    </div>
                </SectionFooter>
            </FormSection>
        )
    }
}

aws-amplify-reactのSignUp コンポーネントを継承し、必要な箇所だけオーバーライドする形で実装しています。 まずshowComponentをオーバーライドし、住所用にInputRowコンポーネントを追加しています。

<InputRow
    placeholder={I18n.get('address')}
    theme={theme}
    key="address"
    name="address"
    onChange={this.handleInputChange}
/>

次にsignUpメソッドをオーバーライドします。 基底クラスのSignUpクラスでのsignUpメソッドの実装は下記の通りです。

    signUp() {
        const { username, password, email, phone_number } = this.inputs;
        Auth.signUp(username, password, email, phone_number)
            .then(() => this.changeState('confirmSignUp', username))
            .catch(err => this.error(err));
    }

AuthクラスのsignUpメソッドに

  • ユーザー名
  • パスワード
  • メールアドレス
  • 電話番号

を渡しています。

住所を追加したい場合はどうしたら良いのでしょうか? aws-amplifyのドキュメントを確認すると、下記のサンプルがありました。

import { Auth } from 'aws-amplify';

Auth.signUp({
        username,
        password,
        attributes: {
            email,          // optional
            phone_number,   // optional - E.164 number convention
            // other custom attributes
        },
        validationData: []  //optional
    })
    .then(data => console.log(data))
    .catch(err => console.log(err));

// Collect confirmation code, then
Auth.confirmSignUp(username, code, {
    // Optional. Force user confirmation irrespective of existing alias. By default set to True.
    forceAliasCreation: true    
}).then(data => console.log(data))
.catch(err => console.log(err));

attributesというプロパティにemail等々を渡してやれば良さそうですね。 一応ソースコードを確認してみます。

https://github.com/aws/aws-amplify/blob/master/packages/aws-amplify/src/Auth/Auth.ts

    /**
     * Sign up with username, password and other attrbutes like phone, email
     * @param {String | object} params - The user attirbutes used for signin
     * @param {String[]} restOfAttrs - for the backward compatability
     * @return - A promise resolves callback data if success
     */
    public signUp(params: string | object, ...restOfAttrs: string[]): Promise<any> {
        if (!this.userPool) { return Promise.reject('No userPool'); }

        let username : string = null;
        let password : string = null;
        const attributes : object[] = [];
        let validationData: object[] = null;
        if (params && typeof params === 'string') {
            username = params;
            password = restOfAttrs? restOfAttrs[0] : null;
            const email : string = restOfAttrs? restOfAttrs[1] : null;
            const phone_number : string = restOfAttrs? restOfAttrs[2] : null;
            if (email) attributes.push({Name: 'email', Value: email});
            if (phone_number) attributes.push({Name: 'phone_number', Value: phone_number});
        } else if (params && typeof params === 'object') {
            username = params['username'];
            password = params['password'];
            const attrs = params['attributes'];
            if (attrs) {
                Object.keys(attrs).map(key => {
                    const ele : object = { Name: key, Value: attrs[key] };
                    attributes.push(ele);
                });
            }
            validationData = params['validationData'] || null;
        } else {
            return Promise.reject('The first parameter should either be non-null string or object');
        }

        if (!username) { return Promise.reject('Username cannot be empty'); }
        if (!password) { return Promise.reject('Password cannot be empty'); }

        logger.debug('signUp attrs:', attributes);
        logger.debug('signUp validation data:', validationData);

        return new Promise((resolve, reject) => {
            this.userPool.signUp(username, password, attributes, validationData, function(err, data) {
                if (err) {
                    dispatchAuthEvent('signUp_failure', err);
                    reject(err);
                } else {
                    dispatchAuthEvent('signUp', data);
                    resolve(data);
                }
            });
        });
    }

14行目のif文で分岐しているのが分かります。 引数のparamsがobjectの場合は、attributesというプロパティから各項目を取り出し、25〜30行目でattributesという配列に追加していきます。 こうやって作成されたattributesは、43行目でthis.userPool.signUpを呼び出す際に引数として渡しています。

呼び出し方も分かったので、MySignUpコンポーネントのsignUpメソッドを下記のように修正しました。

    signUp() {
        const { username, password, email, phone_number, address } = this.inputs;
        const param = {
            username: username,
            password: password,
            attributes:{
                email: email,
                phone_number: phone_number,
                address: address
            }
        }
        Auth.signUp(param)
            .then(() => this.changeState('confirmSignUp', username))
            .catch(err => this.error(err));
    }

最後に標準のwithAuthenticatorをラップするwithMyAuthenticatorという関数を作ります。 MyAuth.jsというファイルに下記のように記述しました。

MyAuth.js

import React from 'react';
import { ConfirmSignIn, ConfirmSignUp, ForgotPassword, SignIn, VerifyContact, withAuthenticator } from 'aws-amplify-react';
import MySignUp from './MySignUp'


export function withMyAuthenticator(Comp, includeGreetings=false) {
  return withAuthenticator(Comp, includeGreetings, [
      <SignIn/>,
      <ConfirmSignIn/>,
      <VerifyContact/>,
      <MySignUp/>,
      <ConfirmSignUp/>,
      <ForgotPassword/>
    ]);
}

ここまで準備できたら、あとは仕上げです。App.jsを修正し、カスタマイズしたサインアップ画面を含む認証機能を追加します。

App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
// import { withAuthenticator } from 'aws-amplify-react';
import { withMyAuthenticator } from './MyAuth'

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default withMyAuthenticator(App);

これで準備OKです。

動作確認

準備ができたので、先ほどと同じようにnpm startして見ると、今度は住所欄が表示されています。

これで住所の必須チェックに引っかかることも無くなりました このまま必要事項を入力してサインアップすると、無事にAWSから確認コードが送信されて来ました。

これでサインアップ機能のカスタマイズ完了です!!

まとめ

AWS Amplify Reactのサインアップ機能をカスタマイズしてみました。 Amplify周りはまだまだ日本語の情報が少なく、不明点があると解決するのに時間がかかってしまう印象です。 このエントリが誰かのお役に立てば幸いです。