この記事で行うこと
この記事の続編です→「vue-apolloに付属するデモチャットアプリ「ApolloChat」のGraphQLバックエンドをPostGraphileに置き換えてみた」
自前のユーザー認証をやめて、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を使用して作成し、そのファイルは「 ~/work/vue-apollo/tests/demo/doc/ 」に保存しました。
【旧】 自前認証
【新】 OpenID ConnectでSSO
ユーザー認証後の流れ【新旧共通】
実装の手抜き(デモにありがちな)
- httpsで通信すべきところもhttpで通信しています。
- 本記事ではJWTをローカルストレージに保存していますが、本番環境ではなさらないようお願いします。HTML5のLocal Storageを使ってはいけない(翻訳) (2019-10-10追記)
- PostGraphile用JWTが"jwt expired"になった後の処理は実装していませんので、JWT関連で動きが怪しくなったらWebフロントエンドを再起動してください。再起動するとローカルストレージの古いJWTが削除され、OpenIDプロバイダーの認可エンドポイントにリダイレクトし直します。
- PostGraphile用JWTの期限を発行後60分にし、GitLabのトークンの期限との同期を考慮しておりません。
- 認可エンドポイントに渡すstateパラメーターは、毎回ランダム文字列を生成すべきところですが、今回は固定で'abcde'という文字列を使用しています。
- フロントエンドの実装が雑なので、画面がチラつく等、動きがどんくさいです。
- その他、前回の記事と同様に手抜きがあります。
- 私(C++おじさん)はまだJavaScriptもVue.jsも経験が浅いので、変な箇所がありましたら優しくご指摘ください。
参考ページ(感謝します)
多分わかりやすいOpenID Connect
OAuth 2 in Action
[Python] PyJWT で Google OAuth 2.0 API の ID Token を検証
PostgreSQLで「あればUPDATE、なければINSERT」のUPSERTをやってみる
JavaScriptでURLクエリを取得する
Vue使いなら知っておきたいVueのパターン・小技集
Visual Studio Code で UML を描こう!
PlantUML 概要:シーケンス図
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のパスワードを変更します。

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

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

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

下の画面が出たら以下を入力し、Save applicationボタンを押します。
Name:test01 (任意の名称)
Redirect URI:http://localhost:8080/demo/login (後ほどフロント開発で実装します)
チェック:openid、email

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

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を両方試してみた」の「環境構築その1」と同様の作業が必要となります。本記事では割愛します。
それに加え、以下の手順も必要です。
「docker-compose up」でコンテナを立ち上げる前に、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作成
前回の記事に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
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関数
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使用)
user_login関数を実装する上での参考URL:[Python] PyJWT で Google OAuth 2.0 API の ID 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;
できそう(^^)PostgreSQLのユーザー定義関数でPythonが使えて助かった。PL/pgSQLだったらどう実装して良いか(そもそも実装できるのか)わからなかった。
— 普通のプログラマ@舌下免疫療法中 (@kanedaq) August 30, 2019
環境構築&バックエンド実装(その3):PostGraphile関連
前回の記事の「環境構築&バックエンド実装(その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
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パラメーターは今回は固定値を使っていますが、本来は毎回ランダム文字列を生成すべきです。
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プロバイダーの認可エンドポイントにリダイレクトする処理を追加しました。
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のパターン・小技集
<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の別ユーザー(一般ユーザー)を作成して確認することにします。

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

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

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

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

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

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

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

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

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

ブラウザで「 http://localhost:8080/demo/ 」にアクセスして、OpenID版ApolloChatのフロントエンドを起動します。
画面がチラついた後、GitLabの認証画面が出てきました。

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

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

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

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

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