Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その4)

Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その3)」の続きです。

npm start
します。Browseが起動して、Login画面が表示されます。画面中付近に、 Upload Fileを入力できるボタンが表示されます。

S3のBucketが空であることを確認しておきます。Management ConsoleからS3を開き、upload Bucketをクリックします。

Web Applicationに戻って、FileをUploadします。 「Browse」をクリックして、Fileを指定します。今回はTiff Fileのみ選択できるようにしてあるので、適当なTiff Fileを指定して、「開く」をクリックします。

指定したFileが表示されていることを確認して、「Upload File」をクリックします。Uploadできず、Errorが表示されます。Loginしていない状態では、FileのUploadが出来ないことを確認しました。

再度、S3のUpload Bucketが空であることを確認しておきます。

Loginして、同様にFileをUploadします。Fileは指定されたままなので、「Upload File」をクリックします。もちろん「Browse」をクリックして、Fileを再指定しても良いです。FileがUploadできました。

S3にFileがUploadされているか確認します。Management ConsoleからS3を開き、Upload Bucketをクリックします。Bucketの内容を表示している画面のままでしたら、更新のIconをクリックします。「protected」というFolderが作成されています。「protected」をクリックします。「ap-northeast-1:」で始まるFolderが作成されています。そのFolderをクリックします。「upload」というFolderが作成されています。「upload」をクリックします。UploadしたFileが見えます。今回は、AmplifyのStorageのLevelにProtectedを指定したので、上記のような階層にFileがUploadされます。Policyを使ってS3へのAccess制御を指定しましたが、Amplify StorageのLevelとPolicyを細かく指定することによって、User間でお互いにFileを見れるようにしたり、他のUserからは見れないようにしたりすることが可能です。

この方法ではUploadするFileを一つ一つ指定してUploadする必要があります。それでは不便なため、複数のFileを一気にUploadできるようにするためにDropZoneを使ってUploadするFileを指定できるようにします。まず、react-dropzoneをInstallします。
cd upload
npm install -save react-dropzone

App.jsを編集してDropZoneからUpload Fileを指定するように変更します。

import React, { useState, useCallback, useMemo }  from 'react';
import { useDropzone } from 'react-dropzone';
import Storage from "@aws-amplify/storage";
import { SetS3Config } from "./services";
import logo from './logo.svg';
import './App.css';
const baseStyle = {
  flex: 1,
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'center',
  padding: '20px',
  borderWidth: 2,
  borderRadius: 2,
  borderColor: '#eeeeee',
  borderStyle: 'dashed',
  backgroundColor: '#fafafa',
  color: '#bdbdbd',
  outline: 'none',
  transition: 'border .24s ease-in-out'
};
const activeStyle = {
  borderColor: '#2196f3'
};
const acceptStyle = {
  borderColor: '#00e676'
};
const rejectStyle = {
  borderColor: '#ff1744'
};
function App(props) {
  const [response, setResponse] = useState("");
  const onDrop = useCallback(acceptedFiles => {
    acceptedFiles.forEach(file => {
      const reader = new FileReader();
      reader.onload = () => {
        const fileAsArrayBuffer = new Uint8Array(reader.result);
          // do whatever you want with the file content
        SetS3Config(process.env.REACT_APP_bucket_name, "protected");
        Storage.put('upload/' + file.name, fileAsArrayBuffer, { contentType: file.type })
        .then(result => {
            console.log(result);
            setResponse("File is uploaded!");
            return true;
        })
        .catch(err => {
            console.log(err);
            setResponse(`File is not uploaded: ${err}`);
            return true;
        });
      };
      reader.onabort = () => {
        console.log("Reading file was aborted");
        setResponse("Reading file was aborted");
      }
      reader.onerror = () => {
        console.log("Reading file has failed");
        setResponse("Reading file has failed");
      }
      reader.readAsArrayBuffer(file);
    });
  }, []);
  const {acceptedFiles, getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject} = useDropzone({onDrop, accept: 'image/tif, image/tiff'});
  const files = acceptedFiles.map(file => (
    <div align="center">
      {file.path} - {file.size} bytes
    </div>
  ));
  const style = useMemo(() => ({
    ...baseStyle,
    ...(isDragActive ? activeStyle : {}),
    ...(isDragAccept ? acceptStyle : {}),
    ...(isDragReject ? rejectStyle : {})
  }), [
    isDragActive,
    isDragAccept,
    isDragReject
  ]);
  if(props.authState === 'signedIn') {
    return (
      <div className="App">
        <div {...getRootProps({style})}>
          <input {...getInputProps()} />
          <p>Drag 'n' drop some files here, or click to select files</p>
        </div>
        <div>
          <h4>Files</h4>
          <p>{files}</p>
        </div>
        {!!response && <div>{response}</div>}
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  } else if(props.authState === 'confirmSignUp') {
    return (
      <div className="Your account is not granted">
        <header className="App-header">
          Your account is not granted.
        </header>
      </div>
    );
  } else if(props.authState === 'signIn') {
    return (
      <div className="Welcome to the world">
        <header className="App-header">
          Welcome to the world.
        </header>
      </div>
    );
  } else {
    return (
      <div className="Login failed">
        <header className="App-header">
          Login failed.
        </header>
      </div>
    );
  }
}
export default App;

App.jsの編集が完了したら、
npm start
します。 Browseが起動して、Login画面が表示されたら、Loginします。Loginすると、UploadするFileをDrag&Dropする領域が出来ています。Uploadする複数のFileをDrag&Dropします。Drag&Dropする領域をクリックして、File Browserから複数のFileを指定することもできます。Uploadが完了したら、Management ConsoleからS3を開き、UploadされたFileを確認します。

これで、 File Upload機能 の実装は完了です。

それでは、以前と同様にS3にDeployしてみましょう。
package.jsonにhomepageのentryが設定されているか確認します。
cd upload
npm run build
aws s3 sync --exact-timestamps --delete --acl public-readdirname bucketname

Management ConsoleからS3を開き、export.hacya.comのindex.htmlのURLを確認して、BrowserでそのURLを開きます。同様にTiff FileをUploadします。

Uploadできました。これで完了です。

Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その3)

Web Applicationを実装します。「Reactを使ってStaticなWeb Hostingをする。」と同じように、まずは、Reactのひな型を作ります。
create-react-app upload
cd upload
npm install -save aws-amplify aws-amplify-react

前回作成、または、変更したFileを参考に、S3の設定を追加しながら、作成したひな形にCopyします。詳細な説明は、「Cognitoユーザー認証をAmplifyとReactを使って実装する。(その3)」に戻って確認してください。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import AppWithAuth from './AppWithAuth';
import * as serviceWorker from './serviceWorker';

//Import Amplify Configure function
import { configureAmplify } from './services'

configureAmplify();

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

services.jsでは、S3の設定を追加します。また、SetS3Config()というfunctionを定義します。BucketやS3のAccess Levelを変更した後、AmplifyのStorage.Configure()をCallして、Amplifyの設定を変更する必要があるためです。

import Amplify from '@aws-amplify/core';
import Auth from '@aws-amplify/auth';
import Storage from '@aws-amplify/storage';

export function configureAmplify() {
  Amplify.configure(
  {
    Auth: {
      region: process.env.REACT_APP_region,
      userPoolId: process.env.REACT_APP_userPoolId,
      userPoolWebClientId: process.env.REACT_APP_userPoolWebClientId,
      identityPoolId: process.env.REACT_APP_identityPoolId,
    },
    Storage: {
      AWSS3: {
        bucket: process.env.REACT_APP_bucket_name,
        region: process.env.REACT_APP_region,
      }
    }

  } );
}

//Configure Storage with S3 bucket information
export function SetS3Config(bucket, level){
  Auth.configure({
    region:  process.env.REACT_APP_region,
    identityPoolId: process.env.REACT_APP_identityPoolId,
  });
  Storage.configure({ 
    bucket: bucket,
    level: level,
  });
}

projectのroot directoryの “.env”にS3の設定を追加した、5つの情報を設定ます。
REACT_APP_region=ap-northeast-1
REACT_APP_userPoolId=ap-northeast-1yourPoolID
REACT_APP_userPoolWebClientId=yourWebClientId
REACT_APP_identityPoolId=ap-northeast-1:yourIdentityPoolId
REACT_APP_bucket_name=yourBucketName

AppWithAuth.js、MyVerifyContact.js、MyConfirmSignUp.jsは変更ありません。

import React from 'react';
import { Authenticator, ConfirmSignIn, RequireNewPassword, ConfirmSignUp, VerifyContact, ForgotPassword, TOTPSetup } from 'aws-amplify-react';
import App from "./App";
import MyVerifyContact from "./MyVerifyContact"
import MyConfirmSignUp from "./MyConfirmSignUp"

const signUpConfig = {
  hideAllDefaults: true,
  signUpFields: [
    {
      label: 'Username',
      key: 'username',
      required: true,
      placeholder: 'Username',
      displayOrder: 1
    },
    {
      label: 'Password',
      key: 'password',
      required: true,
      placeholder: 'Password',
      type: 'password',
      displayOrder: 2
    }
  ]
}

function AppWithAuth() {

  function handleAuthStateChange(state, data) {
    if (state === 'confirmSignIn') {
        /* Do something when the user has signed-in */
    }
  }

  return (
    <Authenticator signUpConfig={signUpConfig} hide={[ConfirmSignIn, RequireNewPassword, VerifyContact, ConfirmSignUp, ForgotPassword, TOTPSetup]} onStateChange={handleAuthStateChange}>
      <MyVerifyContact override={VerifyContact} />
      <MyConfirmSignUp override={ConfirmSignUp} />
      <App />
    </Authenticator>
  )
}

export default AppWithAuth;
import { AuthPiece } from 'aws-amplify-react';

class MyVerifyContact extends AuthPiece {
  constructor(props) {
    super(props);
    this._validAuthStates = ["verifyContact"];
    this.state = {}
  }

  static getDerivedStateFromProps(props, state) {
    if(props.authState === "verifyContact") {
      props.onStateChange('signedIn');
      return props;
    } else {
      return null;
    }
  }

  showComponent(theme) {
    return null;
  }

}

export default MyVerifyContact;
import React from 'react';

import { I18n } from 'aws-amplify';
import { AuthPiece } from 'aws-amplify-react';

import {
  FormSection,
  SectionHeader,
  SectionBody,
  SectionFooter,
  Button
} from 'aws-amplify-react';

class MyConfirmSignUp extends AuthPiece {
  constructor(props) {
    super(props);
    this._validAuthStates = ["confirmSignUp"];
    this.gotoSignIn = this.gotoSignIn.bind(this);
  }

  gotoSignIn() {
    super.changeState('signIn');
  }

  showComponent(theme) {
    return (
      <FormSection theme={theme}>
        <SectionHeader theme={theme}>{I18n.get('Please contact your administrator to grant your account.')}</SectionHeader>
        <SectionBody theme={theme}>
        </SectionBody>
        <SectionFooter theme={theme}>
          <Button onClick={this.gotoSignIn} theme={theme}>
            {I18n.get('Goto SignIn')}
          </Button>
        </SectionFooter>
      </FormSection>
    );

  }

}

export default MyConfirmSignUp;

最後にApp.jsを編集して、S3へUploadできるようにします。

import React, { useState, createRef }  from 'react';
import logo from './logo.svg';
import Storage from "@aws-amplify/storage";
import { SetS3Config } from "./services";
import './App.css';

function App(props) {

  const [uploadFile, setUploadFile] = useState({});
  const [response, setResponse] = useState("");
  const uploadRef = createRef();
  const onInputClick = () => {
    if(uploadRef.current) {
      uploadRef.current.value = null;
      uploadRef.current.click();
    }
    setResponse("");
  }

  function uploadFileToS3() {
    SetS3Config(process.env.REACT_APP_bucket_name, "protected");
    Storage.put(`upload/${uploadFile.name}`,
                uploadFile,
                { contentType: uploadFile.type })
      .then(result => {
        console.log(result);
        setUploadFile({});
        setResponse("File is uploaded!");
      })
      .catch(err => {
        console.log(err);
        setResponse(`File is not uploaded: ${err}`);
      });
  };

  function InputUploadFile(props) {
    return(
      <div  align="center">
        <input
          type="file"
          style={{ display: "none" }}
          accept="image/tif, image/tiff"
          ref={uploadRef}
          onChange={(e) => setUploadFile(e.target.files[0])}
        />
        <input value={uploadFile.name} placeholder="Select file" />
        <button onClick={onInputClick}> Browse </button>
        <button onClick={uploadFileToS3}> Upload File </button>

        {!!response && <div>{response}</div>}
      </div>
    )
  }

  if(props.authState === 'signedIn') {
    return (
      <div className="App">
        <InputUploadFile />

        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  } else if(props.authState === 'confirmSignUp') {
    return (
      <div className="Your account is not granted">
        <header className="App-header">
          Your account is not granted.
        </header>
      </div>
    );
  } else if(props.authState === 'signIn') {
    return (
      <div className="Welcome to the world">
        <InputUploadFile />

        <header className="App-header">
          Welcome to the world.
        </header>
      </div>
    );
  } else {
    return (
      <div className="Login failed">
        <header className="App-header">
          Login failed.
        </header>
      </div>
    );
  }
}

export default App;

npm startからの説明は次回。

Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その2)

Cognitoで認証されたユーザーの権限を設定します。

AWS Serviceを利用する事に対する権限の設定にはIAM UserとIAM Roleの2種類があります。IAM Userは、UserがManagement ConsoleにLoginしてAWS Serviceを利用するための権限設定と言えます。Management Consoleを操作できない仮想のUserを設定することも可能です。個人的な理解はIAM UserはAWS Serviceを外側から利用することを想定した権限設定です。IAM Roleは、AWS Serviceが他のAWS Serviceを利用するための権限設定と言えます。こちらは、AWS Serviceを内側から利用することを想定した権限設定と理解しています。
IAM UserもIAM Roleも個々の権限を直に記述して設定することができます。また、IAM Policyに権限の設定を記述し、複数のIAM PolicyをIAM UserやIAM RoleにAttachして権限を構成することもできます。

Cognitoユーザー認証で認証されたユーザーは、Cognitoで設定されたIAM Roleの権限でAWS Serviceを利用することになります。今回は、Congintoの設定時に生成されたIAM RoleにS3 Accessに必要な権限をIAM Policyに設定し、IAM RoleにAttachします。

Management ConsoleからIAMを開き、「ポリシー」をクリックします。「ポリシーのフィルタ」に「S3」を入力すると、S3の名前を含んだIAM Policyが表示されます。AmazonS3FullAccessの▶をクリックして、ポリシーの書き方を確認します。

「ポリシーの作成」をクリックして、 「JOSN」タブを開き、 ポリシーを設定します。 (その1)で作成したBucketだけにAccess出来るように、(その1)で確認したARNは「arn:aws:s3:::upload.hacya.com」なので、次のJSONのTemplateの{enter bucket name}に「upload.hacya.com」を入れます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::{enter bucket name}/public/*",
                "arn:aws:s3:::{enter bucket name}/protected/${cognito-identity.amazonaws.com:sub}/*",
                "arn:aws:s3:::{enter bucket name}/private/${cognito-identity.amazonaws.com:sub}/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::{enter bucket name}/uploads/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::{enter bucket name}/protected/*"
            ],
            "Effect": "Allow"
        },
        {
            "Condition": {
                "StringLike": {
                    "s3:prefix": [
                        "public/",
                        "public/*",
                        "protected/",
                        "protected/*",
                        "private/${cognito-identity.amazonaws.com:sub}/",
                        "private/${cognito-identity.amazonaws.com:sub}/*"
                    ]
                }
            },
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::{enter bucket name}"
            ],
            "Effect": "Allow"
        }
    ]
}

「ポリシーの確認」をクリックし、ポリシーの確認をします。
ポリシーの確認では、「名前」を設定します。今回は「CognitoS3UploadPolicy」としました。「ポリシーの作成」をクリックし、ポリシーを作成します。

ポリシーが作成できたら、左側の「ロール」をクリックします。ロールが表示されるので、CognitoのUser Poolを作成する時に作成した「Cognito_UserPoolAuth_Role」をクリックします。「ポリシーをアタッチします」をクリックし、「ポリシーのフィルタ」に「S3」を入力し、先ほど作成した 「CognitoS3UploadPolicy」の左側のチェックボックスをクリックしてチェックを入れます。「ポリシーのアタッチ」をクリックして、ポリシーをアタッチします。

これで、Cognitoの認証ユーザーが持つRoleに、S3のAccessに必要な権限を追加したことになります。

Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その1)

今回は、ユーザーから提供されるFileに対して何かしらの処理を行うことを想定したWeb Serviceの実現の為、File Upload機能を実装します。使うComponentは、今までとほぼ同じく、Cloud側のFile ServiceにS3、ユーザー認証にCognito、Client側はAmplify、Reactを使い、最後にDropZoneによるDrag&Dropを追加します。これにより、Cognitoユーザー認証された 特定多数のUserに対し、S3 BucketにFileをUploadできる機能を実現します。

まずは、File Upload用のS3 Bucketを準備します。Management ConsoleからS3を開きます。前回作成したBucketが見えると思います。「Bucketを作成する」をクリックしてBucketを作成します。 その後のStepは、
1. 「名前とリージョン」タブでバケット名を入力します。バケット名は、FQDN形式で入力します。AWS全体で重複のない名前を指定する必要があります。今回は、”upload.hacya.com”としました。リージョンは”アジアパシフィック(東京)”を選んでおきましょう。
2. 「オプションの設定」タブでは、タグを付けておきましょう。他はデフォルトのままで良いです。
3. 「アクセス許可の設定」タブは、デフォルトのままで良いです。今回は認証されたユーザーにのみ、中身を公開します。
4. 「確認」タブで、内容を確認して「バケットを作成」ボタンをクリックしてBucketを作成します。

作成したBucketのPropertyを見ておきましょう。作成したBucketの左側のチェックボックスをクリックしてチェックを付けると、右側にPop-up Windowが開きPropertyが表示されます。Pop-up Windowsの上の方の「バケットARNをコピーする」をクリックしてARNをクリップボードにコピーし、テキストエディターなどに貼り付け、バケットARNを書き留めておきます。あとで、Policyを作成するときに使います。

次に、作成したBucketにCORSを設定します。CORSは、Cross-Origin Resource Sharingの略で、通常ブラウザは異なるドメインからのデータを読み込めないように設定されています。CORSを設定することで、ブラウザが異なるドメインのデータを読み込めるようにすることができます。例えば、local環境でテストする場合、ReactのApplicationのDefaultでは、http://localhost:3000がOriginとなり、そこからS3のデータを参照する際、例えば東京リージョンのS3のURL、https://s3-ap-northeast-1.amazonaws.comは異なるドメインという事になります。
作成したBucketをクリックして、「アクセス権限」タブをクリック、「CORSの設定」をクリックします。 CORS 構成エディター に、下記の設定を入力し、「保存」をクリックします。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <ExposeHeader>x-amz-server-side-encryption</ExposeHeader>
    <ExposeHeader>x-amz-request-id</ExposeHeader>
    <ExposeHeader>x-amz-id-2</ExposeHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

これで、S3の設定は完了です。

Cognitoユーザー認証をAmplifyとReactを使って実装する。(その4)

実際に作成したApplicationの動作を確認していきます。
npm start
すると、DefaultのBrowserが立ち上がり、「Sign in to your account」画面が表示されます。

最初はCognitoに一つもAccountが設定されていないので、下側にある「Create Account」をクリックして、「Create a new account」画面に移動し、UsernameとPasswordを入力、「CREATE ACCOUNT」をクリックして、新しいAccountを作成します。

「Your account is not granted」と表示されます。これは、Eメールでの自動認証の設定をしていないためで、Management Consoleから認証します。

Management ConsoleからCognitoサービスを開き、「ユーザープールの管理」へ移動します。「ユーザープール」では、作成したユーザープールをクリックし、画面左側の「ユーザーとグループ」タブをクリックします。
先ほど作成したユーザーが、「UNCONFIRMED」の状態で表示されます。ユーザー名をクリックして、詳細を表示し、「ユーザーの確認」をクリックします。「アカウントのステータス」が、「Enabled/CONFIRMED」になった事を確認して、画面左側の「ユーザーとグループ」をクリックし、ユーザーの一覧を表示させます。

Browserに戻って、「Goto Signin」をクリックして、「Sign in to your account」画面に戻り、先ほど作成したAccountのUsernameとPasswordを入力し、「SIGN IN」をクリックします。

お馴染みのReactの画面が表示されます。
右上の「SIGN OUT」をクリックしてSign Outすると、 「Sign in to your account」画面に戻り ます。

これで、AWS Cogintoを使ったServerlessのユーザー管理が出来ることが分かりました。
Client側のJava ScriptからCognitoのSign inの状態が分かる、という所まで理解できたと思います。

最後に、このApplicationをS3にDeployします。
cd export
npm run build
aws s3 sync --exact-timestamps --delete --acl public-readdirname bucketname

S3にUploadが完了したら、 Web BrowserからAccessしてみましょう。Localで実行した時と同じ画面になり、動作も同じになります。

Serverlessでユーザー管理を実現ができることが分かりました。

Cognitoユーザー認証をAmplifyとReactを使って実装する。(その3)

Client側のAmplifyをInstallします。環境は前回「Reactを使ってStaticなWeb Hostingをする。」で使用した環境に追加します。
npm install --save aws-amplify aws-amplify-react

Client側のApplicationを作って行きます。ReactのWelcome画面にCognitoユーザー認証を追加します。

初めに読み込まれる、index.jsにAWS Amplifyの設定を入れて置く”services”というfileをimportする行を追加します。
import { configureAmplify } from './services'
そのあとで、Amlifyをconfigureするために、
configureAmplify();
を呼び出します。ReactDom.renderに渡すelementを<App />から<AppWithAuth />に変更し、ユーザー認証を追加した描画処理に変更します。
ReactDOM.render(<AppWithAuth />, document.getElementById('root'));

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import AppWithAuth from './AppWithAuth';
import * as serviceWorker from './serviceWorker';

//Import Amplify Configure function
import { configureAmplify } from './services'

configureAmplify();

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

index.jsからimportされる”services”を準備します。”services”では、Amplifyの設定をしますが、実際の情報は環境変数に設定しておき、起動時に”.env”から読み込むようにしておきます。このようにすることで、環境が変わっても再buildすることなく、環境を変更することができるようになります。

import Amplify from '@aws-amplify/core';

export function configureAmplify() {
  Amplify.configure(
  {
    Auth: {
      region: process.env.REACT_APP_region,
      userPoolId: process.env.REACT_APP_userPoolId,
      userPoolWebClientId: process.env.REACT_APP_userPoolWebClientId,
      identityPoolId: process.env.REACT_APP_identityPoolId,
   }
 } );
}

“.env”で4つの情報を設定し、projectのroot directoryに置きます。
REACT_APP_region=ap-northeast-1
REACT_APP_userPoolId=ap-northeast-1yourPoolID
REACT_APP_userPoolWebClientId=yourWebClientId
REACT_APP_identityPoolId=ap-northeast-1:yourIdentityPoolId
これらの情報は、Cognito User Poolを作成した時に生成されたUser ID、App Client IDと、Cognito Identity Poolを作成した時に生成されたIdentity Pool IDを設定します。これらは、Cognitoに接続するための情報ですが、URLのような物で隠す必要はありません。ボットによる攻撃を避けるためには、Cognito User Poolの作成の際に、Client Deviceの認証を追加するなど検討します。

次に、 ReactDom.renderから最初に描画されるelement、”AppWithAuth.js”を作成します。今回はEメールによる認証を行わないため、Login画面を簡素化しました。単純にusernameとpasswordだけを入力できるようにしています。

import React from 'react';
import { Authenticator, ConfirmSignIn, RequireNewPassword, ConfirmSignUp, VerifyContact, ForgotPassword, TOTPSetup } from 'aws-amplify-react';
import App from "./App";
import MyVerifyContact from "./MyVerifyContact"
import MyConfirmSignUp from "./MyConfirmSignUp"

const signUpConfig = {
  hideAllDefaults: true,
  signUpFields: [
    {
      label: 'Username',
      key: 'username',
      required: true,
      placeholder: 'Username',
      displayOrder: 1
    },
    {
      label: 'Password',
      key: 'password',
      required: true,
      placeholder: 'Password',
      type: 'password',
      displayOrder: 2
    }
  ]
}

function AppWithAuth() {

  function handleAuthStateChange(state, data) {
    if (state === 'confirmSignIn') {
        /* Do something when the user has signed-in */
    }
  }

  return (
    <Authenticator signUpConfig={signUpConfig} hide={[ConfirmSignIn, RequireNewPassword, VerifyContact, ConfirmSignUp, ForgotPassword, TOTPSetup]} onStateChange={handleAuthStateChange}>
      <MyVerifyContact override={VerifyContact} />
      <MyConfirmSignUp override={ConfirmSignUp} />
      <App />
    </Authenticator>
  )
}

export default AppWithAuth;

Login画面を簡素化するために、Amplifyが提供している”VerifyContact”、”ConfirmSignUp”をoverrideしてCustomizeしています。Amplifyは手っ取り早くApplicationを作成できるのですが、こういったCustomizeをするのが簡単ではありません。

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

class MyVerifyContact extends AuthPiece {
  constructor(props) {
    super(props);
    this._validAuthStates = ["verifyContact"];
    this.state = {}
  }

  static getDerivedStateFromProps(props, state) {
    if(props.authState === "verifyContact") {
      props.onStateChange('signedIn');
      return props;
    } else {
      return null;
    }
  }

  showComponent(theme) {
    return null;
  }

}

export default MyVerifyContact;
import React from 'react';

import { I18n } from 'aws-amplify';
import { AuthPiece } from 'aws-amplify-react';

import {
  FormSection,
  SectionHeader,
  SectionBody,
  SectionFooter,
  Button
} from 'aws-amplify-react';

class MyConfirmSignUp extends AuthPiece {
  constructor(props) {
    super(props);
    this._validAuthStates = ["confirmSignUp"];
    this.gotoSignIn = this.gotoSignIn.bind(this);
  }

  gotoSignIn() {
    super.changeState('signIn');
  }

  showComponent(theme) {
    return (
      <FormSection theme={theme}>
        <SectionHeader theme={theme}>{I18n.get('Please contact your administrator to grant your account.')}</SectionHeader>
        <SectionBody theme={theme}>
        </SectionBody>
        <SectionFooter theme={theme}>
          <Button onClick={this.gotoSignIn} theme={theme}>
            {I18n.get('Goto SignIn')}
          </Button>
        </SectionFooter>
      </FormSection>
    );

  }

}

export default MyConfirmSignUp;

最後に、”App.js”を編集して、Loginの状態を表示するようにします。

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App(props) {

  if(props.authState === 'signedIn') {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  } else if(props.authState === 'confirmSignUp') {
    return (
      <div className="Your account is not granted">
        <header className="App-header">
          Your account is not granted.
        </header>
      </div>
    );
  } else if(props.authState === 'signIn') {
    return (
      <div className="Welcome to the world">
        <header className="App-header">
          Welcome to the world.
        </header>
      </div>
    );
  } else {
    return (
      <div className="Login failed">
        <header className="App-header">
          Login failed.
        </header>
      </div>
    );
  }
}

export default App;

今回はLocal環境からnpm startしてみます。

AmplifyのLogin画面が表示されます。

次回は、ユーザー認証の方法について説明していきます。

Cognitoユーザー認証をAmplifyとReactを使って実装する。(その2)

次にFederated Identity Poolを作成します。Management ConsoleからCognitoを開き、「IDプールの管理」をクリックします。

「新しいIDプールの作成」で、「IDプール名」を設定します。「認証プロバイダー」の▶をクリックして開き、「Cognito」タブを設定します。Cognito User Poolで作成した、「ユーザープールID」と「アプリクライアントID」を設定し、「プールの作成」をクリックします。

「Your Cognito identities require access to your resources」では、「詳細を表示」の▶をクリックし開きます。Cognito Federated IdentityにRoleを設定する必要があり、ここで新しく作成されるRoleが表示されます。
ここの画面は英語が混ざっていたり、既存のRoleを設定することが出来なかったり、デフォルト以外のRoleを設定できなかったりと、今一な作りです。
「許可」をクリックします。
既にRoleがあったり、Roleを作成する権限がなかったりすると、エラーになりますが、Cognito Federated Identityは作成されます。エラーになった場合は、Dash Boardに戻って内容を確認し、必要があれば修正します。
「Amazon Cognitoでの作業開始」では、「ダッシュボードに移動」をクリックして「ダッシュボード」に移動し、右上の「IDプールの編集」をクリックします。

「IDプールの編集」では、作成した「IDプールのID」を確認します。このIDは、Amplifyの設定で使用します。Roleが正しく設定されているか確認します。Management ConsoleからIAMを開き、設定されているRoleのTrust Relationshipsが正しく設定されているか確認します。

これで、Federated Identity Poolの作成は完了です。

Cognitoユーザー認証をAmplifyとReactを使って実装する。(その1)

Web Serviceを実現する上で、Serviceを運営する側のUser管理はIAM Userで行います。不特定多数のUserにServiceを提供する場合は、特段のユーザー認証は必要ありませんが、不特定多数のUserからAccessを無制限に受け付けてしまうと、意図しないFileがUploadされたり、意図しない情報漏洩が起きたり、と問題となることが考えられます。一般的に不特定多数のUserからのAccessはRead Onlyとし、認証された特定多数のUserに対し、FileのUploadや、特定の情報を開示するようにすることが必要になります。

Web Serviceを利用するUserに対するUser管理は、Data Base(DB)を使って管理するのが一般的です。Serviceを24時間提供しようとすると、24時間User管理のDBを稼働させておく必要があります。AWSには、CognitoというUser管理Serviceがあり、これを利用すれば自前でUser管理用のDBを稼働させておく必要がなくなります。また、 AWSにはAmplifyというOpen SourceのLibraryがあり、Client側の実装を簡単に実現できてしまいます。まだまだ発展途上の感はありますが、これを使ってユーザー認証を実装してCognitoの使い方を把握していきます。

AWSのWeb PageのInstructionに従って
amplify configure
を実行すると、Server側も設定してくれます。ただ、これからWeb Serviceを運用する側からすると、裏で何が起こっているのか分からないのは気持ちが悪く、Server側の設定は全て把握しておきたい所です。今回は、Server側の設定は手作業で行います。最終的にはScriptにしてDeployの自動化を目指すことになりますが、今はAWSに必要な設定を理解することに主眼を置きます。
今回は、特定のユーザーにのみServiceを公開する、という前提で、Eメールでの認証をせず、管理者が手動で認証する方式にします。

それでは、Cognito User Poolを作成していきます。Management ConsoleからCognitoを開きます。「ユーザープールの管理」をクリックして、ユーザープール作成画面に移ります。「ユーザープールを作成する」をクリックして、ユーザープールを作成する画面、名前タブに移ります。

「名前」タブでは、「プール名」を入力して、「ステップに従って設定する」をクリックします。

「属性」タブに移ったら、「エンドユーザーをどのようにサインインさせますか」では、「ユーザー名」を選択、「どの標準属性が必要ですか?」では、全ての項目のチェックを外します。「カスタム属性を追加しますか」では、「カスタム属性の追加」をクリックして、カスタム属性を追加しておきます。今回は「group」という属性を追加し、変更可能のチェックを外しました。「次のステップ」をクリックします。

「ポリシー」タブでは、「パスワードの強度はどれくらいを要求しますか?」は、希望のパスワード強度を設定します。今回は「特殊文字を要求する」のチェックを外しました。「ユーザーに自己サインアップを許可しますか?」は、「ユーザーに自己サインアップを許可する」のチェックのままにします。「有効期限(日数)」は「7」のままにして、「次のステップ」をクリックします。

「MFAそして確認」タブ、「メッセージのカスタマイズ」タブは、Eメールを使わないので内容は変更せず「次のステップ」をクリックしていきます。

「タグ」タブでは、タグを設定して、 「次のステップ」をクリックします。

「デバイス」タブは変更せず、「次のステップ」をクリックします。

「アプリクライアント」タブでは、アプリクライアントの設定を行います。このアプリクライアントの情報をAmplifyで使用して、Cognitoのユーザー認証を行うことになります。「アプリクライアント名」を設定して、「クライアントシークレットを作成」のチェックを外し、あとは変更せず、「アプリクライアントの作成」をクリックします。 「このユーザープールへのアクセス権限があるアプリクライアントはどれですか?」では、内容を確認して「次のステップ」をクリックします。

「トリガー」タブは変更せず、「次のステップ」をクリックします。
「確認」タブで内容を確認して、「プールの作成」をクリックします。

「ユーザープールは正常に作成されました。」と表示されるので、その下の「プールID」を確認します。 このIDは、Cognito Federated Identityの作成時と、Amplifyの設定で使用します。
左側の「アプリクライアント」をクリックして、「アプリクライアントID」を確認します。「詳細を表示」をクリックして詳細も確認します。 アプリクライアントIDは、Cognito Federated Identityの作成時と、Amplifyの設定で使用します。「アプリクライアントのシークレット」 が「シークレットキーなし」になっていることを確認します。
最後に左側の「全般設定」の下の「ユーザーとグループ」をクリックして、作成されたCognito User Poolが空であることを確認しておきます。

以上で、Cognito User Poolの作成が完了しました。

Reactを使ってStaticなWeb Hostingをする。(その2)

それでは、前回作成したReactのSample PageをAWS S3にDeployしましょう。
AWS S3にDeployする際、AWSのManagement ConsoleからWeb Pageを構成するJava Script Fileを一つ一つUploadするのは現実的ではありません。Command LineからCommand一発で、直接Uploadしたいですよね。そこで、AWSのCLI(Command Line Interface)を使ってUploadすることにします。

まず、AWS上にS3へのUpload権限をもつIAM (Identity and Access Management) Userを作成します。AWSでは、このような権限管理が徹底していて、強靭なSystemを構成することができます。その反面、設定が煩雑になり、難解で取っつきにくい印象を持たれているように感じます。それはさておき、AWS Management ConsoleからIAM Serviceを呼び出します。

IAMの「ダッシュボード」から、「ユーザ」をクリック、「ユーザを追加」をクリックしてユーザの追加を行います。ユーザ名を「S3UploadUser」として、アクセスの種類は「 プログラムによるアクセス 」をチェックします。「 AWS マネジメントコンソールへのアクセス 」は、このユーザでManagement ConsoleにLoginすることは無いので、チェックしません。

「アクセス許可の設定」では、「既存のポリシーを直接アタッチ」をクリックし、「ポリシーのフィルタ」に「S3」を入力し、S3関連のPolicyを表示させます。この中から、「AmazonS3FullAccess」をチェックして、 「S3UploadUser」 に権限を与えます。

後々、管理しやすいようにタグを付けておきましょう。

内容を確認して、「ユーザの作成」をクリックしてユーザを作成します。ユーザが作成されたら、必ず「.csvのダウンロード」をクリックして、.csvをダウンロードして保存してください。AWS CLIからAWSへのアクセスする際に、.csvに保存されているアクセスキーIDとシークレットアクセスキーを使って認証を行います。このキーペアーが外部に漏れると、外部からAWSへアクセスし悪用される可能性があるため、外部に漏れないように管理してください。

作成したユーザを確認しましょう。作成したユーザ名をクリックすると、「概要」の「アクセス権限」タブに許可されている権限が表示されます。「認証情報」タブをクリックして、「サインイン認証情報」の「コンソールパスワード」が無効になっていることを確認します。また、「アクセスキー」に「アクセスキーID」があることを確認します。このアクセスキーIDに対応するシークレットアクセスキーは、先ほど保存した.csvにあります。もし紛失した場合は、紛失したアクセスキーIDを無効化し、再度この画面から「アクセスキーの作成」を行うことで再作成することができます。

次はAWS CLIのInstallです。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-windows.html
を参考にInstallします。今回は、「 Windows (64 ビット) 用 の AWS CLI MSI インストーラのダウンロード 」を使います。

Downloadが完了したら、AWSCLI64PY3.msiをダブルクリックしてInstallしていきます。

Installが完了したら、AWS CLIの設定を行います。
aws configure

ここで、先ほど保存したアクセスキーIDとシークレットアクセスキーを入力します。Regionは、ap-northeast-1(東京)を指定、Output formatはjsonを指定します。

これで環境設定は完了です。

次にReactの設定を変更します。 前回exportというディレクトリにReactのApplicationを作成しました。exportの直下にpackage.jsonファイルがあります。AWS S3にUploadしてWeb Applicationを構成する場合、AWS S3のURLを指定してBuildする必要があるため、”homepage”というエントリを追加します。

{
  "name": "export",
  "version": "0.1.0",
  "private": true,
  "homepage" : "https://s3-ap-northeast-1.amazonaws.com/export.hacya.com ",
  "dependencies": {
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-scripts": "3.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Web ApplicationをBuildします。結果は、buildディレクトリに作成されます。
cd export
npm run build

作成したWeb ApplicationをAWS S3にDeployします。ここで、先にInstallしたAWS CLIを使います。AWS S3 Syncを使って、S3にCodeをUploadします。
aws s3 sync --exact-timestamps --delete --acl public-read dirname bucketname

Uploadが完了したら、index.htmlの時と同じようにWeb BrowserからAccessしてみましょう。ReactをLocalで実行した時と同じ画面になります。

これで、少し複雑なStatic Web ApplicationをAWS S3を使って公開することができることになります。

Reactを使ってStaticなWeb Hostingをする。(その1)

前回はindex.htmlを置くだけという簡単なWeb Hostingでしたが、今回はReactを使ってみましょう。
Reactは、Facebook社が開発したJavaScriptライブラリーで、Web Pageの更新をVirtual DOMを使って管理するため、Web Pageの更新管理が簡単になります。

まずは、Node.jsのInstallです。
https://nodejs.org/en/download/
からCurrent Latest FeaturesをDownloadしてInstallします。
今回は、Windows Installer (.msi)の64-bitを選びました。

現在(2019年6月)のVersionは、V12.5.0で、
node-v12.5.0-x64.msi
をdownloadして、downloadしたfileをDouble-clickしてInstallします。

Tools for Native Modulesでは、Optionを選択せずに進みます。これを選択すると、Python 2がInstallされ、あとあと面倒なことになります。このToolが必要なModulesに出くわしたら、その時Installするか考えましょう。

Node.jsのInstallが完了しました。Node.jsがInstallされているか確認してみます。同時にnpmがInstallされているか確認してみます。
node -v
npm -v
npmは、Node.jsのPackageを管理するApplicationです。

npmをFirewallの内側から使用する場合は、proxyの設定が必要になる場合が多いです。Firewallの設定は、npm config set~で行います。
npm config set proxy "http://192.168.1.1:8080/"
npm config set https-proxy "http://192.168.1.1:8080/"
npm config list
Proxy ServerのIP AddressやPort Numberは、Windowsの設定を参照します。proxy.pacを使った設定方法の場合は、proxy.pacをText FileとしてdownloadしてText Editorで設定内容を確認します。Browserにproxy.pacのURLを入力すればTextとしてDownloadすることができます。

ReactのWeb Applicationのひな形を作成するのに便利なTool、create-react-appを使って、Web Applicationを作ります。
まず、create-react-appをnpmを使ってInstallします。
続けて、create-react-appで、exportというApplicationを作成します。
npm install -g create-react-app
create-react-app export

実際にWeb Applicationを立ち上げましょう。
cd export
npm start
すると、DefaultのBrowserが立ち上がり、
http://localhost:3000/
を表示します。
お勧めのBrowserはGoogle Chromeです。

これで完了です。次回は、このApplicationをAWS S3にDeployします。