North Detail / ノースディテール

BLOG ブログ

ブログ
CATEGORY
TECH

Amplify で開発してみる #3 / Authentication と API (GraphQL) を連携した実装

こんにちは、tacckです。

シリーズの第三回目です。

Comment Any は、イベントごとにコメントを投稿できるようになりました。
最低限、これがあれば少人数で使う分には十分なものだと思います。

しかし、一回目で書いた通り「ミーティングやLTなど複数の人が集まった時に、コメントを気軽に投稿できるシステム」を作るのが目標です。
そのため、一つのイベントにある程度人数が集まりつつ、各ユーザーがイベントを自由に作成し、コメントの管理が行なえることが必要になってきます。

そして複数のユーザーを見分けるためには、認証機能によってユーザーの管理を行なう必要があります。

Amplify では、認証機能もとても簡単に導入することができます。
今回は認証機能を導入し、ユーザーごとの制御を入れていきましょう。

今回実装するのは、「認証機能の導入と認証機能によるユーザー管理」、「自分のイベントだけロック機能が使える」、「自分のイベントのコメントだけ削除できる」、というところです。

前回までのコードはこちらです。

GitHub Event機能追加ブランチ

Amplify で開発してみるシリーズ

* Amplify で開発してみる #1 / API (GraphQL) その1 テーブル一つの実装

* Amplify で開発してみる #2 / API (GraphQL) その2 テーブル連携した実装

* Amplify で開発してみる #3 / Authentication と API (GraphQL) を連携した実装 (今回)

* Amplify で開発してみる #4 / Subscription を使った API (GraphQL) のリアルタイムデータ連携

開発

Authentication (認証機能) 導入

まずは、認証機能を導入しましょう。

これは、 Amplify CLI で簡単に行なうことができます。

$ 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 commentanyXXXXXXXX locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

$

これだけで、 Amazon Cognito が提供するユーザー管理機能を、 Amplify から利用することができます。(今回も、色の薄くなった行が入力の必要なところです。)

登録フォームやパスワードの管理、といったところは Cognito で管理されるため、我々開発者はこれを利用するだけで安全にユーザーの認証機能を利用することができるようになります。

では、これを AWS へ push しておきましょう。

$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name      | Operation | Provider plugin   |
| -------- | ------------------ | --------- | ----------------- |
| Auth     | commentanyXXXXXXXX | Create    | awscloudformation |
| Api      | commentany         | No Change | awscloudformation |
? Are you sure you want to continue? Yes
⠴ Updating resources in the cloud. This may take a few minutes...

(snip)

UPDATE_COMPLETE amplify-commentany-dev-XXXXXX AWS::CloudFormation::Stack Thu Jul 09 2020 18:07:25 GMT+0900 (GMT+09:00) 
UPDATE_COMPLETE apicommentany                 AWS::CloudFormation::Stack Thu Jul 09 2020 18:07:25 GMT+0900 (GMT+09:00) 
✔ All resources are updated in the cloud

$ 

ログイン済みの検知とルーティングの制御

認証機能が組み込めたので、「もし認証済みのユーザーがアクセスしたらどうするか」という制御が必要になります。

これは、まずログイン済みだ、という情報を取得すること、そしてその情報が取得できたユーザーだけが表示できるページを限定すること、といった内容になりますね。

これは、 VueRouter を使って解決します。 src/router/index.js を、下記のように修正しましょう。

import Vue from 'vue'
import VueRouter from 'vue-router'
import { Auth } from 'aws-amplify'
import store from '../store'

Vue.use(VueRouter)

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () =>
      import(/* webpackChunkName: "login" */ '../views/Login.vue'),
    meta: { isPublic: true },
  },
  {
    path: '/event/:eventId',
    name: 'EventDetail',
    component: () =>
      import(/* webpackChunkName: "event" */ '../views/EventDetail.vue'),
    props: true,
  },
  {
    path: '/',
    name: 'Event',
    component: () =>
      import(/* webpackChunkName: "event" */ '../views/Event.vue'),
  },
]

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

router.beforeEach(async (to, from, next) => {
  const user = await Auth.currentUserInfo()
  store.commit('setUser', user)
  if (to.matched.some(record => !record.meta.isPublic) && user === null) {
    next({ path: '/login' })
  } else {
    next()
  }
})

export default router

37〜45行目が処理の中心で、「ページ遷移をする前にどうするか」という判定を差し込んでいます。

38行目の Auth.currentUserInfo() で現在のユーザー情報を取得して、これに成功すればログイン済み、失敗(null)であればログイン前、という状態とします。

また、ログイン画面は誰もが表示できる必要があるので14行目で meta: { isPublic: true }, として属性を付加しています。

ルーティングの制御としては、これで完了です。

ログイン画面の実装

次はログイン画面です。

普段のWeb開発ならここでフォームを作り、バリデーションを作り、確認画面も作ってページ遷移の管理をして、、となりますが、 Amplify ではすでに用意されているので、難しいことは不要です。

<template>
  <v-container>
    <v-row justify="center">
      <v-col cols="auto">
        <amplify-authenticator>
          <amplify-sign-up
            slot="sign-up"
            :form-fields.prop="formFields"
          ></amplify-sign-up>
          <amplify-sign-out></amplify-sign-out>
        </amplify-authenticator>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import { Auth, Hub } from 'aws-amplify'

export default {
  data: function() {
    return {
      formFields: [
        {
          type: 'username',
          label: 'Username',
          placeholder: 'Username',
          required: true,
        },
        {
          type: 'password',
          label: 'Password',
          placeholder: 'Password',
          required: true,
        },
        {
          type: 'email',
          label: 'Email Address',
          placeholder: 'Email',
          required: true,
        },
      ],
    }
  },
  created: function() {
    Hub.listen('auth', this.changeState)
  },
  beforeDestroy: function() {
    Hub.remove('auth', this.changeState)
  },
  methods: {
    changeState: async function(data) {
      const user = await Auth.currentUserInfo()
      switch (data.payload.event) {
        case 'signIn':
          this.$store.commit('setUser', user)
          this.$router.push('/')
          break
        case 'signOut':
          this.$store.commit('setUser', null)
          break
      }
    },
  },
}
</script>

5〜11行目が、 Amplify が用意してくれている 認証関係のコンポーネントです。

認証の状態 ( Sign In 前か、 Sign In 済みか、Sign Up 中か、、など)に合わせて、コンポーネントの表示を切り替えてくれます。

コンポーネントの内容を調整したい場合は、コンポーネントを指定してパラメータを渡します。
ここでは、6〜9行目の amplify-sign-up コンポーネント( Sign Up / ユーザー登録 )でフォームの内容に usernamepasswordemail の3つを指定しています。(23〜42行目)
指定しない場合、不要な入力項目である電話番号が表示されるため、必須の三項目だけになるようにしています。

ちなみに、10行目の amplify-sign-out コンポーネントは指定しないとサインアウトボタンが表示されないため、指定必須のコンポーネントとなります。

ここまでできていれば、ログイン画面の機能としては十分です。

45〜64行目の処理は、ログイン ( Sign In / Sign Up ) が完了した場合に状態の変化を検知してイベント一覧画面へ自動的に遷移させる、というものです。
(ついでに、ユーザー情報を保持したりクリアしたりもしています。)

この範囲は実装しなくても、システムの動作上問題ありません。

ログイン画面で各コンポーネントごとの表示は、下記のようになります。

Sign In
Sign In

Sign Up
Sign Up

Sign Out
Sign Out

Schema 設定

続いて、 Schema にも認証による権限制御を入れていきます。

これは、データについて「誰が」「何を」操作可能とするか、というものです。

今回であれば、「イベントの作成は各ユーザーが可能」、「イベントの更新(ロック処理)はイベントの作成者のみが可能」、「イベントの一覧表示はログイン済ユーザーが可能」、「コメントはログイン済みユーザーなら自由に操作可能」といった制御を入れていきます。

type Event
  @model
  @auth(
    rules: [
      { allow: owner }
      { allow: private, operations: [read] }
    ]
  ) {
  id: ID!
  name: String
  active: Boolean!
  comments: [Comment] @connection(keyName: "listCommentByEventId", fields: ["id"])
  createdAt: AWSDateTime
  owner: String
}

type Comment
  @model
  @auth(
    rules: [
      { allow: private }
    ]
  )
  @key (name: "listCommentByEventId", fields: ["eventId"], queryField: "listCommentByEventId")
{
  id: ID!
  eventId: ID!
  comment: String!
  likes: Int!
  name: String
  updatedAt: AWSDateTime
}

上記のように @auth ディレクティブによって、データ操作の権限制御を行なっています。

まず、イベントの方です。

5行目「 owner(作成者) は全ての権限 ( read 、create 、 update 、 delete ) を許可」、
6行目「 private(ログイン済ユーザー) は読み込み権限を許可」、
となっています。(5行目、特に指定しなければ全権限を許可、となります。)

これによって、「イベントの作成は各ユーザーが可能」、「イベントの更新(ロック処理)は作成したイベントのみ可能」、「イベントの一覧表示はログイン済ユーザーが可能」、の部分を実現しています。

この辺りの権限制御を Schema の宣言だけで実現できるため、ロジックミスによるバグなども抑えられることになります。

一方、コメントの方です。

21行目で「 private(ログイン済ユーザー) は全ての権限 ( read 、create 、 update 、 delete ) を許可」、
となっています。

これによって、「コメントはログイン済みユーザーなら自由に操作可能」を実現しています。

「あれ、自由ってことは誰でも削除できるの?」と思った方、正解です。

このシステムのコメント削除機能は、イベントの作成者が自分のイベントについてのみ誰が書いたコメントでも自由に削除できる、というものです。

しかし、イベントの作成者とコメントの作成者は別の人の可能性があるので、 owner での宣言では実現できず、 private で許可しています。

そのため、ここはロジック ( JavaScript ) によって実現する必要がありますが、もし悪意のあるユーザーがイベントに参加していた場合、不正にコメントを操作できてしまう可能性があるわけです。

もちろんそれを回避することはできるわけですが、その辺りは応用編の記事を書けるときに書いてみたいと思います。

@auth ディレクティブについては、オフィシャルのドキュメントも参照してみてください。

API (GraphQL) Directives @auth

API の認証方法変更

Schema で @auth ディレクティブを使用することになったので、 API の認証方法を Cognito を使ったものに変更しましょう。

$ amplify update api
? Please select from one of the below mentioned services: GraphQL
? Select from the options below Update auth settings
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Configure additional auth types? No

GraphQL schema compiled successfully.

Edit your schema at /Users/[YOUR_DIRECTORY]/comment-any/amplify/backend/api/commentany/schema.graphql or place .graphql files in a directory at /Users/[YOUR_DIRECTORY]/comment-any/amplify/backend/api/commentany/schema
Successfully updated resource
$ 

ここまで進んだら、再度 AWS へ push しておきましょう。

$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name      | Operation | Provider plugin   |
| -------- | ------------------ | --------- | ----------------- |
| Api      | commentany         | Update    | awscloudformation |
| Auth     | commentanyXXXXXXXX | No Change | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.

Edit your schema at /Users/[YOUR_DIRECTORY]/comment-any/amplify/backend/api/commentany/schema.graphql or place .graphql files in a directory at /Users/[YOUR_DIRECTORY]/comment-any/amplify/backend/api/commentany/schema
? Do you want to update code for your updated GraphQL API Yes
? Do you want to generate GraphQL statements (queries, mutations and subscription) based on your schema types?
This will overwrite your current graphql queries, mutations and subscriptions Yes
⠦ Updating resources in the cloud. This may take a few minutes...

(snip)

UPDATE_COMPLETE amplify-commentany-dev-XXXXXX AWS::CloudFormation::Stack Fri Jul 10 2020 11:10:16 GMT+0900 (GMT+09:00) 
UPDATE_COMPLETE apicommentany                 AWS::CloudFormation::Stack Fri Jul 10 2020 11:10:15 GMT+0900 (GMT+09:00) 
✔ Generated GraphQL operations successfully and saved at src/graphql
✔ All resources are updated in the cloud

GraphQL endpoint: https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.ap-northeast-1.amazonaws.com/graphql

$ 

イベント画面の機能修正

では、イベント画面を認証機能を前提とした実装に修正していきましょう。

とはいえ、権限制御自体は Schema で宣言しているので、イベントが「自分のものかどうか」の判定が必要なくらいです。

そして、基本的な実装はモックで対応済みなので、下記の修正を行えば対応完了です。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: null,
  },
  mutations: {
    setUser: function(state, user) {
      state.user = user
    },
  },
  actions: {},
  modules: {},
})
  created: async function() {
    this.userName = this.$store.state.user.username

    const items = await API.graphql(graphqlOperation(listEvents)).catch(err =>
      console.error('listEvents', err),
    )
    this.events = items.data.listEvents.items
  },
    addEvent: async function() {
      if (!this.newEventName || this.newEventName.length <= 0) {
        return
      }

      const input = {
        name: this.newEventName,
        active: true,
      }

      const item = await API.graphql(
        graphqlOperation(createEvent, { input: input }),
      ).catch(err => console.error('createEvent', err))
      const savedEvent = item.data.createEvent

      this.events.push(savedEvent)

      this.newEventNameDialog = false
      this.newEventName = ''
    },

二つ目の addEvent メソッドの中は、6〜9行目内のパラメータの owner を削除しています。
これは、イベント追加時に Amplify (が使っている AppSync ) が自動で追加してくれるからです。

イベント詳細画面での機能修正

最後に、イベント詳細画面を認証機能を前提とした実装に修正していきましょう。

とはいえ、基本的な実装はモックで対応済みなので、下記の修正を行えば対応完了です。

  created: async function() {
    this.linkUrl = location.href
    this.userName = this.$store.state.user.username

    const item = await API.graphql(
      graphqlOperation(getEvent, { id: this.eventId }),
    ).catch(err => console.error('getEvent', err))
    this.event = item.data.getEvent
    this.comments = this.event.comments.items
  },

やってみたけどうまくいかない、など、細かなコード差分をみたい場合にはこちらを参照してみてください。

GitHub 認証機能 実装差分

うまくできたでしょうか?

うまくいけば、下記の動画のように Sign In 、イベント追加して自分のイベントだけロック、自分のイベントだけコメント削除、といったものが動作するようになるはずです。

今回の対応で利用者の名前 (username) を扱うことができるようになったので、例えばコメントに投稿者を表示する、という応用もできるようになります。

そういった拡張も試してみてください。

まとめ

今回は、 Authentication の機能をみていきました。こちらも API (GraphQL) と同様とても手軽に導入し、認証機能を簡単に実装することができました。

Web開発での認証機能の作り込みは、思ったよりも大変になることが多いと思います。そういった部分を Amplify が肩代わりしてくれることで、とても手軽に機能追加ができてしまいます。

今回は複数のユーザー間での操作後の状態確認は、ブラウザのリロードが必要でした。
次回はこれのリアルタイム連携を行ない、 Comment Any を完成させましょう。

tacck
WRITER:tacck
元 技術推進Group Group Leader

現在は株式会社ノースディテールを離れて、
エバンジェリストとして技術啓蒙や勉強会の開催、各種プロジェクトに参画しています。
機会があれば、ノースディテールでのプロジェクトに参加できればと思っています。

言語は問わずに対応しますが、心はPHPer。
フロントエンド・バックエンド・インフラ・スマホアプリなどを、
「垣根を超えて」どう作るか、を考えるのが好きです。

好きなフィギュアスケートの技はスプレッド・イーグル。
主な記事 一覧へ

一覧へ

IS 501383 / ISO 27001