【PostGraphile版ApolloChat続編】自前のユーザー認証をやめて、OpenID Connectを使ってシングルサインオンするように実装してみた

# この記事で行うこと

この記事の続編です→[「vue-apolloに付属するデモチャットアプリ「ApolloChat」のGraphQLバックエンドをPostGraphileに置き換えてみた」](https://qiita.com/kanedaq/items/81e1126c699ca4d40b9b)
自前のユーザー認証をやめて、OpenID Connectを使ってシングルサインオンするように書き換えてみました。OpenIDプロバイダーとして、オンプレのGitLabを使用します。ユーザー認証をGitLabに任せることにより、PostgreSQLで利用者のパスワード等を管理しなくて良くなりました。
以後、自前認証のApolloChatを「自前版ApolloChat」、OpenIDを使って実装し直すApolloChatを「OpenID版ApolloChat」と呼びます。

## ソースコード

https://github.com/kanedaq/vue-apollo から入手できます。OpenID版ApolloChatはブランチ名「openid_version」です。
本記事は、自前版ApolloChatとOpenID版ApolloChatのソースコードの差分について記述します。PostgreSQLのdatabaseは作り直します。
また、本記事では「 ~/work/ 」の下で作業を行います。

### 自前版ApolloChatのソースコード

```
cd ~/work
git clone https://github.com/kanedaq/vue-apollo
cd vue-apollo/tests/demo/
```

### OpenID版ApolloChatのソースコード

上記コマンドに加えて、以下を実行します。
Webで見るならこちら：https://github.com/kanedaq/vue-apollo/tree/openid_version/tests/demo

```
git checkout openid_version
```

動かすには、以下を実行してWebサーバーを立ち上げ、ブラウザから「 http://localhost:8080/demo/ 」にアクセスします。
ただしOpenID版ApolloChatは、GitLabから取得したApplication IDやSecret等で設定ファイルやソースコードを書き換えてから動かす必要があります（後述）。

```
yarn install
yarn serve
```

## ユーザー認証の流れ

新旧のユーザー認証を図にしました。
シーケンス図はVisual Studio Code上で[PlantUML](http://plantuml.com/ja/)を使用して作成し、そのファイルは「 ~/work/vue-apollo/tests/demo/doc/ 」に保存しました。

### 【旧】 自前認証

![old.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/ea1f46f0-ff01-3fc0-835f-dcf5804546b4.png)


### 【新】 OpenID ConnectでSSO

![new.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/d8505177-0001-f6ba-bab0-8d21e63af865.png)


## ユーザー認証後の流れ【新旧共通】

![query.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/0e4e1805-4021-51f1-f7bd-05fab386d066.png)


## 実装の手抜き（デモにありがちな）

- httpsで通信すべきところもhttpで通信しています。
- 本記事ではJWTをローカルストレージに保存していますが、本番環境ではなさらないようお願いします。[HTML5のLocal Storageを使ってはいけない（翻訳）](https://techracho.bpsinc.jp/hachi8833/2019_10_09/80851) (2019-10-10追記)
- PostGraphile用JWTが"jwt expired"になった後の処理は実装していませんので、JWT関連で動きが怪しくなったらWebフロントエンドを再起動してください。再起動するとローカルストレージの古いJWTが削除され、OpenIDプロバイダーの認可エンドポイントにリダイレクトし直します。
- PostGraphile用JWTの期限を発行後60分にし、GitLabのトークンの期限との同期を考慮しておりません。
- 認可エンドポイントに渡すstateパラメーターは、毎回ランダム文字列を生成すべきところですが、今回は固定で'abcde'という文字列を使用しています。
- フロントエンドの実装が雑なので、画面がチラつく等、動きがどんくさいです。
- その他、[前回の記事](https://qiita.com/kanedaq/items/81e1126c699ca4d40b9b)と同様に手抜きがあります。
- 私（C++おじさん）はまだJavaScriptもVue.jsも経験が浅いので、変な箇所がありましたら優しくご指摘ください。

## 参考ページ（感謝します）

[多分わかりやすいOpenID Connect](https://tech-lab.sios.jp/archives/8651)
[OAuth 2 in Action](https://github.com/oauthinaction/oauth-in-action-code)
[[Python] PyJWT で Google OAuth 2.0 API の ID Token を検証](https://www.ykrods.net/posts/2019/04/30/pyjwt-id_token-validation/)
[PostgreSQLで「あればUPDATE、なければINSERT」のUPSERTをやってみる](https://blog.officekoma.co.jp/2018/06/postgresqlupdateinsertupsert.html)
[JavaScriptでURLクエリを取得する](https://qiita.com/akinov/items/26a7fc36d7c0045dd2db)
[Vue使いなら知っておきたいVueのパターン・小技集](https://qiita.com/HayatoKamono/items/5958d8648007adf6881b)
[Visual Studio Code で UML を描こう！](https://qiita.com/couzie/items/9dedb834c5aff09ea7b2)
[PlantUML 概要：シーケンス図](http://plantuml.com/ja/sequence-diagram)

# GitLab導入・設定

それでは作業を開始します。
OpenIDプロバイダーとして、オンプレのGitLabを使用します。今回は手っ取り早くDockerでローカルに導入します。
適当なディレクトリに移動後、以下を実行します。

```
git clone https://github.com/sameersbn/docker-gitlab
cd docker-gitlab
docker-compose up -d
```

Macを使っている場合、以下のようなエラーが出ることがあります。対処として、docker-compose.ymlを編集し、「/srv/docker」をOSユーザーがアクセスできるディレクトリに変更するのが手っ取り早いでしょう。

```
ERROR: for docker-gitlab_redis_1  Cannot start service redis: b'Mounts denied: \r\nThe path /srv/docker/gitlab/redis\r\nis not shared from OS X and is not known to Docker.\r\nYou can configure shared paths from Docker -> Preferences... -> File Sharing.\r\nSee https://docs.docker.com/docker-for-mac/osxfs/#namespaces for morRecreating docker-gitlab_postgresql_1 ... error
```

コンテナ立ち上げ後に「docker-compose ps」を実行すると、以下のように表示されました。

```
           Name                         Command               State                           Ports                        
---------------------------------------------------------------------------------------------------------------------------
docker-gitlab_gitlab_1       /sbin/entrypoint.sh app:start    Up      0.0.0.0:10022->22/tcp, 443/tcp, 0.0.0.0:10080->80/tcp
docker-gitlab_postgresql_1   /sbin/entrypoint.sh              Up      5432/tcp                                             
docker-gitlab_redis_1        /sbin/entrypoint.sh --logl ...   Up      6379/tcp                                             
```

GitLabコンテナのhttpポートが10080ですので、ブラウザで http://localhost:10080/ にアクセスします。下の画面が出たら、rootのパスワードを変更します。

<img width="668" alt="スクリーンショット 2019-09-03 15.46.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/fc1e516f-25cb-a53b-26f0-2d1312301722.png">

下の画面に行き、開発ユーザーを作成します。

<img width="668" alt="スクリーンショット 2019-09-03 15.56.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/5a85840d-897e-503b-73c1-13d508d35c56.png">

ログインできたら、下の画面のように右上のメニューからSettingsを選択します。

<img width="668" alt="スクリーンショット 2019-09-03 15.59.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/e96147ea-16fc-5867-ec2a-e35b0fc64c1c.png">

左のメニューからApplicationsを選択します。

<img width="668" alt="スクリーンショット 2019-09-03 16.01.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/db8576f8-edbf-deca-fd2e-c06637b05e84.png">

下の画面が出たら以下を入力し、Save applicationボタンを押します。

```
Name：test01 （任意の名称）
Redirect URI：http://localhost:8080/demo/login （後ほどフロント開発で実装します）
チェック：openid、email
```

<img width="668" alt="スクリーンショット 2019-09-03 16.04.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/573b457d-e5a8-2f69-f1a6-1aba92f8a0e8.png">

下の画面が出たら、Application IDとSecretを開発用にコピペして保存しておきます。本番環境でのSecretは取り扱い要注意で、少なくともフロントエンドのソースコードに記述すべきではありません。

<img width="668" alt="スクリーンショット 2019-09-03 16.10.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/d6add0e7-521a-cb3e-fa88-8b18f02425fb.png">

```
Application ID：cfd5dbe5d3093f2eb497030463737dc8438ead0e3779563ead3aaefdfc0838da
Secret：fdeb3e8f90000c378855ac1e557072dfb661038cf1e201a1fc50384ac441dcad
```

端末で以下を実行して、OpenIDプロバイダーに関する情報を取得しておきます。

```
curl http://localhost:10080/.well-known/openid-configuration | jq
```

以下のように表示されました。これも開発用にコピペして保存しておきます。ただし本記事ではhttpを使用しますので、アクセスするURLのhttpsはhttpに置き換えます。

```
{
  "issuer": "http://localhost:10080",
  "authorization_endpoint": "https://localhost:10080/oauth/authorize",
  "token_endpoint": "https://localhost:10080/oauth/token",
  "userinfo_endpoint": "https://localhost:10080/oauth/userinfo",
  "jwks_uri": "https://localhost:10080/oauth/discovery/keys",
  "scopes_supported": [
    "api",
    "read_user",
    "read_repository",
    "write_repository",
    "sudo",
    "openid",
    "profile",
    "email"
  ],
  "response_types_supported": [
    "code",
    "token"
  ],
  "response_modes_supported": [
    "query",
    "fragment"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "claim_types_supported": [
    "normal"
  ],
  "claims_supported": [
    "iss",
    "sub",
    "aud",
    "exp",
    "iat",
    "sub_legacy",
    "name",
    "nickname",
    "email",
    "email_verified",
    "website",
    "profile",
    "picture",
    "groups"
  ]
}
```

# バックエンド実装

それでは、自前版ApolloChatをOpenID版ApolloChatに移植する作業を開始します。まずはバックエンドから。

## 環境構築＆バックエンド実装(その1)：PostgreSQL関連まで

まず、以前書いた記事「[PostgreSQLを操作するAPIを提供する、PostgRESTとPostGraphileを両方試してみた](https://qiita.com/kanedaq/items/0c3097604d0e86afd1e3)」の「環境構築その1」と同様の作業が必要となります。本記事では割愛します。
それに加え、以下の手順も必要です。

「docker-compose up」でコンテナを立ち上げる前に、docker-compose.ymlに以下のように環境変数を加えます。

```~/work/laradock/docker-compose.yml
### PostgreSQL ###########################################
      environment:
+         - GITLAB_ISSUER=http://localhost:10080
+         - GITLAB_TOKEN_ENDPOINT=http://172.20.0.1:10080/oauth/token
+         - GITLAB_USERINFO_ENDPOINT=http://172.20.0.1:10080/oauth/userinfo
+         - GITLAB_JWKS_URI=http://172.20.0.1:10080/oauth/discovery/keys
+         - GITLAB_REDIRECT_URI=http://localhost:8080/demo/login
+         - GITLAB_CLIENT_ID=cfd5dbe5d3093f2eb497030463737dc8438ead0e3779563ead3aaefdfc0838da
+         - GITLAB_CLIENT_SECRET=fdeb3e8f90000c378855ac1e557072dfb661038cf1e201a1fc50384ac441dcad
```

環境変数の中身は以下の通りです。

 - GITLAB_ISSUER ： 先程curlで取得した"issuer"をそのまま設定
 - GITLAB_TOKEN_ENDPOINT ： 先程curlで取得した"token_endpoint"を、postgresコンテナ内からアクセスできるURLに変換して設定
 - GITLAB_USERINFO_ENDPOINT ： 先程curlで取得した"userinfo_endpoint"を、postgresコンテナ内からアクセスできるURLに変換して設定
 - GITLAB_JWKS_URI ： 先程curlで取得した"jwks_uri"を、postgresコンテナ内からアクセスできるURLに変換して設定
 - GITLAB_REDIRECT_URI ： GitLabのGUIで設定した"Redirect URI"をそのまま設定
 - GITLAB_CLIENT_ID ： GitLabのGUIから取得した"Application ID"を設定
 - GITLAB_CLIENT_SECRET ： GitLabのGUIから取得した"Secret"を設定

設定後、コンテナを立ち上げます。

```
cd ~/work/laradock
docker-compose up -d postgres pgadmin workspace
docker-compose ps
```

以下のように表示されました。

```
           Name                          Command              State           Ports         
--------------------------------------------------------------------------------------------
laradock_docker-in-docker_1   dockerd-entrypoint.sh           Up      2375/tcp              
laradock_pgadmin_1            docker-entrypoint.sh pgadmin4   Up      0.0.0.0:5050->5050/tcp
laradock_postgres_1           docker-entrypoint.sh postgres   Up      0.0.0.0:5432->5432/tcp
laradock_workspace_1          /sbin/my_init                   Up      0.0.0.0:2222->22/tcp  
```

本記事では、PostgreSQLのユーザー定義関数でPythonを使用しますので、Laradockのpostgresコンテナに入って、Python等をインストールします。

```
docker exec -it laradock_postgres_1 bash
```

OSの確認

```
cat /etc/issue
```

以下のように表示され、postgresコンテナのOSはAlpine Linuxであることがわかりました。

```
Welcome to Alpine Linux 3.10
Kernel \r on an \m (\l)
```

以下を実行して、python関連のパッケージをインストールします。

```
apk add --no-cache --repository http://nl.alpinelinux.org/alpine/edge/testing python3 postgresql-plpython3
```

以下のように表示されました。

```
fetch http://nl.alpinelinux.org/alpine/edge/testing/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/x86_64/APKINDEX.tar.gz
(1/8) Installing libbz2 (1.0.6-r7)
(2/8) Installing expat (2.2.7-r0)
(3/8) Installing libffi (3.2.1-r6)
(4/8) Installing gdbm (1.13-r1)
(5/8) Installing xz-libs (5.2.4-r0)
(6/8) Installing sqlite-libs (3.28.0-r0)
(7/8) Installing python3 (3.7.3-r0)
(8/8) Installing postgresql-plpython3 (11.5-r0)
Executing busybox-1.30.1-r2.trigger
OK: 109 MiB in 39 packages
```

以下を実行して、postgresql-plpython3関連がインストールされた場所を調べます。

```
find / -name '*plpy*'
```

以下のように表示されました。

```
/usr/lib/postgresql/pgxs/src/pl/plpython
/usr/lib/postgresql/plpython3.so
/usr/share/postgresql/extension/plpython3u--unpackaged--1.0.sql
/usr/share/postgresql/extension/plpython3u.control
/usr/share/postgresql/extension/plpython3u--1.0.sql
```

私が動かした時は、上記ファイルは「/usr/local」になければ動きませんでしたので、以下のようにコピーしてしまいます。本番環境ではシンボリックリンクの方が良いでしょう。
参考URL：https://github.com/timescale/timescaledb/issues/473

```
cp -r /usr/lib/postgresql/* /usr/local/lib/postgresql/
cp /usr/share/postgresql/extension/* /usr/local/share/postgresql/extension/
```

IDトークンを検証するのに必要なパッケージをインストールします。

```
apk add gcc python3-dev musl-dev libffi-dev openssl-dev
```

以下のように表示されました。

```
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/x86_64/APKINDEX.tar.gz
(1/15) Upgrading musl (1.1.22-r2 -> 1.1.22-r3)
(2/15) Installing binutils (2.32-r0)
(3/15) Installing gmp (6.1.2-r1)
(4/15) Installing isl (0.18-r0)
(5/15) Installing libgomp (8.3.0-r0)
(6/15) Installing libatomic (8.3.0-r0)
(7/15) Installing mpfr3 (3.1.5-r1)
(8/15) Installing mpc1 (1.1.0-r0)
(9/15) Installing gcc (8.3.0-r0)
(10/15) Installing linux-headers (4.19.36-r0)
(11/15) Installing pkgconf (1.6.1-r1)
(12/15) Installing libffi-dev (3.2.1-r6)
(13/15) Installing musl-dev (1.1.22-r3)
(14/15) Installing openssl-dev (1.1.1c-r0)
(15/15) Installing python3-dev (3.7.3-r0)
Executing busybox-1.30.1-r2.trigger
OK: 271 MiB in 53 packages
```

引き続き、以下を実行します。

```
pip3 install requests pyjwt cryptography oauthlib pycrypto pyxero
```

以下のように表示されました。

```
Collecting requests
  Downloading https://files.pythonhosted.org/packages/51/bd/23c926cd341ea6b7dd0b2a00aba99ae0f828be89d72b2190f27c11d4b7fb/requests-2.22.0-py2.py3-none-any.whl (57kB)
    100% |████████████████████████████████| 61kB 2.0MB/s 
Collecting pyjwt
  Downloading https://files.pythonhosted.org/packages/87/8b/6a9f14b5f781697e51259d81657e6048fd31a113229cf346880bb7545565/PyJWT-1.7.1-py2.py3-none-any.whl
Collecting cryptography
  Downloading https://files.pythonhosted.org/packages/c2/95/f43d02315f4ec074219c6e3124a87eba1d2d12196c2767fadfdc07a83884/cryptography-2.7.tar.gz (495kB)
    100% |████████████████████████████████| 501kB 681kB/s 
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Collecting oauthlib
  Downloading https://files.pythonhosted.org/packages/05/57/ce2e7a8fa7c0afb54a0581b14a65b56e62b5759dbc98e80627142b8a3704/oauthlib-3.1.0-py2.py3-none-any.whl (147kB)
    100% |████████████████████████████████| 153kB 1.5MB/s 
Collecting pycrypto
  Downloading https://files.pythonhosted.org/packages/60/db/645aa9af249f059cc3a368b118de33889219e0362141e75d4eaf6f80f163/pycrypto-2.6.1.tar.gz (446kB)
    100% |████████████████████████████████| 450kB 1.1MB/s 
Collecting pyxero
  Downloading https://files.pythonhosted.org/packages/20/3a/fb5fd6e92cc04855d500ca5aa5a150e069b951a51c138a236e3997d50214/pyxero-0.9.1-py2.py3-none-any.whl
Collecting idna<2.9,>=2.5 (from requests)
  Downloading https://files.pythonhosted.org/packages/14/2c/cd551d81dbe15200be1cf41cd03869a46fe7226e7450af7a6545bfc474c9/idna-2.8-py2.py3-none-any.whl (58kB)
    100% |████████████████████████████████| 61kB 337kB/s 
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 (from requests)
  Downloading https://files.pythonhosted.org/packages/e6/60/247f23a7121ae632d62811ba7f273d0e58972d75e58a94d329d51550a47d/urllib3-1.25.3-py2.py3-none-any.whl (150kB)
    100% |████████████████████████████████| 153kB 710kB/s 
Collecting certifi>=2017.4.17 (from requests)
  Downloading https://files.pythonhosted.org/packages/69/1b/b853c7a9d4f6a6d00749e94eb6f3a041e342a885b87340b79c1ef73e3a78/certifi-2019.6.16-py2.py3-none-any.whl (157kB)
    100% |████████████████████████████████| 163kB 586kB/s 
Collecting chardet<3.1.0,>=3.0.2 (from requests)
  Downloading https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl (133kB)
    100% |████████████████████████████████| 143kB 625kB/s 
Collecting asn1crypto>=0.21.0 (from cryptography)
  Downloading https://files.pythonhosted.org/packages/ea/cd/35485615f45f30a510576f1a56d1e0a7ad7bd8ab5ed7cdc600ef7cd06222/asn1crypto-0.24.0-py2.py3-none-any.whl (101kB)
    100% |████████████████████████████████| 102kB 1.1MB/s 
Collecting six>=1.4.1 (from cryptography)
  Downloading https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl
Collecting cffi!=1.11.3,>=1.8 (from cryptography)
  Using cached https://files.pythonhosted.org/packages/93/1a/ab8c62b5838722f29f3daffcc8d4bd61844aa9b5f437341cc890ceee483b/cffi-1.12.3.tar.gz
Collecting requests-oauthlib>=0.3.0 (from pyxero)
  Downloading https://files.pythonhosted.org/packages/c2/e2/9fd03d55ffb70fe51f587f20bcf407a6927eb121de86928b34d162f0b1ac/requests_oauthlib-1.2.0-py2.py3-none-any.whl
Collecting python-dateutil>=2.1 (from pyxero)
  Downloading https://files.pythonhosted.org/packages/41/17/c62faccbfbd163c7f57f3844689e3a78bae1f403648a6afb1d0866d87fbb/python_dateutil-2.8.0-py2.py3-none-any.whl (226kB)
    100% |████████████████████████████████| 235kB 3.6MB/s 
Collecting pycparser (from cffi!=1.11.3,>=1.8->cryptography)
  Using cached https://files.pythonhosted.org/packages/68/9e/49196946aee219aead1290e00d1e7fdeab8567783e83e1b9ab5585e6206a/pycparser-2.19.tar.gz
Building wheels for collected packages: cryptography
  Building wheel for cryptography (PEP 517) ... done
  Stored in directory: /root/.cache/pip/wheels/d0/02/96/64b1439e5409591b6b0294d1da2f66a4ae4f0548d1bdb225b7
Successfully built cryptography
Installing collected packages: idna, urllib3, certifi, chardet, requests, pyjwt, asn1crypto, six, pycparser, cffi, cryptography, oauthlib, pycrypto, requests-oauthlib, python-dateutil, pyxero
  Running setup.py install for pycparser ... done
  Running setup.py install for cffi ... done
  Running setup.py install for pycrypto ... done
Successfully installed asn1crypto-0.24.0 certifi-2019.6.16 cffi-1.12.3 chardet-3.0.4 cryptography-2.7 idna-2.8 oauthlib-3.1.0 pycparser-2.19 pycrypto-2.6.1 pyjwt-1.7.1 python-dateutil-2.8.0 pyxero-0.9.1 requests-2.22.0 requests-oauthlib-1.2.0 six-1.12.0 urllib3-1.25.3
You are using pip version 19.0.3, however version 19.2.3 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
```

以上でLaradockのpostgresコンテナ内での作業は完了です。コンテナから抜けておきます。

```
exit
```

## 環境構築＆バックエンド実装(その2)：DB作成

[前回の記事](https://qiita.com/kanedaq/items/81e1126c699ca4d40b9b)にER図を載せましたが、本記事ではユーザーのパスワードは管理しないため、user_accountsテーブルは不要となります。ユーザー登録用のuser_register関数等も不要となります。また、user_login関数の実装が大きく変わります。

### 作業手順

psqlに入リます。defaultユーザのパスワード: secret

```
psql -U default -h localhost -p 5432 -d default
```

OpenID版ApolloChat用DBを作成し、接続できることを確認します。
DB名をdemodbとしましたが、名前は何でも良いです。

```
create database demodb;
\c demodb
```

「 \c demodb 」し忘れると、別のデータベース内にスキーマを作成することになりますのでご注意ください。

引き続き以下のSQLを実行して、スキーマとデータを作成します（内容は後ほどご説明します）。
このSQLは「 ~/work/vue-apollo/tests/demo/postgraphile-server/schema_and_data.sql 」に保存しておきます。
なお、JWTの期限を発行後60分にしてあります。変更したい場合は、「60 minutes」の部分を書き換えてください。
参考URL：https://github.com/graphile/postgraphile/blob/master/examples/forum/schema.sql

```~/work/vue-apollo/tests/demo/postgraphile-server/schema_and_data.sql
begin;

create extension if not exists plpython3u;

create role apollo_demo_postgraphile login password 'xyz';

create role apollo_demo_anonymous;
grant apollo_demo_anonymous to apollo_demo_postgraphile;

create role apollo_demo_login_user;
grant apollo_demo_login_user to apollo_demo_postgraphile;

create schema apollo_demo;
create schema apollo_demo_private;

create table apollo_demo.users (
  id               integer primary key,
  nickname         text not null,
  email            text not null unique check (email ~* '^.+@.+\..+$')
);

comment on table apollo_demo.users is 'A user of the chat.';
comment on column apollo_demo.users.id is 'The primary unique identifier for the user.';
comment on column apollo_demo.users.nickname is 'The user’s nickname.';
comment on column apollo_demo.users.email is 'The email address of the user.';

create table apollo_demo.channels (
  id               text primary key,
  name             text not null
);

comment on table apollo_demo.channels is 'A channel of the chat.';
comment on column apollo_demo.channels.id is 'The primary key for the channel.';
comment on column apollo_demo.channels.name is 'The channel’s name.';

create table apollo_demo.messages (
  id               integer primary key generated by default as identity,
  channel_id       text not null references apollo_demo.channels(id),
  user_id          integer not null references apollo_demo.users(id),
  content          text not null,
  date_added       timestamp not null default current_timestamp,
  date_updated     timestamp
) ;

comment on table apollo_demo.messages is 'A message written by a user.';
comment on column apollo_demo.messages.id is 'The primary key for the message.';
comment on column apollo_demo.messages.channel_id is 'The id of the channel.';
comment on column apollo_demo.messages.user_id is 'The id of the user.';
comment on column apollo_demo.messages.content is 'The content this has been posted in.';
comment on column apollo_demo.messages.date_added is 'The time this message was added.';
comment on column apollo_demo.messages.date_updated is 'The time this message was updated';

alter default privileges revoke execute on functions from public;

create function apollo_demo_private.set_date_updated() returns trigger as $$
begin
  new.date_updated := current_timestamp;
  return new;
end;
$$ language plpgsql;

create trigger messages_date_updated before update
  on apollo_demo.messages
  for each row
  execute procedure apollo_demo_private.set_date_updated();

create type apollo_demo.jwt_token as (
  role text,
  user_id integer,
  exp integer
);

create type apollo_demo.usr_and_token as (
  usr apollo_demo.users,
  token apollo_demo.jwt_token
);

create or replace function apollo_demo.user_login(
  authorization_code text
) returns apollo_demo.usr_and_token as $$
  import os
  import requests
  import json
  import jwt

  GITLAB_ISSUER = os.environ["GITLAB_ISSUER"]
  GITLAB_TOKEN_ENDPOINT = os.environ["GITLAB_TOKEN_ENDPOINT"]
  GITLAB_USERINFO_ENDPOINT = os.environ["GITLAB_USERINFO_ENDPOINT"]
  GITLAB_JWKS_URI = os.environ["GITLAB_JWKS_URI"]
  GITLAB_REDIRECT_URI = os.environ["GITLAB_REDIRECT_URI"]
  GITLAB_CLIENT_ID = os.environ["GITLAB_CLIENT_ID"]
  GITLAB_CLIENT_SECRET = os.environ["GITLAB_CLIENT_SECRET"]

  def validate_id_token(id_token):
    # id_tokenからヘッダ取り出し
    header = jwt.get_unverified_header(id_token)

    # 公開鍵取得
    response = requests.get(GITLAB_JWKS_URI)
    jwk_set = response.json()
    jwk = next(filter(lambda k: k['kid'] == header['kid'], jwk_set['keys']))

    public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk))

    # id_tokenからクレーム取り出し
    claims = jwt.decode(id_token,
                        public_key,
                        issuer=GITLAB_ISSUER,
                        audience=GITLAB_CLIENT_ID,
                        algorithms=["RS256"])
    return claims

  # token取得
  data = {
    "client_id": GITLAB_CLIENT_ID,
    "client_secret": GITLAB_CLIENT_SECRET,
    "code": authorization_code,
    "grant_type": "authorization_code",
    "redirect_uri": GITLAB_REDIRECT_URI
  }
  response = requests.post(GITLAB_TOKEN_ENDPOINT, data=data)

  id_token = response.json()["id_token"]
  access_token = response.json()["access_token"]

  # id_tokenの検証
  claims = validate_id_token(id_token)
  if claims["aud"] != GITLAB_CLIENT_ID:
    raise jwt.InvalidSignatureError('Signature verification failed')

  # UserInfoエンドポイントからユーザー名とメールアドレスを取得
  data = {
    "schema": "openid",
  }
  headers = {
    "Authorization": "Bearer " + access_token,
  }
  response = requests.post(GITLAB_USERINFO_ENDPOINT, data=data, headers=headers)

  id = response.json()["sub"]
  # name = response.json()["name"]
  nickname = response.json()["nickname"]
  email = response.json()["email"]

  # usersテーブルにUPSERT
  plpy.execute(f'''\
INSERT INTO apollo_demo.users (id, nickname, email) VALUES
  ({id}, '{nickname}', '{email}')
  ON CONFLICT (id)
  DO UPDATE SET nickname = '{nickname}', email = '{email}'
''')

  # 戻り値作成
  result = plpy.execute(f'''\
SELECT (({id}, '{nickname}', '{email}')::apollo_demo.users,
        ('apollo_demo_login_user', {id}, extract(epoch from (now() + interval '60 minutes')))::apollo_demo.jwt_token
       )::apollo_demo.usr_and_token as usr_and_token
''')
  return result[0]["usr_and_token"];
$$ language plpython3u strict security definer;

comment on function apollo_demo.user_login(text) is 'Creates a JWT token that will securely identify a user and give them certain permissions.';

create function apollo_demo.user_current() returns apollo_demo.users as $$
  select *
  from apollo_demo.users
  where id = current_setting('jwt.claims.user_id', true)::integer
$$ language sql stable;

comment on function apollo_demo.user_current() is 'Gets the user who was identified by our JWT.';

create function apollo_demo.user_logout(
) returns boolean as $$
begin
  return true;
end;
$$ language plpgsql stable;

create function apollo_demo.message_add(
  channel_id text,
  content text
) returns apollo_demo.messages as $$
declare
  message apollo_demo.messages;
begin
  INSERT INTO apollo_demo.messages (channel_id, user_id, content) VALUES
    (message_add.channel_id, current_setting('jwt.claims.user_id', true)::integer, message_add.content)
    RETURNING messages.* into message;
  return message;
end;
$$ language plpgsql strict security definer;

CREATE SEQUENCE mock_message_seq
    INCREMENT BY 1
    START WITH 1
;

create function apollo_demo.mock_message_send(
) returns boolean as $$
begin
  INSERT INTO apollo_demo.messages (channel_id, user_id, content)
    SELECT 'general', 0, 'How are you doing? ' || nextval('mock_message_seq');
  return true;
end;
$$ language plpgsql strict security definer;

grant usage on schema apollo_demo to apollo_demo_anonymous, apollo_demo_login_user;

grant select on table apollo_demo.users to apollo_demo_login_user;
grant update, delete on table apollo_demo.users to apollo_demo_login_user;

grant select on table apollo_demo.channels to apollo_demo_login_user;
grant insert, update, delete on table apollo_demo.channels to apollo_demo_login_user;

grant select on table apollo_demo.messages to apollo_demo_login_user;
grant insert, update, delete on table apollo_demo.messages to apollo_demo_login_user;

grant execute on function apollo_demo.user_login(text) to apollo_demo_anonymous;
grant execute on function apollo_demo.user_current() to apollo_demo_login_user;
grant execute on function apollo_demo.user_logout() to apollo_demo_anonymous, apollo_demo_login_user;
grant execute on function apollo_demo.message_add(text, text) to apollo_demo_login_user;
grant execute on function apollo_demo.mock_message_send() to apollo_demo_login_user;

alter table apollo_demo.users enable row level security;
alter table apollo_demo.messages enable row level security;

create policy select_users on apollo_demo.users for select
  using (true);

create policy select_messages on apollo_demo.messages for select
  using (true);

create policy update_users on apollo_demo.users for update to apollo_demo_login_user
  using (id = current_setting('jwt.claims.user_id', true)::integer);

create policy delete_users on apollo_demo.users for delete to apollo_demo_login_user
  using (id = current_setting('jwt.claims.user_id', true)::integer);

create policy insert_messages on apollo_demo.messages for insert to apollo_demo_login_user
  with check (user_id = current_setting('jwt.claims.user_id', true)::integer);

create policy update_messages on apollo_demo.messages for update to apollo_demo_login_user
  using (user_id = current_setting('jwt.claims.user_id', true)::integer);

create policy delete_messages on apollo_demo.messages for delete to apollo_demo_login_user
  using (user_id = current_setting('jwt.claims.user_id', true)::integer);


INSERT INTO apollo_demo.users (id, nickname, email) VALUES
  (0, 'The Bot', 'bot@bot.com');

INSERT INTO apollo_demo.channels (id, name) VALUES
  ('general', 'General discussion'),
  ('random', 'Have fun chatting!'),
  ('help', 'Ask for or give help');

INSERT INTO apollo_demo.messages (channel_id, user_id, content) VALUES
  ('general', 0, 'Welcome to the chat!');


create type apollo_demo.message_nullable as (
  id               integer,
  channel_id       text,
  user_id          integer,
  content          text,
  date_added       timestamp,
  date_updated     timestamp
) ;

create or replace function apollo_demo.graphql_subscription() returns trigger as $$
declare
  v_process_new bool = (TG_OP = 'INSERT' OR TG_OP = 'UPDATE');
  v_process_old bool = (TG_OP = 'UPDATE' OR TG_OP = 'DELETE');
  v_event text = TG_ARGV[0];
  v_topic_template text = TG_ARGV[1];
  v_attribute text = TG_ARGV[2];
  v_record record;
  v_sub text;
  v_topic text;
begin
  if v_process_new then
    v_record = new;
  else
    v_record = old;
  end if;

  if v_attribute is not null then
    execute 'select $1.' || quote_ident(v_attribute)
    using v_record
    into v_sub;
  end if;

  if v_sub is not null then
    v_topic = replace(v_topic_template, '$1', v_sub);
  else
    v_topic = v_topic_template;
  end if;

  perform pg_notify(v_topic, json_build_object(
      'event', v_event,
      'newrec', new.*,
      'oldrec', old.*,
      'type', TG_OP
    )::text);

  -- RAISE LOG     'v_topic = %', v_topic;
  -- RAISE LOG     'json_build_object = %', json_build_object('event', v_event, 'newrec', new.*, 'oldrec', old.*, 'type', TG_OP);

  return v_record;
end;
$$ language plpgsql volatile set search_path from current;

DROP TRIGGER IF EXISTS _500_gql_update_message ON apollo_demo.messages;
CREATE TRIGGER _500_gql_update_message
  AFTER INSERT OR UPDATE OR DELETE ON apollo_demo.messages
  FOR EACH ROW
  EXECUTE PROCEDURE apollo_demo.graphql_subscription(
    'messageChanged', -- the "event" string, useful for the client to know what happened
    'graphql:message:$1', -- the "topic" the event will be published to, as a template
    'channel_id' -- If specified, `$1` above will be replaced with NEW.channel_id or OLD.channel_id from the trigger.
  );

commit;
```

処理が成功したら、psqlから抜けておきます。

```
\q
```

### user_login関数について

今流したSQL文に含まれているユーザー定義関数「user_login」は、自前版ApolloChatと大きく実装が変わりました。
以下に、シーケンス図及びソースコードを再喝します。user_login関数は、シーケンス図のPostGraphileの部分を実装しています。

#### 自前版ApolloChatのuser_login関数

![old.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/ea1f46f0-ff01-3fc0-835f-dcf5804546b4.png)

```
create function apollo_demo.user_login(
  email text,
  password text
) returns apollo_demo.usr_and_token as $$
  select ((users.*)::apollo_demo.users, ('apollo_demo_login_user', users.id, extract(epoch from (now() + interval '60 minutes')))::apollo_demo.jwt_token)::apollo_demo.usr_and_token
    from apollo_demo.users
      inner join apollo_demo_private.user_accounts
      on users.id = user_accounts.user_id
    where 
      users.email = user_login.email
      and user_accounts.password_hash = crypt(user_login.password, user_accounts.password_hash);
$$ language sql strict security definer;
```

#### OpenID版ApolloChatのuser_login関数（Python使用）

![new.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/d8505177-0001-f6ba-bab0-8d21e63af865.png)

user_login関数を実装する上での参考URL：[[Python] PyJWT で Google OAuth 2.0 API の ID Token を検証](https://www.ykrods.net/posts/2019/04/30/pyjwt-id_token-validation/)

```
create or replace function apollo_demo.user_login(
  authorization_code text
) returns apollo_demo.usr_and_token as $$
  import os
  import requests
  import json
  import jwt

  GITLAB_ISSUER = os.environ["GITLAB_ISSUER"]
  GITLAB_TOKEN_ENDPOINT = os.environ["GITLAB_TOKEN_ENDPOINT"]
  GITLAB_USERINFO_ENDPOINT = os.environ["GITLAB_USERINFO_ENDPOINT"]
  GITLAB_JWKS_URI = os.environ["GITLAB_JWKS_URI"]
  GITLAB_REDIRECT_URI = os.environ["GITLAB_REDIRECT_URI"]
  GITLAB_CLIENT_ID = os.environ["GITLAB_CLIENT_ID"]
  GITLAB_CLIENT_SECRET = os.environ["GITLAB_CLIENT_SECRET"]

  def validate_id_token(id_token):
    # id_tokenからヘッダ取り出し
    header = jwt.get_unverified_header(id_token)

    # 公開鍵取得
    response = requests.get(GITLAB_JWKS_URI)
    jwk_set = response.json()
    jwk = next(filter(lambda k: k['kid'] == header['kid'], jwk_set['keys']))

    public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk))

    # id_tokenからクレーム取り出し
    claims = jwt.decode(id_token,
                        public_key,
                        issuer=GITLAB_ISSUER,
                        audience=GITLAB_CLIENT_ID,
                        algorithms=["RS256"])
    return claims

  # token取得
  data = {
    "client_id": GITLAB_CLIENT_ID,
    "client_secret": GITLAB_CLIENT_SECRET,
    "code": authorization_code,
    "grant_type": "authorization_code",
    "redirect_uri": GITLAB_REDIRECT_URI
  }
  response = requests.post(GITLAB_TOKEN_ENDPOINT, data=data)

  id_token = response.json()["id_token"]
  access_token = response.json()["access_token"]

  # id_tokenの検証
  claims = validate_id_token(id_token)
  if claims["aud"] != GITLAB_CLIENT_ID:
    raise jwt.InvalidSignatureError('Signature verification failed')

  # UserInfoエンドポイントからユーザー名とメールアドレスを取得
  data = {
    "schema": "openid",
  }
  headers = {
    "Authorization": "Bearer " + access_token,
  }
  response = requests.post(GITLAB_USERINFO_ENDPOINT, data=data, headers=headers)

  id = response.json()["sub"]
  # name = response.json()["name"]
  nickname = response.json()["nickname"]
  email = response.json()["email"]

  # usersテーブルにUPSERT
  plpy.execute(f'''\
INSERT INTO apollo_demo.users (id, nickname, email) VALUES
  ({id}, '{nickname}', '{email}')
  ON CONFLICT (id)
  DO UPDATE SET nickname = '{nickname}', email = '{email}'
''')

  # 戻り値作成
  result = plpy.execute(f'''\
SELECT (({id}, '{nickname}', '{email}')::apollo_demo.users,
        ('apollo_demo_login_user', {id}, extract(epoch from (now() + interval '60 minutes')))::apollo_demo.jwt_token
       )::apollo_demo.usr_and_token as usr_and_token
''')
  return result[0]["usr_and_token"];
$$ language plpython3u strict security definer;
```

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">できそう(^^)PostgreSQLのユーザー定義関数でPythonが使えて助かった。PL/pgSQLだったらどう実装して良いか（そもそも実装できるのか）わからなかった。</p>&mdash; 普通のプログラマ＠舌下免疫療法中 (@kanedaq) <a href="https://twitter.com/kanedaq/status/1167380504754081792?ref_src=twsrc%5Etfw">August 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

## 環境構築＆バックエンド実装(その3)：PostGraphile関連

[前回の記事](https://qiita.com/kanedaq/items/81e1126c699ca4d40b9b)の「環境構築＆バックエンド実装(その3)」と全く同じです。本記事では割愛します。
環境構築後に、PostGraphileも立ち上げます。

```
docker-compose up -d postgres pgadmin workspace postgraphile_demo
```

```
$ docker-compose ps
            Name                          Command              State                Ports            
-----------------------------------------------------------------------------------------------------
laradock_docker-in-docker_1    dockerd-entrypoint.sh           Up       2375/tcp                     
laradock_pgadmin_1             docker-entrypoint.sh pgadmin4   Up       0.0.0.0:5050->5050/tcp       
laradock_postgraphile_demo_1   ./cli.js --plugins @graphi      Up       0.0.0.0:16000->16000/tcp,    
                               ...                                      5000/tcp                     
laradock_postgres_1            docker-entrypoint.sh postgres   Up       0.0.0.0:5432->5432/tcp       
laradock_workspace_1           sudo bash -c [ -e /var/run      Up       0.0.0.0:2222->22/tcp,        
                               ...                                      0.0.0.0:3389->3389/tcp,      
                                                                        0.0.0.0:14000->4000/tcp,     
                                                                        0.0.0.0:18080->8080/tcp      
```

```
$ docker logs laradock_postgraphile_demo_1
PostGraphile v4.4.1 server listening on port 16000 🚀

  ‣ GraphQL API:         http://0.0.0.0:16000/graphql (subscriptions enabled)
  ‣ GraphiQL GUI/IDE:    http://0.0.0.0:16000/graphiql
  ‣ Postgres connection: postgres://apollo_demo_postgraphile:[SECRET]@postgres/demodb
  ‣ Postgres schema(s):  apollo_demo
  ‣ Documentation:       https://graphile.org/postgraphile/introduction/

* * *
```

# フロントエンド変更

バックエンドの作業が完了しましたので、次はフロントエンドのVue.jsアプリに手を加えていきます。
以下、自前版ApolloChatとOpenID版ApolloChatのソースコード差分を示します。必要に応じて、シーケンス図も再喝します。

### ~/work/vue-apollo/tests/demo/src/common.js (新規)

汎用性の高いルーチンを格納するソースファイルを新規作成しました。
実装する上での参考URL：[JavaScriptでURLクエリを取得する](https://qiita.com/akinov/items/26a7fc36d7c0045dd2db)

```~/work/vue-apollo/tests/demo/src/common.js
// https://qiita.com/akinov/items/26a7fc36d7c0045dd2db
export function getUrlQueries () {
  var queryStr = window.location.search.slice(1) // 文頭?を除外
  var queries = {}

  // クエリがない場合は空のオブジェクトを返す
  if (!queryStr) {
    return queries
  }

  // クエリ文字列を & で分割して処理
  queryStr.split('&').forEach(function (queryStr) {
    // = で分割してkey,valueをオブジェクトに格納
    var queryArr = queryStr.split('=')
    queries[queryArr[0]] = queryArr[1]
  })

  return queries
}
```

### ~/work/vue-apollo/tests/demo/src/vue-apollo.js (変更)

主な変更点としては、

 - Configに以下の値を追加
     - gitlab_authorization_endpoint ： curlで取得した"authorization_endpoint"を、https→httpにして設定
     - gitlab_response_type ： 認可コードを意味する'code'を設定
     - gitlab_client_id ： GitLabのGUIから取得した"Application ID"を設定
     - gitlab_scope ： GitLabのGUIで設定したopenidとemailを空白区切りで設定
     - gitlab_redirect_uri ： GitLabのGUIで設定した"Redirect URI"をそのまま設定
 - readGitlabState関数の追加：ローカルストレージに設定されているstateを返す関数
 - redirectGitlab関数の追加：Webフロントエンド起動直後に呼ばれることを想定した関数。古いJWTを削除後、OpenIDプロバイダーの認可エンドポイントにリダイレクトします（下図参照）。stateパラメーターは今回は固定値を使っていますが、本来は毎回ランダム文字列を生成すべきです。

![new.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/d8505177-0001-f6ba-bab0-8d21e63af865.png)

```~/work/vue-apollo/tests/demo/src/vue-apollo.js
import Vue from 'vue'
import VueApollo from '../../../'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
+ import { getUrlQueries } from './common'

// Install the vue plugin
Vue.use(VueApollo)

// Name of the localStorage item
const AUTH_TOKEN = 'postgraphile-demo-token'
+ const AUTH_GITLAB_STATE = 'postgraphile-demo-gitlab-state'

// Config
const defaultOptions = {
  // You can use `https` for secure connection (recommended in production)
  httpEndpoint: process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql',
  // You can use `wss` for secure connection (recommended in production)
  // Use `null` to disable subscriptions
  wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
  // LocalStorage token
  tokenName: AUTH_TOKEN,
  // Enable Automatic Query persisting with Apollo Engine
  persisting: false,
  // Use websockets for everything (no HTTP)
  // You need to pass a `wsEndpoint` for this to work
  websocketsOnly: false,
  // Is being rendered on the server?
  ssr: false,
  // Override default http link
  // link: myLink,
  // Override default cache
  // cache: myCache,
  cache: new InMemoryCache(),
  // Additional ApolloClient options
  // apollo: { ... }
  getAuth: tokenName => {
    // get the authentication token from local storage if it exists
    const token = localStorage.getItem(tokenName)
    // return the headers to the context so httpLink can read them
    return token ? ('Bearer ' + token) : ''
  },
+ 
+   gitlab_authorization_endpoint: 'http://localhost:10080/oauth/authorize',
+   gitlab_response_type: 'code',
+   gitlab_client_id: 'cfd5dbe5d3093f2eb497030463737dc8438ead0e3779563ead3aaefdfc0838da',
+   gitlab_scope: 'openid email',
+   gitlab_redirect_uri: 'http://localhost:8080/demo/login',
}
+ 
+ export function readGitlabState () {
+   return localStorage.getItem(AUTH_GITLAB_STATE)
+ }
+ 
+ export function redirectGitlab () {
+   let queries = getUrlQueries()
+   if (!queries['code']) {
+     let state = 'abcde'
+     localStorage.setItem(AUTH_GITLAB_STATE, state)
+     localStorage.removeItem(AUTH_TOKEN)
+     window.location = `${defaultOptions.gitlab_authorization_endpoint}?response_type=${defaultOptions.gitlab_response_type}&state=${state}&client_id=${defaultOptions.gitlab_client_id}&scope=${defaultOptions.gitlab_scope}&redirect_uri=${defaultOptions.gitlab_redirect_uri}`
+   }
+ }

// Call this in the Vue app file
export function createProvider (options = {}, { router }) {
  // Create apollo client
  const { apolloClient, wsClient } = createApolloClient({
    ...defaultOptions,
    ...options,
  })
  apolloClient.wsClient = wsClient

  // Create vue apollo provider
  const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
    defaultOptions: {
      $query: {
        fetchPolicy: 'cache-and-network',
      },
    },
    errorHandler (error) {
      if (isUnauthorizedError(error)) {
        // Redirect to login page
        if (router.currentRoute.name !== 'login') {
          router.replace({
            name: 'login',
            params: {
              wantedRoute: router.currentRoute.fullPath,
            },
          })
        }
      } else {
        console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
      }
    },
  })

  return apolloProvider
}

// Manually call this when user log in
export async function onLogin (apolloClient, token) {
  localStorage.setItem(AUTH_TOKEN, token)
  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
  try {
    await apolloClient.resetStore()
  } catch (e) {
    if (!isUnauthorizedError(e)) {
      console.log('%cError on cache reset (login)', 'color: orange;', e.message)
    }
  }
}

// Manually call this when user log out
export async function onLogout (apolloClient) {
  localStorage.removeItem(AUTH_TOKEN)
  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
  try {
    await apolloClient.resetStore()
  } catch (e) {
    if (!isUnauthorizedError(e)) {
      console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
    }
  }
}

function isUnauthorizedError (error) {
  if (error) {
    if (error.message.indexOf('Unauthorized') >= 0 || error.message.indexOf('permission denied') >= 0 || error.message.indexOf('status code 401') >= 0) {
      return true
    }
  }
  return false
}
```

### ~/work/vue-apollo/tests/demo/src/main.js (変更)

冒頭にOpenIDプロバイダーの認可エンドポイントにリダイレクトする処理を追加しました。

```~/work/vue-apollo/tests/demo/src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
- import { createProvider } from './vue-apollo'
+ import { redirectGitlab, createProvider } from './vue-apollo'

Vue.config.productionTip = false

+ redirectGitlab()
const apolloProvider = createProvider({}, { router })

new Vue({
  router,
  store,
  apolloProvider,
  ...App,
}).$mount('#app')
```

### ~/work/vue-apollo/tests/demo/src/components/UserLogin.vue (変更)

OpenIDプロバイダーの認可エンドポイントからリダイレクトされてきた直後の処理を実装します（下図参照）。
今回新規作成したcommon.jsのgetUrlQueries関数を使って、URLパラメーターからstateと認可コードを取り出し、PostGraphileのuserLoginミューテーションに認可コードを渡して呼び出します。
するとPostgreSQL側ではユーザー定義関数「user_login」が呼び出され、認証がOKであればレスポンスとしてPostGraphile用JWTが返ってきますので、それをローカルストレージに設定します。
実装する上での参考URL：[Vue使いなら知っておきたいVueのパターン・小技集](https://qiita.com/HayatoKamono/items/5958d8648007adf6881b)

![new.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/d8505177-0001-f6ba-bab0-8d21e63af865.png)

```~/work/vue-apollo/tests/demo/src/components/UserLogin.vue
<script>
import UserCurrent from '../mixins/UserCurrent'
import USER_CURRENT from '../graphql/userCurrent.gql'
- import { onLogin } from '../vue-apollo'
+ import { onLogin, readGitlabState } from '../vue-apollo'
+ import { getUrlQueries } from '../common'
+ import USER_LOGIN from '../graphql/userLogin.gql'

export default {
  name: 'UserLogin',

  mixins: [
    UserCurrent,
  ],

  data () {
    return {
      showRegister: false,
      email: '',
      password: '',
      nickname: '',
    }
  },

  watch: {
-     // If already logged in redirect to other page
-     userCurrent (value) {
-       if (value) {
-         this.redirect()
-       }
+     // https://qiita.com/HayatoKamono/items/5958d8648007adf6881b
+     isOpen: {
+       immediate: true,
+       async handler () {
+         let queries = getUrlQueries()
+         let state = readGitlabState()
+         if (queries['state'] && queries['state'] === state) {
+           await this.$apollo.mutate({
+             mutation: USER_LOGIN,
+             variables: {
+               input: {
+                 authorizationCode: queries['code'],
+               },
+             },
+           }).then(async (res) => {
+             const apolloClient = this.$apollo.provider.defaultClient
+             await onLogin(apolloClient, res.data.userLogin.usrAndToken.token)
+             apolloClient.writeQuery({
+               query: USER_CURRENT,
+               data: {
+                 userCurrent: res.data.userLogin.usrAndToken.usr,
+               },
+             })
+             this.redirect()
+           }).catch((error) => {
+             console.error(error)
+           })
+         }
+         // localStorage.removeItem(AUTH_GITLAB_STATE)
+       },
    },
  },

  methods: {
-     async onDone (result) {
-       if (this.showRegister) {
-         this.showRegister = false
-       } else {
-         if (!result.data.userLogin) return
-         const apolloClient = this.$apollo.provider.defaultClient
-         // Update token and reset cache
-         await onLogin(apolloClient, result.data.userLogin.usrAndToken.token)
-         // Update cache
-         apolloClient.writeQuery({
-           query: USER_CURRENT,
-           data: {
-             userCurrent: result.data.userLogin.usrAndToken.usr,
-           },
-         })
-       }
-     },
- 
    redirect () {
      this.$router.replace(this.$route.params.wantedRoute || { name: 'home' })
    },
  },
}
</script>

<template>
  <div class="user-login">
    <div class="logo">
      <i class="material-icons icon">chat</i>
    </div>
    <div class="app-name">
      Apollo<b>Chat</b>
    </div>
-     <ApolloMutation
-       :mutation="showRegister
-         ? require('../graphql/userRegister.gql')
-         : require('../graphql/userLogin.gql')"
-       :variables="showRegister
-         ? {
-           input: {
-             email,
-             password,
-             nickname,
-           },
-         }
-         : {
-           input: {
-             email,
-             password,
-           },
-         }"
-       class="wrapper"
-       @done="onDone"
-     >
-       <form
-         slot-scope="{ mutate, loading, gqlError: error }"
-         :key="showRegister"
-         class="form"
-         @submit.prevent="mutate()"
-       >
-         <input
-           v-model="email"
-           class="form-input"
-           type="email"
-           name="email"
-           placeholder="Email"
-           required
-         >
-         <input
-           v-model="password"
-           class="form-input"
-           type="password"
-           name="password"
-           placeholder="Password"
-           required
-         >
-         <input
-           v-if="showRegister"
-           v-model="nickname"
-           class="form-input"
-           name="nickname"
-           placeholder="Nickname"
-           required
-         >
-         <div v-if="error" class="error">{{ error.message }}</div>
-         <template v-if="!showRegister">
-           <button
-             type="submit"
-             :disabled="loading"
-             class="button"
-             data-id="login"
-           >Login</button>
-           <div class="actions">
-             <a
-               data-id="create-account"
-               @click="showRegister = true"
-             >Create an account</a>
-           </div>
-         </template>
-         <template v-else>
-           <button
-             type="submit"
-             :disabled="loading"
-             class="button"
-             data-id="submit-new-account"
-           >Create new account</button>
-           <div class="actions">
-             <a @click="showRegister = false">Go back</a>
-           </div>
-         </template>
-       </form>
-     </ApolloMutation>
  </div>
</template>

<style lang="stylus" scoped>
@import '~@/style/imports'

.user-login
  height 100%
  display flex
  flex-direction column
  align-items center
  justify-content center

.logo
  .icon
    font-size 80px
    color $color

.app-name
  font-size 42px
  font-weight lighter
  margin-bottom 32px

.wrapper
  flex auto 0 0

.form
  width 100vw
  max-width 300px

.form-input,
.button
  display block
  width 100%
  box-sizing border-box

.form-input
  margin-bottom 12px

.actions
  margin-top 12px
  text-align center
  font-size 12px
</style>
```

以上でソースコードの変更は完了です。

# OpenID版ApolloChatの実行

それでは動かしてみます。
以下を実行して、Webサーバー（Vue.jsアプリ配信）を立ち上げます。

```
cd ~/work/vue-apollo/tests/demo
yarn serve
```

正常にコンパイルされることを確認します。
コンパイル後、以下のように表示されました。

```
 DONE  Compiled successfully in 3918ms                                  11:55:16

 
  App running at:
  - Local:   http://localhost:8080/demo/ 
  - Network: unavailable

  Note that the development build is not optimized.
  To create a production build, run yarn build.
```

表示に従い、ブラウザで「 http://localhost:8080/demo/ 」にアクセスして、OpenID版ApolloChatのフロントエンドを起動します。
画面がチラついた後、GitLabの認可画面が出てきました。
本当は認証画面も確認したかったのですが、うっかりGitLabにログインした状態で起動したので、認証画面を飛ばして認可画面が出てきてしまいました。
認証画面は、後ほどGitLabの別ユーザー（一般ユーザー）を作成して確認することにします。

<img width="668" alt="スクリーンショット 2019-09-03 17.38.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/2b0120fe-3d42-7199-07f1-0a18c20eb6f9.png">

Authorizeボタンをクリックすると、画面がチラついた後、ログイン後の画面が現れます。フロントエンドの実装が荒いので画面がチラつきますが、今回はこれで良しとします。左上にGitLabから取得したアカウント情報が表示されています。

<img width="668" alt="スクリーンショット 2019-09-04 11.57.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/20da5661-3f6f-bf27-10aa-88493f9d0490.png">

ちなみに何らかのエラーが起きていたり、操作中にJWTが失効すると、以下のように右側にApolloChatのアイコンが出て、再起動しない限り先に進めなくなります。

<img width="668" alt="スクリーンショット 2019-09-04 13.24.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/b4725deb-f5f4-c25d-e0e2-a667b9221701.png">

ではgeneralチャンネルに入ってみます。JWTが失効していなければ入室できるでしょう。

<img width="668" alt="スクリーンショット 2019-09-04 13.18.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/eb5ae194-f7c2-a266-8321-b19367e922ee.png">

generalチャンネルで左下の「Send bot message」を押してみます。

<img width="668" alt="スクリーンショット 2019-09-04 13.19.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/e785c47e-7a56-e0a5-79b7-d6cfcf493624.png">

「"How are you doing? " + 連番」が投稿されました。
右下のメッセージ入力欄から「こんにちは世界」と投稿してみます。

<img width="668" alt="スクリーンショット 2019-09-04 13.22.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/de074a56-872a-ab67-4359-365f07b4431a.png">

「こんにちは世界」が投稿されました。

それではGitLabで一般ユーザーを作成し、開発ユーザー以外でもアプリを使えるか確認します。ブラウザで「 http://localhost:10080/ 」にアクセスし、開発ユーザーからサインアウトします。

<img width="668" alt="スクリーンショット 2019-09-04 13.28.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/287e082c-6c52-c2cb-65a4-2bb7278a7c19.png">

一般ユーザーを作成します。

<img width="668" alt="スクリーンショット 2019-09-04 13.30.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/aee8553e-7768-ab11-22cb-96352958008d.png">

すぐにサインアウトします。

<img width="668" alt="スクリーンショット 2019-09-04 13.32.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/3483ba8e-0cfc-d169-6754-21f52642b16a.png">

全ユーザーからサインアウトできました。

<img width="668" alt="スクリーンショット 2019-09-04 13.34.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/e95dba63-25c6-e3b6-f8b0-648b143c38ab.png">

ブラウザで「 http://localhost:8080/demo/ 」にアクセスして、OpenID版ApolloChatのフロントエンドを起動します。

画面がチラついた後、GitLabの認証画面が出てきました。

<img width="668" alt="スクリーンショット 2019-09-04 13.34.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/84f6ece7-dd20-538a-b0c8-c1d191949286.png">

Sign inタブをクリックし、一般ユーザーでサインインします。

<img width="668" alt="スクリーンショット 2019-09-04 13.40.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/31ebe8cc-2b59-f5b5-92c3-83f7a8c86c53.png">

初回ですので、一般ユーザーに対する認可画面が出てきました。

<img width="668" alt="スクリーンショット 2019-09-04 13.41.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/e5987695-ecd4-6f77-a4e4-6236126baf43.png">

Authorizeボタンをクリックすると、画面がチラついた後、ログイン後の画面が現れます。左上にGitLabから取得した一般ユーザーのアカウント情報が表示されています。

<img width="668" alt="スクリーンショット 2019-09-04 14.38.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/1772341c-a3ca-1708-cf9a-72f341954b0d.png">

generalチャンネルに入って、右下のメッセージ入力欄から「Hello World!」と投稿してみます。

<img width="668" alt="スクリーンショット 2019-09-04 13.45.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/caf4bc44-4257-9f80-275c-17ead8861486.png">

「Hello World!」が投稿され、一般ユーザーもアプリが使えることが確認できました。