AWS Amplify Reactのサインアップ画面をカスタマイズしてみる
サーバーレス開発部@大阪の岩田です。 下記のエントリでも紹介されているようにAWS AmplifyとAWS Amplify Reactを使用すると、簡単にReactアプリに認証機能を追加することができます。
しかし、実業務で認証機能を利用する際は、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に下記のコードを追記します。
// ...略 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等々を渡してやれば良さそうですね。 一応ソースコードを確認してみます。
/** * 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というファイルに下記のように記述しました。
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を修正し、カスタマイズしたサインアップ画面を含む認証機能を追加します。
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周りはまだまだ日本語の情報が少なく、不明点があると解決するのに時間がかかってしまう印象です。 このエントリが誰かのお役に立てば幸いです。