Heroku(Python3)+Auth0+GitHub Pages(Vue.js)で大学の関係者だけが見れるページをサクッと(?)作る。



# 前作の話

こちらは前に書き、なんとトレンドに乗っかってしまった記事です。

[Firebase+Vue.jsで大学の関係者だけが見れるサイトをサクッと作る - Qiita](https://qiita.com/reud/items/06f2b87e96c7384e1609)

この投稿からなんと5ヶ月が経ちました。

いろいろ学びを深めたのでそのアウトプットがてら、この記事を持ちましてリベンジをさせて頂きたいと思います。

# 概要
学生に配布されるメールアドレスのドメインを見て、同じ大学のメールアドレスのドメインの場合だけ見れるようなサイト（ページ）を作りたい！

# 使う技術・サービス・言語

- github.io 
  - 静的サイトをホスティングしてくれるサービス。GitHubアカウントがあれば利用できます。
- Auth0 
  - 認証プラットフォーム、〇〇でログインとか簡単に実装できてすごく便利。無料でもそれなりに遊べます。
  - 今回の記事はAuth0のRulesが便利すぎて凄いぞと思って生まれています。
- Heroku 
  - アプリを実行してくれるプラットフォーム、いわゆるPaaS。今回はAPIサーバ扱い
- Python3 
  - ここなら割とお馴染みプログラミング言語、機械学習が熱い(らしい)
- Vue.js (JavaScriptフレームワーク)

# 動作環境
- macOS Catalina 10.15 Beta
- Python3.7.3
- nodejs v10.15.3
  - npmは v6.4.1


# 動作イメージ
1. ログインの処理をAuth0にぶん投げます。
2. アクセストークン の検証をHeroku(Backend,Python)で ~~適当に~~ やって、正しい形式なら秘密の情報を返します。
3. フロントエンドは受け取った情報を良い感じにして表示します。(今回はGitHub Pagesにデプロイします。)

この手順をサクッと進めていこう！　と言う企画。

かなり雑ですがこんな感じ

![アーキテクチャ図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/682ecea1-b2b2-03c1-1054-01c7dcfde56f.png)


# 作成手順

この記事では以下の手順で実装していきます。

1. Auth0側設定
2. Herokuのアカウント作成、アプリ作成、Python3でのコード作成、デプロイ(すなわちバックエンドの構築)
3. フロントエンドアプリの作成
4. デプロイ
5. 大学のメールドメインのバリデーション


# Auth0側設定

ここにアクセスしてサインアップしましょ
[Never Compromise on Identity. - Auth0](https://auth0.com/jp/)


サインアップをすると、テナントドメインを決めてくださいと言う旨のメッセージが出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/599c4ea9-01d6-da97-edd2-24ce3e530bc5.png)

でこの辺はお好きに。

完了したらダッシュボードに移動して、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/7d9dbf05-52e9-6b9c-1f5c-7a3df1701490.png)

CREATE APPLICATOIONを選択して、作成画面に移行。

 アプリ名とかお好きに、application typeは**Single Page Web Applications** に設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/1ea5a65f-edc2-9e4e-b996-2c0e3f8a8e75.png)

こんな感じで作成します。

Auth0のテナント名はメモっておいてください。

`<ここ>.auth0.com`です。

次に、Googleログインを無効にします。(今回はメアド+パスワードのみの許可にしたいので)

出来たら、Applicationsから自分の作成したアプリを選択しましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/d24c78a5-cd65-8c82-d3e1-61bc250d8248.png)

そしてタブからConnectionsを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/6848dbda-f223-9878-6f28-f50aeee46a1c.png)

そしてこんな感じにgoogle-oauth2を無効にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/ca9f81aa-0678-5620-fe4d-8adee36a2b12.png)

ここまできたら完了です。お疲れ様です。

# バックエンド環境の構築
## Herokuアカウントの作成
[Heroku | Sign up](https://signup.heroku.com/login)

ここにアクセスしてサインアップしましょう。

## アプリの作成
アカウント作ったらこんな感じの画面になるはず

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/8adf697a-fab2-4427-1425-1b595ea278ba.png)

Create New Appを選んでApp nameやRegionを設定。この辺はお任せします。

今回はこんな感じにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/faf2f710-5d88-d36a-d0bd-494a4b616d94.png)

## デプロイ準備
アプリ作ったらいろいろ出てきますが、まずは[Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line)だけインストールしましょう。(HerokuのデプロイとかをCLIからやってくれるツールです。)

```sh
$ brew tap heroku/brew && brew install heroku
```
インストールできたら、

```sh
$ heroku login
```

でログインをしましょう。

## コーディング
とりあえず、こんな感じの要件で作るとします。

```
・ アクセストークンが正しければ、JSON形式で{ "himitu": "ゴニョゴニョ"}
・ アクセストークンが変だったら403を返す。
```

大体こんな感じの機能で作ります。

今回はAPIフレームワークとしてFastAPIを使います。

かなり新しめのフレームワークです。

[Features - FastAPI](https://fastapi.tiangolo.com/features/)

理由としてはAPIサーバ作るなら一番シンプルだったり、今回は関係ないですがSwagger UIで自動的にdocumentを生成してくれたりで学習コストが軽量な割りに凄く便利で気に入ったからです。 **ついでに僕は新しい物好きなので**

で、Heroku+FastAPIってことをしているサンプルコード探していたのですが、ないので作りました。　今回はこれを弄くり回して使いましょう。 

[reud/fast-api-heroku-sample: Python-Server + Heroku Sample](https://github.com/reud/fast-api-heroku-sample)

forkしてcloneとかしてください。

で、gitリポジトリできたらとりあえず、プロジェクトフォルダ直下で


`$ heroku git:remote -a アプリ名` でリポジトリとherokuアプリの結び付けを行います。

結び付けが出来たら、githubとかにpushする感覚と同様にaddとか色々し終わってるのを確認してから、

`git push heroku master` でdeployが完了します。

(手順わからない場合は参考資料が凄く役に立つはずなのでそちらをご覧ください)

僕の場合はApp nameの関係で、
`https://himitu-api-server.herokuapp.com/` にデプロイされます。

他の人は`https://<App name>.herokuapp.com/`にデプロイされると思います。

ブラウザでアクセスしてみてコード通りの動作している事を確認してください。

多分こんな感じでJSONがブラウザに表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/5052ee01-a224-5b6f-6f00-7fb0a9b9ed17.png)

で、ここからコードを弄りましょう。

## その前に

今回はAuth0からアクセストークンを貰う形式ですが、何に対して発行するかで、やることが変わってきます。

今回はベストなやり方としては、APIサーバを作っている以上Auth0にカスタムAPIを作成して、「このカスタムAPIをください！」ってaudienceを使用して教えて上げる必要があります。(audienceはどのリソースサーバを使用するか決定するために指定します。)

すなわち、Auth0にURL (https://himitu-api-server.herokuapp.com/) でカスタムAPIを作成するのが一番正しいはずです。

しかし、カスタムAPIを作成すると手間がまぁ増えます。

参考: [Validate an Access Token - Auth0](https://auth0.com/docs/tokens/guides/access-token/validate-access-token)

端的に言うと、カスタムAPIでアクセストークンを得た場合、トークン検証が必要になります。アクセストークンはJWTと言われる形式で渡されるのでそれをゴニョゴニョしてJWKSを照らし合わせて・・・ってやるのですが、それだけで1記事出来てしまうのと、まだ勉強不足なので今手を出すとこの記事が未完になりそうなため少し手抜きをします。

取得するAuth0のアクセストークンのaudienceをManagement APIにします。こうした場合はJWTの検証は不要になります。

Management APIはユーザの作成や削除などAuth0の全体(テナントって呼ばれています。)を管理する**Auth0のAPI**です。

Auth0のAPIを発行しているためAuth0がトークンの検証を担保するイメージですね。(これを利用して検証をサボります。)

そんな激ヤバなトークンを発行して良いのか、トークン抜き出されたら終わりではって思うかもしれませんが、その辺も大丈夫です。

さらにSPA側でManagement API アクセストークンを発行すると、勝手にscopeに制限がかかり、出来ることが大幅に減ります。

参考: [Get Management API Tokens for Single-Page Applications -Auth0](https://auth0.com/docs/api/management/v2/get-access-tokens-for-spas)

上記サイトを見る限り、他のユーザに干渉出来るようなscopeは一つも貰えず、良い感じに渡しても良いscopeしか渡さなくなります。

今回は、scopeに**read:current_user**を入れて、バックエンドはSPAから受け取ったアクセストークン でManagement APIを叩いて、上手く行ったらOKということにしちゃいましょう。

# コードを書く
とりあえず、今回はリクエストからAuthorizationヘッダーの値をぶっこ抜いて、得られた文字列をManagenement APIに投げて、上手く行ったらOKみたいな感じにしたいと思います。

まずは追加でパッケージのインストール、

```sh
pip install starlette
pip install requests
```

コードを書きましょう。 `main.py` にこんな感じのコードをつらつらと、

```python
import traceback

import requests
from fastapi import FastAPI, HTTPException
from starlette.requests import Request
from starlette.middleware.cors import CORSMiddleware
import os

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,   # 追記により追加
    allow_methods=["*"],      # 追記により追加
    allow_headers=["*"]       # 追記により追加
)

@app.get("/")
def read_root():
    return {"200": "Welcome To Heroku"}


@app.get("/himitu/{user_id}")
def read_item(user_id: str, request: Request):
    k = request.headers.get('Authorization')
    if not k:
        raise HTTPException(status_code=403, detail="forbidden")
    headers = {'Authorization': k}
    # 適宜読み替えてください
    try:
        r = requests.get(f'https://{ os.getenv("TENANT") }.auth0.com/api/v2/users/{user_id}', headers=headers)
    except:
        raise HTTPException(status_code=403, detail=traceback.print_exc())
    j = r.json()
    try:
        # checker
        n = j['nickname']

        return {'himitsu': os.getenv('HIMITSU')}
    except:
        raise HTTPException(status_code=403, detail='forbidden')
```

で、

`$ heroku config:set HIMITSU="<あなたの秘密のメッセージ>" TENANT="<あなたのAuth0 テナント名>"`

をして、Herokuに環境変数を設定します。

Postmanやcurlなどのツールでリクエストを送ってみて正常に動作したらバックエンドの構築は完了！！お疲れ様です。

# フロントエンドの環境構築

サクッと作るためにAuth0のサンプルを持ってくるとします。

- [auth0-samples/auth0-vue-samples: Auth0 Integration Samples for Vue.js Applications](https://github.com/auth0-samples/auth0-vue-samples)

これをforkしてcloneしちゃいましょう。

upgrade-sdkブランチに切り替え、ざっとこんな感じに直してください。(URLはよしなに・・・)

デプロイ先は`https://<GitHub ユーザ名>.github.io/auth0-vue-samples/` になるはず。

[update · reud/auth0-vue-samples@b73103a](https://github.com/reud/auth0-vue-samples/commit/b73103a4c491b5e1730aa6dd90f0f97bc3aaee47)

diffにも出ちゃってますが、デプロイに `gh-pages` を使用するので、

`npm install gh-pages --save-dev`

でインストールしちゃいましょう。

色々編集し終えたら、

`npm run build` からの`gh-pages -d dist` でアップロードされます。

アップロード後は`https://<GitHub ユーザ名>.github.io/auth0-vue-samples/`にアクセスして、動作するか確認してみましょう。 アクセス後、画面右上LOGINからユーザを作成して、ログインしてみます。

ログインしてこんな感じの画面になればおkです。(Vue.jsのロゴは読み込まれていませんが、今回は直さないまま進めちゃいます。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/b457eacd-bc64-6010-e759-8feec88d470c.png)

大丈夫そうなら、上記ナビゲーションバーから秘密のページに移動して、Call APIをクリックして、バックエンドとの疎通を試みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/c7befc4b-c2b5-a4bd-3de7-44c0e3fcbc0a.png)

こんな感じでうまくいけば完了です！

# 大学の関係者のみ見れる様にする
ここまででログイン時のみ見れる特別なページを作る事が出来ました。
しかし今のままでは大学の関係者だとか関係なく、メールアドレスを持っていれば、ページがみれてしまいます。

そこで、メールアドレスが大学のドメインでない場合は、ログインに失敗させる様にしましょう。

Auth0のダッシュボードにあるサイドバーからRulesを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/cfc78de6-a8e5-5a95-36d1-e9b6fe667ec7.png)

## 大学のメールドメインでない場合は弾く様に設定

CREATE RULEを選ぶ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206574/2b64d4b8-68d6-57c3-eaca-08a116959a4b.png)

テンプレート一覧が表示されるので、

`Check if user email domain matches configured domain` を選択。

そして中身をこんな感じに編集

```javascript
function (user, context, callback) {

  const allowedDomains = new Set(['stu.hoge.ac.jp']);
	
  // Access allowed if domain is found
  const emailSplit = user.email.split('@');
  const userEmailDomain = emailSplit[emailSplit.length - 1].toLowerCase();
  if (allowedDomains.has(userEmailDomain)) return callback(null, user, context);

  return callback('Access denied you are'+userEmailDomain+" but, expected stu.hoge.ac.jp");
}
```
これはログイン時のメールアドレスが、`stu.hoge.ac.jp`でない場合はログイン失敗させるというRuleです。`stu.hoge.ac.jp`をいじって自分の大学のメールドメインに変更してください。

## メールの確認されていない場合は弾く様に設定
デフォルトでは、メールアドレスを確認しなくてもそのままログインできてしまいます。

すなわち、サインアップ時にドメインだけ正しければ、適当なメールアドレスを入れてしまえば秘密のページが見れてしまう事になります。 

これは流石によくないので、メールアドレスの確認を強制させましょう。 これもAuth0のRuleで上手くやってくれます。

先ほどと同じ様にCREATE RULEを選んで、Force email verificationを選択、今回は何も編集せずにSAVE CHANGESで大丈夫です。

# 動作を確認する。

(ログイン失敗時などはつどCookieを削除してみてください)

今回は以下の部分において手抜きをしているため、正常に動作しません。

- ナビゲーションバーの推移先(URLの設定が上手くいっていないため)
- ログイン・サインアップ失敗時(URLみるとログイン・サインアップ失敗理由が書いてありますが、Vue側は何もしていないので何も出てきません。また、その状態でLOGINボタンを再度押してもログイン失敗してしまうため、Cookieを手動で削除する必要があります。)

また、以下の部分は甘いです。

- Heroku側のCORS設定(もっと絞り込む必要あり)
- カスタムAPIを使用していない(ユーザ毎に権限を振り分けたい時は)(このままだとあまり認可っぽくない)
  - 今回はJWT検証サボりたいためこんな感じの実装にしていますが、ちゃんとしたサービス作るならカスタムAPIにすべきかと

もっと洗い出せば沢山出てきそうですが、とりあえずこの辺り。

所属大学の学生を対象としたサービスなどを作ろうと考えている際はこの辺りを気を付けつつこの記事を見れば役に立つのかもしれません。

以上です。ありがとうございました。



# 参考資料

- [認証プラットフォーム Auth0 とは？ - Qiita](https://qiita.com/furuth/items/68c3caa3127cbf4f6b77)
- [【Python】PythonプログラムをHerokuにデプロイする方法 - Qiita](https://qiita.com/1-row/items/80f89c8ada2e61f04446#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)
- [JWT Authentication with FastAPI and AWS Cognito - Data Driven Investor - Medium](https://medium.com/datadriveninvestor/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e)
- [Why is it necessary to pass the 'audience' parameter to receive a JWT? - Auth0 Community](https://community.auth0.com/t/why-is-it-necessary-to-pass-the-audience-parameter-to-receive-a-jwt/11412/3)
- [Get Management API Tokens for Single-Page Applications](https://auth0.com/docs/api/management/v2/get-access-tokens-for-spas)
- [FastAPIでCORSを回避 - Qiita](https://qiita.com/angel_katayoku/items/0e1f5dbbe62efc612a78)