aws amplifyの認証にamplifyの外で取得した情報を用いたくて頑張った話

aws amplifyで構築しているシステムがありますが、これはとあるシステム(php)のサブシステム的な扱いとなっており、認証自体はそのとあるシステムで行いたいという話があります。

## 環境

### とあるシステム

- php
- 認証はアプリケーション内で完結
- Mysql
- www.example.com/

### サブシステム(aws amplify)

- aws amplify
- cognito
- vuejs
- Dynamdb
- Appsync(graphQL)
- www.example.com/sub/

## やりたいこと

- とあるシステムで認証済みの状態でサブシステムに遷移してもログイン状態を保持していたい。
- ただしシステム間は疎結合

## やろうと思うこと

- とあるシステムでの認証時にcognitoへのログインを同時におこない、cognitoのアクセストークンを取得
- 取得したトークンはcookie(ドメイン x path x SSLでセキュリティ担保）に持つ

## それではやってみよう

前置きはおしまい。
今回、 **とあるシステム** に関してはスルーします。トークンはある前提でamplify側だけの検証です。
ではamplify側の構築からやっていきます。導入するものは `auth` と `api` ですね。

## aws amplify初期設定

知ってる前提、というか自分の検証目的メインの記事なので細かい説明などは行いませんが、簡単にamplifyが何かというと、

 **firebaseのaws版** です



・・いや、それだとあんまりか。

 **cognitoやstorageといったaws周りの仕組みをフロントエンドのフレームワークで使い倒せる仕組み**

といった所がニアリーイコールですかね。正確には違う所も多いけど。簡単なことをやる分には簡単にやれちゃいます。

基本、この通りにやるとモック的なシステムは構築できます。
https://docs.amplify.aws/start/q/integration/vue
前提として下記が必要

> Node.js v10.x or later
npm v5.x or later
git v2.14.1 or later

(※nodeとかがglobalに入ってたりするとsudoが必要なことが多いかも)

```bash
$ npm install -g @aws-amplify/cli
$ npm install -g @vue/cli
$ vue create myppap

Vue CLI v4.4.6
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? No

$ cd myppap
$ amplify init

Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project myppap
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script serve
Using default provider  awscloudformation
AWS access credentials can not be found.
? Setup new user No

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

? accessKeyId:  ********************
? secretAccessKey:  ****************************************
? region:  ap-northeast-1
Adding backend environment dev to AWS Amplify Console app: xxxxxxxx

$ npm install aws-amplify @aws-amplify/ui-vue　aws-amplify-vue
$ cat - << EOS > src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import Amplify from 'aws-amplify';

import '@aws-amplify/ui-vue';
import aws_exports from './aws-exports';

Amplify.configure(aws_exports);

new Vue({
          el: '#app',
          router,
          components: { App },
          template: '<App/>'
})
EOS
$ npm run serve
```

とりあえず、最小限な状態のamplifyはこれで動く。

![vuetop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/aa0325a6-6b88-2f22-8695-ef5d7878cf2a.png)

次にauthを追加してpublish
https://docs.amplify.aws/start/getting-started/auth/q/integration/vue

```bash
$ amplify add auth

Using service: Cognito, provided by: awscloudformation

 The current configured provider is Amazon Cognito.

 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added resource myppap70d7df9c locally

$ amplify push
```

これでcognitoのリソースは作られる。
次に普通にsign inをできるようにはしておく。
App.vueのテンプレを下記に。（ほぼtutorial通り)
AmplifyのデフォルトのUIが表示されます。
(※どちらかというとHome.vueに書くべきだったけど・・ここは見逃して。。）

```vue:App.vue
<template>
  <div class="home">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
    <amplify-authenticator>
      <!-- The rest of your app code -->
      <amplify-sign-out></amplify-sign-out>
    </amplify-authenticator>
    <router-view/>
  </div>
</template>
```

次に `src/router/router.js` に下記の感じにする。
単純に、signin済みなら/aboutに、違えばトップにという動作をするだけ。

```
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

import { Amplify, Auth } from 'aws-amplify'
import * as AmplifyModules from 'aws-amplify'
import { AmplifyEventBus, AmplifyPlugin } from 'aws-amplify-vue'

Vue.use(AmplifyPlugin, AmplifyModules)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/About',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackchunkname: "about" */ '../views/About.vue'),
    meta: { requiresauth: true}
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

router.beforeEach(async (to, from, next) => {
  if (to.matched.some(record => record.meta.requiresauth)) {
    try {
      await Auth.currentAuthenticatedUser()
      console.log('in');
      next();
    } catch (err) {
      console.log('out');
      next({ path: '/', query: { redirect: to.fullPath }});
    }
  }
});

AmplifyEventBus.$on('authState', info => {
  if (info === 'signedIn') {
    console.log('/about');
    router.push({name: 'About'});
  } else {
    console.log('/');
    router.push({name: 'Home'});
    window.location.href = process.env.BASE_URL;
  }
});

export default router
```

あと `src/views/About.vue` には下記の一行を適当に入れておく。

```
<amplify-sign-out/>
```

これでrun serveするとこうなる。

![vuetop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/64ae392e-5302-3503-4ede-e826ad6ef4c2.png)

さっそく、 **Create acount** しておきましょう。
とりあえず、 `TestUser:Test1234` にしてしまった。あとで消すのを忘れないようにしないと。

![vuetop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/111fb59a-dc1c-f6af-9b88-fbe867f26c61.png)

そんなわけでsign inはできました。
だいぶ簡素化して書いちゃってますが、このあたりの情報はぐぐればいっぱいでてきますので。

## cliで認証

さて長い前置きは終わり。
今回はcognitoのユーザープールに対してサインインして、返ってきたアクセストークン(1時間有効)をつかって、amplify上の認証が必要なページを閲覧したいという内容。
で、このアクセストークンを得るところは今回CLIでやってしまう。
コマンド一発ですけどね。なお、awscliは必須です。

https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/admin-initiate-auth.html

>   admin-initiate-auth
--user-pool-id <value>
--client-id <value>
--auth-flow <value>

user-pool-idとclient-idはsrc/aws-exports.jsに書かれてるやつを使えばOK。
あとcognitoのconsoleを開き、*管理 API のユーザー名パスワード認証を有効にする* も有効にしないといけない。

```
aws cognito-idp admin-initiate-auth --user-pool-id ap-northeast-1_7xxxxxxxx --client-id vunghxxxxxxxxxxxxxxxxxx --auth-flow ADMIN_NO_SRP_AUTH  --auth-parameters "USERNAME=TestUser,PASSWORD=Test1234"
```

で、いろいろと返ってくるけど、ほしいのはIdTokenと、AccessToken、RefreshToken。

```
aws cognito-idp admin-initiate-auth --user-pool-id ap-northeast-1_7xxxxxxxx --client-id vunghxxxxxxxxxxxxxxxxxx --auth-flow ADMIN_NO_SRP_AUTH  --auth-parameters "USERNAME=TestUser,PASSWORD=Test1234" | jq .AuthenticationResult.IdToken
```

これで認証はできるしTokenは取れた。
ちなみに、このIdTokenとかにユーザー名とかが入ってたりする。

```
$ echo ${ACCESSTOKEN} | cut -d'.' -f 2 | base64 -d
{"sub":"11d9f7b0-xxxx-40c6-8ca1-xxxxxxx","event_id":"d604176f-xxxx-4e37-a1d6-xxxxxxxxx","token_use":"access","scope":"aws.cognito.signin.user.admin","auth_time":1595671549,"iss":"https:\/\/cognito-idp.ap-northeast-1.amazonaws.com\/ap-northeast-xxxxxxxx","exp":1595675149,"iat":1595671549,"jti":"c04785f1-575a-48e3-abb8-xxxxxxxx","client_id":"vunxxxxxxxxx","username":"TestUser"}
```
IdTokenの方にはemailなんかもあった。

## Cognitoを手動で組み込む。

で、Tokenを使って `/about` のページを開きたい。

参考: https://blog.u-chan-chi.com/post/vue-cognito/

```
$ npm install --save amazon-cognito-identity-js aws-sdk
```

・・っていろいろやってみてユーザー情報取ったりはできたものの・・AmplifyのAuthと連携するのがうまくいかなかった。。



**そんなわけで！**

## 強引にやったりました。

tokenとかはほんとはセッションに埋め込む予定だけど、今回は単純にスクリプトに埋め

```
const IdToken = 'eyxxxxxxxx'
const AccessToken = 'eyxxxxxxxx'
const RefreshToken = 'eyxxxxxxxx'
const userName = 'TestUser';
```

そしてこれをlocalStorageにぶっこんでしまう！

```
import aws_exports from '../aws-exports';
const userName = 'TestUser';
const cognitoClientId = aws_exports.aws_user_pools_web_client_id
const makeKey = (name) => `CognitoIdentityServiceProvider.${cognitoClientId}.${userName}.${name}`;
localStorage.setItem(makeKey('accessToken'), AccessToken);
localStorage.setItem(makeKey('idToken'), IdToken);
localStorage.setItem(makeKey('refreshToken'), RefreshToken);
localStorage.setItem(makeKey('clockDrift'), 1);
localStorage.setItem(`CognitoIdentityServiceProvider.${cognitoClientId}.LastAuthUser`, userName);
```

なお、このへんから拾った。
https://stackoverflow.com/questions/61644513/setting-aws-amplify-user-session-manually

![vuetop1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/95843700-0127-5571-eb46-5bf61586291d.png)


なんかログインはできた。
めっちゃ裏口すね。。
なんか正攻法あったらだれか教えてください。。
というかこれやるなら別アプリ（同じドメイン）で直接localStorageに入れてしまえばいいのかもしれない。

## api(appsync)を叩く

最後にこのインチキ臭い方法で入り込んだ状態でapi叩けるか確認しておきますね。

``` 
$ amplify add api

Scanning for plugins...
Plugin scan successful
? Please select from one of the below mentioned services: GraphQL
? Provide API name: myppap
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? Yes
Please edit the file in your editor: /mnt/c/Users/masra/myppap/amplify/backend/api/myppap/schema.graphql
? Press enter to continue


The following types do not have '@auth' enabled. Consider using @auth with @model
         - Todo
Learn more about @auth here: https://docs.amplify.aws/cli/graphql-transformer/directives#auth


GraphQL schema compiled successfully.
```

そしてamplify pushしてしまう。(mockとかも使えるみたいではあるけど)

```
$ amplify push

? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
```

そして[このあたり](https://qiita.com/nakayama_cw/items/6d3514b51c5fbf9ba450) を参考（というかほぼコピペ）にさせていただきつつ、About.vueにTodoアプリのコードを差し込みます。

```js:src/views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <amplify-sign-out/>

    <div class="createTodo">
      <input v-model="name" name="name" />
      <input v-model="description" name="description" />
      <button v-on:click="createTodo()">Create Todo</button>
    </div>
    <template v-for="todo in todos">
      <div>
        <h3>{{todo.name}}</h3>
        <p>{{todo.description}}</p>
      </div>
    </template>
  </div>
</template>

<script>
import Vue from 'vue'
import { API, graphqlOperation} from "aws-amplify"
import { listTodos } from "../graphql/queries"
import { createTodo } from "../graphql/mutations"

export default {
  name: 'Todo',
  data () {
    return {
      todos: [],
      name: "",
      description: ""
    }
  },
  mounted: async function () {
    let todos = await API.graphql(graphqlOperation(listTodos))
    this.todos = todos.data.listTodos.items
  },
  methods: {
    createTodo: async function () {
      if ((this.name === "") || (this.description === "")) return
      const todo = {name: this.name, description: this.description}
      try {
        const todos = [...this.todos, todo]
        this.todos = todos
        this.name = "";
        this.description = "";
        await API.graphql(graphqlOperation(createTodo, {input: todo}))
      } catch (error) {
      }
    }
  },
}
</script>
```

![vuetop1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/bf7d9b29-136d-23e4-6513-4c9191ab06db.png)

はい、でけたでけたー。わーい。わぁー。