GraphQLにおける認証認可事例（Auth0 RBAC仕立て）

# お題
以下の組み合わせで作成しているWebアプリケーションにAuth0による認証認可機能を入れてみる。
認証はID（メールアドレス）とパスワードによる方式を採用。

■通信方式
　・GraphQL
■フロントエンド
　・Vue.js
　・Nuxt.js
　・TypeScript
　・Apollo
■バックエンド
　・Golang
　・gqlgen

挙動としては以下のようになる。

（１）ログイン前。「LOG IN」ボタンを押下する。
![Screenshot at 2020-05-23 15-31-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/ecec0644-24ce-ebff-9bd8-569fc3ba5e17.png)

（２）Auth0のログイン画面（カスタマイズもできるらしい）に飛ばされる。メアドとパスワードを入れて「Continue」ボタンを押下する。
![Screenshot at 2020-05-23 15-32-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/3c720952-b219-edd7-3c07-39161ca5e6e1.png)

（３）認証が通るとアクセストークン付きでコールバック（あらかじめAuth0に設定しておく）が呼ばれる。
![Screenshot at 2020-05-23 15-33-43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/8ce0fb80-b4a9-389a-f694-bfd7ae2de77a.png)

（４）ログイン後のトップ画面を表示する。
![Screenshot at 2020-05-23 15-34-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/886df75a-105c-0399-a017-98709f2fc317.png)

（５）サイドメニューから「Movie」を選択して動画一覧ページに遷移する。
![Screenshot at 2020-05-23 15-34-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/b7605ab1-6e19-cbc8-defd-b7c9e3cb9296.png)

（６）ログインユーザに権限があるので動画一覧ページが表示される。
![Screenshot at 2020-05-23 15-34-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/ee402025-3d6a-9875-39bb-a67ef9e9e470.png)

（７）いったんログアウトする。
![Screenshot at 2020-05-23 15-35-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/f5315971-31aa-a94d-5d26-a24d944e6685.png)

（８）別の（動画一覧表示権限が与えられていない）ユーザでログインする。
（９）サイドメニューから「Movie」を選択して動画一覧ページに遷移する。
（１０）ログインユーザに権限がないので動画一覧ページが表示されない。
![Screenshot at 2020-05-23 15-38-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/0be2d767-77f3-2d02-3127-6c5de5a78af9.png)

画面的には、そっけないエラーページだけど、コンソールログを見ると権限がない旨のエラーログが出ている。
![Screenshot at 2020-05-23 15-39-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/c8d9f3a9-847d-d4c9-3518-6b78cf8227f4.png)

# 前提

- Auth0のSign in方法から各機能の具体的な操作方法など特に細かくは説明しない。
- Vue.jsやGolangの実装方法について１行単位で詳しく解説はしない。

# 開発環境
## # OS - Linux(Ubuntu)
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.4 LTS (Bionic Beaver)"
```

## # フロントエンド
### Nuxt.js
```
$ cat yarn.lock | grep "@nuxt/vue-app"
    "@nuxt/vue-app" "2.12.2"
"@nuxt/vue-app@2.12.2":
  resolved "https://registry.yarnpkg.com/@nuxt/vue-app/-/vue-app-2.12.2.tgz#cc4b68356996eb71d398a30f3b9c9d15f7d531bc"
```

### パッケージマネージャ - Yarn
```
$ yarn -v
1.22.4
```

### IDE - WebStorm
```
WebStorm 2020.1
Build #WS-201.6668.106, built on April 7, 2020
```

## # バックエンド
### # 言語 - Golang
```
$ go version
go version go1.13.9 linux/amd64
```

### IDE - Goland
```
GoLand 2020.1.2
Build #GO-201.7223.97, built on May 1, 2020
```

# 関連記事索引
- 第12回「[GraphQLにおけるRelayスタイルによるページング実装再考(Window関数使用版)](https://qiita.com/sky0621/items/957c010ba01c7a756ef7)」
- 第11回「[Dataloadersを使ったN+1問題への対応](https://qiita.com/sky0621/items/621f075e4257270a9e02)」
- 第10回「[GraphQL(gqlgen)エラーハンドリング](https://qiita.com/sky0621/items/affc833e770fee8b576e)」
- 第９回「[GraphQLにおける認証認可事例（Auth0 RBAC仕立て）](https://qiita.com/sky0621/items/efbb8fa085094230d0fd)」
- 第８回「[GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例](https://qiita.com/sky0621/items/5c2c565678c102d9d4ed)」
- 第７回「[GraphQLにおけるRelayスタイルによるページング実装（後編：フロントエンド）](https://qiita.com/sky0621/items/fdcdaefeef7cf4b750ac)」
- 第６回「[GraphQLにおけるRelayスタイルによるページング実装（前編：バックエンド）](https://qiita.com/sky0621/items/1e8823200633f2c46013)」
- 第５回「[DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動](https://qiita.com/sky0621/items/58fa2bcfc0935f6186cb)」
- 第４回「[graphql-codegenでフロントエンドをGraphQLスキーマファースト](https://qiita.com/sky0621/items/f03631f0309f5c375b1d)」
- 第３回「[go+gqlgenでGraphQLサーバを作る（GORM使ってDB接続）](https://qiita.com/sky0621/items/32e3e26189589080e492)」
- 第２回「[NuxtJS(with Apollo)のTypeScript対応](https://qiita.com/sky0621/items/0297f5a026be380a5929)」
- 第１回「[frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る](https://qiita.com/sky0621/items/8abd445edba347e8f6f1)」

# 全ソース
## バックエンド分
https://github.com/sky0621/fs-mng-backend/tree/v0.2

## フロントエンド分
https://github.com/sky0621/fs-mng-frontend/tree/v0.2

# 実践

## Auth0の設定
### ApplicationとWebAPIの作成
Auth0は、ドキュメントやチュートリアルが充実してるので、ちゃんと読めば、わりかしハマることは少ないと思う。

#### フロントエンドの設定について
フロントエンドはモードとしてはSSRなんだけど、形式としてSPAを選択している。
それが正しいのかは、ちょっとわかっていない。
![screenshot-manage.auth0.com-2020.05.23-17_07_36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/85d323cc-b0bb-ef6d-4549-69a6f5cf4812.png)

ログインやログアウトの処理でAuth0のSDKを使うわけだけど、その際の接続先に関する情報として以下が必要。
![screenshot-manage.auth0.com-2020.05.23-16_44_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/a2ed6e66-67ef-5d05-8d2b-4f71a35bd926.png)

また、Auth0での認証処理が成功した後に再びアプリ側に戻ってくるためのコールバックURLを指定。
あと、ログアウト後の遷移先URLも。
![screenshot-manage.auth0.com-2020.05.23-16_52_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/acff2268-5fc8-d927-0d60-bc31e168b2a5.png)

#### バックエンドの設定について
バックエンドはWebAPIとして機能するのでAPIｓで設定。
![screenshot-manage.auth0.com-2020.05.23-16_09_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/22537ea5-6005-dd0b-9aff-4970807ef3a7.png)

フロントエンドから渡されたJWTのチェックをするために Identifier を使う。
![screenshot-manage.auth0.com-2020.05.23-17_12_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/bc6823b1-feed-48e1-6449-61318f3c71b8.png)

あと、今回は認可機能の実装のために、Auth0が提供しているRBACの仕組みを使うので、Auth0上で定義したRoleとPermissionがJWTのClaimに積まれるようにしておく。
![screenshot-manage.auth0.com-2020.05.23-17_15_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/fa75cd5f-6a97-7770-c575-2eb647229f68.png)

### RoleとPermissionの定義
WebAPIが提供する（予定の）各機能にPermissionを定義していく。
この部分は、WebAPIがどんな機能を提供するかや、どういった粒度でPermission制御が必要かについて設計した上で設定する。
今回は仮に「組織」、「ユーザ」、「コンテンツ（今回だと動画コンテンツ）」といったリソースに対するCRUD操作それぞれに認可制御をする想定で設計。
また、APIを叩いた（認証済み）ユーザが、自分で所有（自分で登録したものや他の人から権限を与えられたもの）するものしか権限がないのか、すべてのリソースに対する権限があるのかという観点でも分けてみた。
まあ、実際のアプリケーションでは、もっといろんな状況があると思うので、このあたりは、要件によりけり。
また、Auth0では、別にAuth0上のWebAPI定義にPermissionを細かく作っていかなくても、実際に動かすアプリケーションの方で機能ごとにPermissionを定義して、それをユーザ作成時にアプリケーションの機能としてAuth0ユーザのMetadataに貼り付けるといったこともできる。
柔軟に制御したいといった場合は、Metadata使う方式の方が後々困らないかもしれない。

ともあれ、とりあえず今回は以下みたいな感じでPermissionを定義。
![screenshot-manage.auth0.com-2020.05.23-17_43_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/f6b2f908-15e0-6b4e-5111-47597fac2c12.png)
　　　　　　　　　　　　・
　　　　　　　　　　　　・
　　　　　　　　　　　　・
![screenshot-manage.auth0.com-2020.05.23-17_45_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/366ba34a-2b62-3688-fcb7-8a536295a27c.png)

で、ユーザにはそれぞれのPermissionを１つ１つ付与していくのではなくRoleを付与するので、Roleを作成。
これも、Roleの定義は作りたいアプリケーションの要件によってまちまちなので、今回のは参考程度に見てもらえると。
![screenshot-manage.auth0.com-2020.05.23-17_29_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/e800dc7f-c1ae-3406-1b21-43ef6c8bd654.png)

このRoleに先ほどのPermissionを割り当てていく。
要するに、このRoleはこのWebAPIに関してどれだけの機能を叩くPermissionを有しているかを設定していく。
先ほどのPermissionもそうだけど、こういった作業を画面でポチポチやるのが地味に時間を要する。
１回きりといえばそうなんだけど、例えば、実際の開発現場では、開発環境、ステージング環境、本番環境を用意し、Auth0のテナントもそれぞれに用意するはず。
各環境用にこの画面ポチポチをつどつどやれって言われたら、正直しんどい。
まあ、たぶん、設定のExport/Import機能くらいあるんだろう、きっと。
Auth0の設定画面になくても、Auth0が用意するManagement系のAPIを使うと、わりとさくっとできるかな。

話がズレた。とりあえずRoleごとのPermission割り当てだけど、AdminとNoneの事例を。
![screenshot-manage.auth0.com-2020.05.23-17_54_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/008fd11e-8347-efd6-8cb0-990202f7e285.png)
　　　　　　　　　　　　・
　　　　　　　　　　　　・
　　　　　　　　　　　　・

Adminは要するに全Permission持ってるということ。

![screenshot-manage.auth0.com-2020.05.23-17_54_31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/404068c0-0069-2843-f733-efc5dc8e32a3.png)

### Userを作成
このあたりは実際のサービスなら、まず、１人だけAdmin的な権限のあるユーザを作成。
Adminの人だけが触れる管理画面でも用意して、Auth0のManagement系APIを使って、別のRole別のユーザを作成していくといった流れになるんだろう。
今回は、まだそういう機能は用意してないので、とりあえず適当に３ユーザくらいAuth0の画面から作成。
![screenshot-manage.auth0.com-2020.05.23-18_01_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/a9ddb550-3db0-11c3-c287-9b16a7e5b393.png)

## フロントエンド実装
Auth0ではSPAアプリ用にSDKが用意されている。
でも、今回ぐらいの例だとNuxtが提供しているAuthモジュールを使うことで事足りるので、SDKは使わない。

### nuxtjs/auth導入
以下参照。
https://auth.nuxtjs.org/guide/setup.html

今回用の設定は下記。

```js:src/nuxt-config.js
export default {
  mode: 'universal',
　　　〜〜〜
  modules: [
　　　〜〜〜
    '@nuxtjs/apollo',
    '@nuxtjs/auth'
  ],
　　　〜〜〜
  apollo: {
　　　〜〜〜
    tokenName: 'auth._token.auth0',
    authenticationType: '' // default の Bearer だと「Bearer: Bearer」というように重複が起きるため
  },
　　　〜〜〜
  auth: {
    redirect: {
      login: '/login',
      logout: '/login',
      callback: '/callback',
      home: '/'
    },
    strategies: {
      auth0: {
        domain: process.env.AUTH0_DOMAIN,
        client_id: process.env.AUTH0_CLIENT_ID,
        audience: process.env.AUTH0_AUDIENCE
      }
    },
    plugins: ['~/plugins/auth.ts']
  },
　　　〜〜〜
```

Auth0用の設定事例は下記に記載している。
https://auth.nuxtjs.org/providers/auth0.html#usage

`domain`と`client_id`はAuth0のフロントエンド分のApplication設定にて表示されていたやつ。
`audience`がちょっとわかりづらいけど、バックエンドのWebAPI設定にて表示されていた「`Identifier`」の値を指定。
このへんの説明は↓に少しだけ書かれている。
https://auth.nuxtjs.org/providers/auth0.html#obtaining-client-id-domain-and-audience

`client_id`とか晒したくないので[nuxtjs/dotenv](https://github.com/nuxt-community/dotenv-module)を使ってる。
あと、GraphQLライブラリとして[Apollo](https://github.com/nuxt-community/apollo-module)を使っていて、

`authenticationType`はデフォルトだと`Bearer`と付くようで、そのままバックエンドに送ると以下のようになってしまう。
なので、明示的にブランクを指定して`Bearer`が重複しないようにしている。

```
authorization: Bearer Bearer eyJhbVZn〜〜〜
```

【参考】
https://stackoverflow.com/questions/61277352/nuxt-apollo-module-request-authorization-header-with-double-bearer

### ログインページ

#### 関連ソース

```
$ tree
.
├── layouts
│   └── unCertified.vue
├── pages
|   ├── callback.vue
│   ├── login.vue
```

#### 未認証時専用のレイアウト

```vue:src/layouts/unCertified.vue
<template>
  <v-app dark>
    <v-container>
      <nuxt />
    </v-container>
  </v-app>
</template>
```

#### ログインページ

```vue:src/pages/login.vue
<template>
  <v-row justify="center">
    <v-col md="auto">
      <div>fs-mng-app</div>
    </v-col>
    <v-col md="auto">
      <v-btn @click="login">Log in</v-btn>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component({ layout: 'unCertified' })
export default class LoginPage extends Vue {
  async login() {
    try {
      await this.$auth.loginWith('auth0')
    } catch (err) {
      this.$toast.error(err)
    }
  }
}
</script>
```

`nuxt.config.js`にて`auth`の名で設定した内容を踏まえたオブジェクトが`$auth`という名前でアクセスできる。
で、`$auth.loginWith('auth0')` と書いただけでAuth0へ認証をかけにいくことができる。
つまり、先述の↓に飛ばしてくれる。
![Screenshot at 2020-05-23 15-32-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/c240edea-fdd8-0e40-c7cc-5370b7eb9dfe.png)
なんだかもう、いろいろなことを隠蔽してくれていて、便利なんだけど怖い。

ログイン完了後、Cookieに`auth._token.auth0`という名前でトークンが書き込まれている。
![Screenshot at 2020-05-23 22-44-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/f69af3c5-d817-8af8-bac9-56a6a00b2077.png)

たぶん設定の問題だけど、デフォルトでLocalStorageの方にも同じ内容で書き込まれている。
![Screenshot at 2020-05-23 22-44-39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/864895c7-f2db-a9fd-90f3-f3dea81b6aa6.png)

#### コールバックページ
Auth0上での認証成功後に、アプリケーションに戻ってこないといけないので、そのためのページを用意。
Auth0の設定で用意したページを認証後のリダイレクト先として指定している。

```vue:src/pages/callback.vue
<template>
  <v-row justify="center">
    <v-col md="auto">
      <div>Trying to callback</div>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component({ layout: 'unCertified' })
export default class LoginPage extends Vue {
  created() {
    this.$router.push('/')
  }
}
</script>
```

やってることは、単にトップページに遷移するだけ。

#### 動画一覧ページ
ここが今回、認可処理を実装した部分ではあるのだけど、まずは、そもそも認証通ってないと、このページ開けないよの部分から。
バックエンドの方の実装は後ほど示すとして、とりあえずここでは、バックエンドではJWT（要するにCookieに積まれていたトークン）をヘッダに積んでないとエラーを返す実装が入っていることを前提とする。

まずは、先述の通りログイン完了後にCookieにトークンが入っている状態で動画一覧ページの表示を試みると、はい、開く。
![Screenshot at 2020-05-23 22-55-21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/e1dd69f8-3e31-15aa-fd69-e3b1f2bf9479.png)

このとき、ChromeのDevツールでNetworkタブより「Headers」を見てみると、`authorization`ヘッダにCookieに入っていたトークンが指定されている。
![Screenshot at 2020-05-23 22-57-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/b982f886-8503-3f33-f0c7-13196279688d.png)

つまり、`nuxt.config.js`に記述した `apollo`モジュールと`auth`モジュールの設定だけで、Auth0ログインにて取得したJWTをHTTPヘッダに積んでGraphQL通信する流れが出来上がっている。

さて、試しに、CookieとLocalStorageからトークンを消して、動画一覧ページを開こうとすると、
![Screenshot at 2020-05-23 23-08-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/700210b3-43ce-236a-34d6-8e823ec14958.png)
![Screenshot at 2020-05-23 23-09-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/aba883f1-e5ed-8f87-d54d-0dc46ed7a500.png)

失敗する。（ただ、これはどちらかというと通信自体に失敗した感じ）
![Screenshot at 2020-05-23 23-12-07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/84870582-6f98-1001-3b87-a94b28e7e53d.png)
![Screenshot at 2020-05-23 23-12-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/18327bc1-f467-cab0-4660-50bc563cabb4.png)

トークンを消すのではなく改ざんっぽく変えてみてから、再度、動画一覧ページを開こうとすると、
画面上は同じエラー画面だけど、通信ログがこのようになる。
「`401 Unauthorized`」これは、わかりやすい。
![Screenshot at 2020-05-23 23-16-47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/a693b93e-fcad-c33f-ece0-959241dfeb88.png)

ともかく、適切なトークンを積んでないとGraphQL通信に失敗する仕組みができている。

#### ログアウト

#### 関連ソース

```
$ tree
.
├── layouts
│   └── default.vue
```

#### ログアウト

「ログアウト」ボタンは、ログイン後のどのページからでも即座に実行できるようメニューの右端に仕込んである。

```vue:src/layouts/default.vue
<template>
  <v-app dark>
　　　　〜〜〜
    <v-navigation-drawer v-model="rightDrawer" :right="right" fixed>
      <v-list>
        <v-list-item @click="logout">
          <v-list-item-action>
            <v-icon>
              mdi-eject
            </v-icon>
          </v-list-item-action>
          <v-list-item-title>Log out</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
　　　　〜〜〜
  </v-app>
</template>

<script lang="ts">
import { Component, Provide, Vue } from 'nuxt-property-decorator'

@Component({})
export default class DefaultLayout extends Vue {
　　　〜〜〜
  @Provide()
  right: boolean = true

  @Provide()
  rightDrawer: boolean = false
　　　〜〜〜
  async logout() {
    try {
      await this.$auth.logout()
    } catch (err) {
      this.$toast.error(err)
    }
  }
}
</script>
```

例によって`$auth`オブジェクトが使えるので、`$auth.logout()`とするだけでAuth0的にログアウト処理を実行してくれる。
あ〜、便利。
ログアウトされて、ログインページが表示される。
![Screenshot at 2020-05-23 23-31-33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/9261f17a-5bb5-8967-a7bd-2bef11b6776d.png)
この状態で、あえて、ログイン後に遷移できるはずのトップページの表示を試みると、
![Screenshot at 2020-05-23 23-31-49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/33b7d3d2-6d8f-6aa6-49a5-5f7ae1972a3c.png)
表示できず、ログインページにリダイレクトされる。
![Screenshot at 2020-05-23 23-32-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/4b435ee6-d094-0d3d-ef1f-c4245f285393.png)

## バックエンド実装

さて、バックエンド側。

### WebAPIサーバ起動動線

```go:src/cmd/main.go
　〜〜〜
func main() {
	// 環境変数からJWTのチェックに使うAuth0のドメインやオーディエンスを取得
	e := loadEnv()
　　　　　〜〜〜
	var router *chi.Mux
	{
		router = chi.NewRouter()
		router.Group(func(r chi.Router) {
　　　　　　　　　　〜〜〜
			// 認証認可チェック用（今はJWTのチェックのみ実装）
			a := auth.New(e.Auth0Domain, e.Auth0Audience, e.AuthDebug, e.AuthCredentialsOptional)
			r.Use(a.CheckJWTHandlerFunc())
			r.Use(a.HoldPermissionsHandler)

			// GraphQLリゾルバー
			resolver := &graph.Resolver{
				DB:           db,
　　　　　　　　　　　　　　　　　　　　〜〜〜
			}

			// GraphQLエンドポイント
			r.Handle("/query", graph.DataLoaderMiddleware(resolver, graphQlServer(resolver)))
		})
	}

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", e.ServerPort)
	if err := http.ListenAndServe(":"+e.ServerPort, router); err != nil {
		fmt.Println(err)
	}
}
```

WebAPIサーバとしてはライブラリとして[chi](https://github.com/go-chi/chi)を使っている。
で、各リクエストのつどデコレートするためにカスタムミドルウェアを２つ定義している。
・`CheckJWTHandlerFunc`
・`HoldPermissionsHandler`
これにより、JWTのチェックと認可チェックをしている。

### JWTのチェック

```go:src/auth/auth.go
package auth

import(
　　　　　〜〜〜
	jwtMiddleware "github.com/auth0/go-jwt-middleware"
	"github.com/dgrijalva/jwt-go"
)

type Auth struct {
	domain              string
	audience            string
	debug               bool
	credentialsOptional bool
}

func New(domain, audience string, debug, credentialsOptional bool) *Auth {
	return &Auth{domain, audience, debug, credentialsOptional}
}

func (a *Auth) CheckJWTHandlerFunc() func(next http.Handler) http.Handler {
	middleware := jwtMiddleware.New(jwtMiddleware.Options{
		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
			// Verify 'aud' claim
			checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(a.audience, false)
			if !checkAud {
				return token, xerrors.New("Invalid audience.")
			}
			// Verify 'iss' claim
			checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(fmt.Sprintf("https://%s/", a.domain), false)
			if !checkIss {
				return token, xerrors.New("Invalid issuer.")
			}

			cert, err := a.getPemCert(token)
			if err != nil {
				return token, xerrors.New("Invalid token.")
			}

			result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
			return result, nil
		},
		SigningMethod:       jwt.SigningMethodRS256,
		Debug:               a.debug,
		CredentialsOptional: a.credentialsOptional,
	})
	return middleware.Handler
}

func (a *Auth) getPemCert(token *jwt.Token) (string, error) {
	cert := ""
	resp, err := http.Get(fmt.Sprintf("https://%s/.well-known/jwks.json", a.domain))

	if err != nil {
		return cert, xerrors.Errorf("failed to http.Get: %w", err)
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			fmt.Println(err)
		}
	}()

	var jwks = Jwks{}
	err = json.NewDecoder(resp.Body).Decode(&jwks)

	if err != nil {
		return cert, xerrors.Errorf("failed to decode jwks: %w", err)
	}

	for k, _ := range jwks.Keys {
		if token.Header["kid"] == jwks.Keys[k].Kid {
			cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
		}
	}

	if cert == "" {
		return cert, xerrors.New("cert is blank.")
	}

	return cert, nil
}

type Jwks struct {
	Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
	Kty string   `json:"kty"`
	Kid string   `json:"kid"`
	Use string   `json:"use"`
	N   string   `json:"n"`
	E   string   `json:"e"`
	X5c []string `json:"x5c"`
}
```

goにおけるJWTのチェック用ライブラリとして[go-jwt-middleware](https://github.com/auth0/go-jwt-middleware)を使う。
チェックの内容については以下を丸コピぐらいの勢いで流用。
https://auth0.com/docs/quickstart/backend/golang/01-authorization#validate-access-tokens

### 認可チェックのためのPermission取得

さて、JWTのチェックはしたものの、「このログインしたユーザは、この叩かれた機能を実行できていいんだっけ？」の部分は、まだチェックできていない。
それ用のミドルウェアが「`HoldPermissionsHandler`」。
下記は先述の「`CheckJWTHandlerFunc`」と同じファイルにあるので `Auth`構造体や`getPemCert`メソッドは共有している。

```go:src/auth/auth.go
package auth
　　　〜〜〜
func (a *Auth) HoldPermissionsHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		authHeaderParts := strings.Split(r.Header.Get("Authorization"), " ")
		tokenString := authHeaderParts[1]

		parser := func(token *jwt.Token) (interface{}, error) {
			cert, err := a.getPemCert(token)
			if err != nil {
				return nil, xerrors.Errorf("failed to getPemCert: %w", err)
			}
			result, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
			if err != nil {
				return nil, xerrors.Errorf("failed to ParseRSAPublicKeyFromPEM: %w", err)
			}
			return result, nil
		}

		token, err := jwt.Parse(tokenString, parser)
		if err != nil {
			log.Printf("failed to ParseWithClaims: %v", err)
			next.ServeHTTP(w, r)
			return
		}
		if !token.Valid {
			log.Print("invalid token")
			next.ServeHTTP(w, r)
			return
		}

		claims, ok := token.Claims.(jwt.MapClaims)
		if !ok {
			log.Print("not implemented MapClaims")
			next.ServeHTTP(w, r)
			return
		}

		authenticatedUser := &AuthenticatedUser{
			PermissionSet: util.NewBlankStringSet(),
		}
		for k, v := range claims {
			switch k {
			case "sub":
				authenticatedUser.ID = v.(string)
			case a.audience + "/email":
				authenticatedUser.EMail = v.(string)
			case "permissions":
				if permissionArray, ok := v.([]interface{}); ok {
					for _, permission := range permissionArray {
						authenticatedUser.PermissionSet.Add(permission.(string))
					}
					break
				}
			}
		}
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), authenticatedUserKey, authenticatedUser)))
	})
}
```

ざっくりと、このメソッドの要点だけ言うと、`claims, ok := token.Claims.(jwt.MapClaims)`によってJWTから取り出したクレームの中から、ログインユーザが持つPermissionを取得してリクエストスコープのコンテキストに格納している。

クレームの中身をデバッグしてみると、こんな感じ。
![Screenshot at 2020-05-24 00-17-16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/9b608ace-0d60-9349-5b0a-56e200e60c00.png)
Auth0のAPIのPermission設定で登録していった全てのPermissionが列挙されている。
これは、Adminロールを持つユーザでログインしたため。（例えばこれがNoneロールを持つユーザでログインした場合だと、持ってるPermissionは「`user:read:mine`」だけとなる）

### 認可チェック
実際の認可チェックはGraphQLの各リゾルバーの中で行う。

```go:src/graph/movie.resolver.go
　〜〜〜
func (r *queryResolver) Movies(ctx context.Context) ([]*model.Movie, error) {
	user := auth.GetAuthenticatedUser(ctx)
	if !user.HasReadMinePermission("content") {
		err := xerrors.New("no permissions")
		log.Print(err)
		return nil, err
	}
　　　　　〜〜〜
	return results, nil
}
```

`user := auth.GetAuthenticatedUser(ctx)`によって、リクエストスコープのコンテキストに格納したPermission群を保持する以下構造体を取得できる。

```go
// 認証チェック済みのユーザー情報を保持
type AuthenticatedUser struct {
	ID            string
	EMail         string
	PermissionSet *util.StringSet
}
```

そして、`user.HasReadMinePermission("content")`により、Permission群の中に”自分の” `content` に `READ` 権限があるかどうかがチェックできる。
このあたりのチェック用に以下のユーティリティメソッド群を用意している。

```go
func (u *AuthenticatedUser) HasPermission(funcName string, crud operation, t target) bool {
	if u == nil || u.PermissionSet == nil {
		return false
	}
	return u.PermissionSet.Contains(fmt.Sprintf("%s:%v:%v", funcName, crud, t))
}

func (u *AuthenticatedUser) HasNoTargetPermission(funcName string, crud operation) bool {
	if u == nil || u.PermissionSet == nil {
		return false
	}
	return u.PermissionSet.Contains(fmt.Sprintf("%s:%v", funcName, crud))
}

func (u *AuthenticatedUser) HasReadAllPermission(funcName string) bool {
	return u.HasPermission(funcName, READ, ALL)
}

func (u *AuthenticatedUser) HasReadMinePermission(funcName string) bool {
	return u.HasPermission(funcName, READ, MINE)
}

func (u *AuthenticatedUser) HasCreatePermission(funcName string) bool {
	return u.HasNoTargetPermission(funcName, CREATE)
}

type operation string
type target string

const (
	READ   operation = "read"
	CREATE           = "create"
	UPDATE           = "update"
	DELETE           = "delete"

	ALL  target = "all"
	MINE        = "mine"
)
```

これにより、リゾルバー毎に求めるPermissionを指定して認可チェックすることができる。

# まとめ
いつも、まとめようとする頃には力尽きている・・・。
次は、Management系APIを使った機能の実装事例かdataloadenの事例かな。