React.jsとAWS(cognito)の連携

##何をする？
reactコンポーネントからAWSのcognitoにユーザ登録を行い、サインインを行います。

## cognitoについて
アプリやwebサービスの認証基盤です。認証ロジックだけAWSに託します。
サービス提供者はログイン情報を保持することなく認証結果のみを受け取ります。
また、cognitoを介して様々なAWSサービスが使えるようになります。
詳細は公式ドキュメントを参照ください。
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html)


## バージョン
- node.js **10.15.3**
- react.js **^16.12.0**
- material-ui **^0.20.2**
- amazon-cognito-identity-js **^3.0.15**

## 準備
awsへの連携にはamazon-cognito-identity-jsを使います。

```
 npm install --save amazon-cognito-identity-js
```

詳細はこちらを参照ください。
[github公式ページ](https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js)

## ユーザプールの作成
![userpool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403352/586d02ac-77fc-e2c2-e228-54d54358c060.png)

![アプリクライアント.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403352/0fbea5e7-2692-3723-2a1a-346c0739be68.png)

ユーザプール > ユーザとグループ
にアカウント情報が蓄積されます。
プール ARNとプール IDとアプリクライアントIDはプログラムで使用します。
また、今回はサインイン時の二段階認証にEメールを利用します。

## IDプールの作成
![IDpool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403352/93354912-7b39-d28a-eb69-30d1d70a1116.png)

![IDﾌﾟｰﾙの編集.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403352/b200f3ce-86fb-b294-0adb-2c7044b0fa9e.png)

ユーザプールへの登録時にIDプールにも情報を登録します。
これにより、登録ユーザはIDプールを介して他のAWSサービスを利用できるようになります。
IDプールのIDはプログラムで使用します。

これでcognitoを使用する準備ができました。
次にアプリケーションとcognitoを連携します。

## 該当のプログラム

###サインイン画面
まずはreact側でユーザIDとパスワードとEメールアドレスを入力する画面を作成。

```signin.js
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles,createMuiTheme,MuiThemeProvider} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import { connect } from "react-redux"
import FormControl from '@material-ui/core/FormControl';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import { compose } from 'redux'
import Grid from '@material-ui/core/Grid';
import CognitoAuth from 'aws'


const styles = theme => ({
    contents: {
        margin: '150px auto 0 ',
        maxWidth : '1260px',
    },
    formContents:{
        margin: '0 auto',
        maxWidth: '480px'
    },
    formControl:{
        display:'block',
        width:'100%'
    },
    btn:{
        marginTop:'20px',
    }

});
function typographyV1Theme(theme) {
    return createMuiTheme({
        ...theme,
        typography: {
            useNextVariants: false,
        },
    });
}

class SignIn extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            password: '',
            user_id: '',
            email:'',
            errors:[],
        };
    }
    goError = async (e) => {
        await this.setState(
            {errors: (e.message || JSON.stringify(e))}
        )
        await console.log("エラー:"+this.state.errors)
    };
    goNext = async () => {
        try{
            await this.props.history.push({
                pathname: '/activate',
                state:{
                    user_name: this.state.user_id,
                    password: this.state.password,
                    email: this.state.email,
                    user_id:this.state.user_id
                }
            })
        } catch (e){
            this.goError(e)
        }
    };
    signInCognito = async () => {
        await alert(this.state.user_id)
        await CognitoAuth.signIn(this.state.user_id,this.state.password,this.state.email)
            .then(this.goNext)
            .catch(this.goError)
    };
    
    render() {
        const { classes,dispatchAddValue } = this.props;
        return (
            <MuiThemeProvider theme={typographyV1Theme}>
                <div className={classes.contents}>
                    <p>お名前をメールアドレスを入力してください。</p>
                </div>
                <div className={classes.formContents}>
                    <Grid container spacing={16}>
                        <Grid item xs={12} sm={12}>
                            <FormControl className={classes.formControl}>
                                <InputLabel htmlFor="component-simple">ID</InputLabel>
                                <Input id="component-simple" name="user_id"  fullWidth defaultValue={this.state.user_id} />
                            </FormControl>
                        </Grid>
                    </Grid>
                    <Grid container spacing={16}>
                        <Grid item xs={12} sm={12}>
                            <FormControl className={classes.formControl}>
                                <InputLabel htmlFor="component-simple">パスワード</InputLabel>
                                <Input id="component-simple"　name="password" fullWidth defaultValue={this.state.password} />
                            </FormControl>
                        </Grid>
                    </Grid>
                    <Grid container spacing={16}>
                        <Grid item xs={12} sm={12}>
                            <FormControl className={classes.formControl}>
                                <InputLabel htmlFor="component-simple">メールアドレス</InputLabel>
                                <Input id="component-simple" name="email"  fullWidth defaultValue={this.state.email} onChange={this.handleChange} />
                            </FormControl>
                        </Grid>
                    </Grid>
                    <Grid container spacing={16}>
                        <Grid item xs={12} sm={12}>
                            {this.state.errors}
                        </Grid>
                    </Grid>

                    <Grid className={classes.btn} container spacing={16}>
                        <Grid item xs={12} sm={12}>
                            <Button variant="contained" color="secondary" fullWidth className={classes.fcButton} onClick={this.signInCognito}>
                               次へ
                            </Button>
                        </Grid>
                    </Grid>
                </div>
            </MuiThemeProvider>
        );

    }
}

SignIn.propTypes = {
    classes: PropTypes.object.isRequired,
};
export default compose(
    withStyles(styles),
    connect(
        mapStateToProps,
        mapDispatchToProps
    )
)(SignIn)

```

画面はこんなイメージ
![サインイン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403352/b3748bf2-94c9-b13f-520d-e76a89a9fef1.png)

###認証
「次へ」ボタンを押すとCognitoAuth.signInが実行され、入力したアドレスに認証コードが届きます。

```aws.js

import AWS from 'aws-sdk'
const AmazonCognitoIdentity = require('amazon-cognito-identity-js')

const CognitoAuth= {
    signIn: function (userName, password, email = null, phoneNumber = null) {
        return new Promise(function(resolve, reject){
            var userPoolData = {
                UserPoolId : [ユーザプールID],
                ClientId : [ユーザプールクライアントID]
            }
            var userPool = new AmazonCognitoIdentity.CognitoUserPool(userPoolData)

            var attributeList = [];

            if (email !== null){
                var attribute = {Name : 'email', Value : email};
                attributeList.push(new AmazonCognitoIdentity.CognitoUserAttribute(attribute));
            }
            if (phoneNumber !== null){
                var attribute = {Name : 'phone_number', Value : phoneNumber};
                attributeList.push(new AmazonCognitoIdentity.CognitoUserAttribute(attribute));
            }
            return userPool.signUp(userName, password, attributeList, null, function(err, result) {
                if (err) {
                    reject(err)
                }
                resolve(result.user)
            })
        })
    },

}
export default CognitoAuth;

```

###認証コード入力
届いた認証コードと、ユーザネームと、パスワードを引数にconfirmRegistrationを実行するだけで認証が通ります。そのままログイン処理を実行することでサインイン後は自動的にホーム画面などに遷移させることができます。

```aws.js

import AWS from 'aws-sdk'
const AmazonCognitoIdentity = require('amazon-cognito-identity-js')

const CognitoAuth= {
・・・
    confirmRegistration: function (userName, password, registrationCode) {
        return new Promise(function(resolve, reject){
            var authenticationData = {
                Username : userName,
                Password : password
            }
            var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData)

            var userPoolData = {
                UserPoolId : [ユーザプールID],
                ClientId : [ユーザプールクライアントID]
            }
            var userPool = new AmazonCognitoIdentity.CognitoUserPool(userPoolData)

            const userData = {
                Username : userName,
                Pool : userPool
            }
            var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData)
            cognitoUser.setAuthenticationFlowType('USER_PASSWORD_AUTH')

            cognitoUser.confirmRegistration(registrationCode, true, function(err, result) {
                if (err) {
                    reject(err)
                }
                resolve(result)
            })
        })
    },
}
export default CognitoAuth;
```


###ログイン

```aws.js
import AWS from 'aws-sdk'
const AmazonCognitoIdentity = require('amazon-cognito-identity-js')

const CognitoAuth= {
・・・
    doLogin: function (userName, password) {
        return new Promise(function(resolve, reject){
            let authenticationData = {
                Username : userName,
                Password : password
            };
            let authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData)

            let userPoolData = {
                UserPoolId : [ユーザプールID],
                ClientId : [ユーザプールクライアントID]
            };
            let userPool = new AmazonCognitoIdentity.CognitoUserPool(userPoolData)
            const userData = {
                Username : userName,
                Pool : userPool
            };
            let cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData)

            cognitoUser.setAuthenticationFlowType('USER_PASSWORD_AUTH')

            cognitoUser.authenticateUser(authenticationDetails,{
                onSuccess: function (result) {

                    AwsAuth.setJwtToken(result.getIdToken().getJwtToken())
                    AWS.config.region = [リージョン]
                    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                        IdentityPoolId : [IDプールのID],
                        Logins : {
                            [[プール ARN]]: result.getIdToken().getJwtToken()
                        }
                    });
                    AWS.config.credentials.clearCachedId()
                    AWS.config.credentials.refresh((error) => {
                        if (error) {
                            reject(error)
                        } else {
                            resolve(result.getIdToken().getJwtToken())
                        }
                    });
                },
                onFailure: function (err) {
                    console.log(err)
                    reject(err)
                },
                mfaRequired: function(result) {
                    reject("mfaRequired")
                },
                newPasswordRequired(result) {
                    reject("newPasswordRequired")
                },
                customChallenge(result) {
                    reject("customChallenge")
                }
            })
        })
    }
}

```
## まとめ
cognitoを使うことでユーザのログイン情報をDBなどに保持する必要がなくなるのでサービスを提供する側にとっては大きなリスク回避になりますね。また、facebookやtwitterアカウントなどのリソースを利用した認証基盤も簡単に導入できます。