Auth0を使ってSPA(Vue.js, Python)の認証機能を作ってみた

はじめに

インターンでお世話になっている、コウゲと申します。現在担当させていただいている仕事は業務で使用している管理画面のマイクロサービス化です。

マイクロサービス間をつなぐ認証機能が必要だったので、認証プラットフォームとして使いやすそうな 「Auth0」で どんなことができるのか、実現したいことは可能なのかを検証してみることになりました。

なぜAuth0なのか

  • ダッシュボードの設定 + 少ないコード で認証機能が簡単に実装できる。

  • Auth0専用ライブラリやSDKが多数存在し、目的に応じてそれを利用することで 簡単にログイン機能を実装することができる。

実現したいこと

f:id:kohgeat:20191120183944p:plain

発表

ありがたいことに、調べた結果を社内で発表する場を設けていただきました。

ブログの内容より少し詳しく書いています。読んだら即実装できるようなスライドを心がけて作りました!

speakerdeck.com

Vue.jsのSPAに認証機能を追加

Vue.jsのSPAにログイン機能を追加するだけなら、このチュートリアルさえやればできるようになる感じでした。

この他にも、Auth0は公式が出しているドキュメント類が豊富なので(分からないことを調べると最終的に公式ドキュメントに辿り着く)、とても良いプラットフォームだなと感じました。

スライドでハンズオンっぽくまとめてあるので見ていただけたら幸いです。

auth0.com

バックエンド(Python)について

SPAにおけるAuth0認証の流れは以下のようになっています。

f:id:kohgeat:20191127165519p:plain
参照元(https://auth0.com/docs/microsites/call-api/call-api-single-page-app)

画像の通り、バックエンドのAPIは フロントエンドのSPAとしか繋がっていません。

Auth0 はJWTを用いて認証しているので、フロントエンドがログイン成功時 Auth0 サーバーから受け取るJWTをバックエンドへ渡し、そのJWTに書かれている署名が正しいかを公開鍵でチェックすることでバックエンドに認証機能を持たせることができます。

JWT って何?

JWT: 電子署名付きのJSON

ユーザーの情報, 発行元, 電子署名の形式 とかがJSONで書かれていて、改ざんされてないことを電子署名で示しているものです。 電子署名は人間がハンコを押す感じと同じ気がしています

f:id:kohgeat:20191127172106p:plain
JWTがどんな感じか見れます(https://jwt.io/)

上の画像を見ると payload の部分に permissions: [ "read:api" ] があり、これがRBACに必要な部分です。

RBACって何?

RBAC: 役割ベースのアクセス制御

簡単に説明すると、ログインしているユーザーに [read:api] みたいな役割を持たせて、これを持っているユーザーだけがAPIにアクセスできるようにすることです。

auth0.com

JWTに "permissions": [read: api] みたいな項目 を加えるにはAuth0 ダッシュボード側で設定が必要で、発表スライドで設定方法を説明しています。

バックエンドの処理の流れ

f:id:kohgeat:20191127180015p:plain
fastAPでJWTの検証を行う方法(スライドから引用)

Auth0でSPAの認証機能作成時、バックエンドAPIに "JWTの検証" という処理が必要になります。

auth0.com

最初この言葉の意味が分からなかったのですが、簡単に言うと、『JWTが書き換えられていないか』、『Auth0が発行したJWTか』を電子署名を用いて確認しています。(JWTの説明もスライドにまとめているので気になった方は見てみてください。)

そしてこの "JWTの検証" を行うために、以下の三つ処理を バックエンド に追記しました。

  1. フロントから認証用の JWTトークン を受け取る
  2. Auth0API へ JWK(JWTトークン検証用の公開鍵) をリクエスト
  3. JWK を用いて JWTトークン の検証(署名が適切かの検証)を行う (使用ライブラリ: https://python-jose.readthedocs.io/en/latest/jwt/api.html)

それと、今回はログインしているユーザーによってアクセスできるエンドポイントを制御したい( RBAC を実現したい)ので permissions をチェックするコードも追加しています。

以下書いたコードとスライドの引用です

f:id:kohgeat:20191127180015p:plain
fastAPでJWTの検証を行う方法(スライドから引用)

# JWT検証用のクラス

import requests
from jose import jwt
from fastapi import HTTPException
from starlette.status import HTTP_403_FORBIDDEN
from starlette.requests import Request
import enum


auth_config = {
    'domain': "dev-928ngan9.auth0.com",
    'audience': "https://api.mysite.com",
    'algorithms': ["RS256"]
}


class Permission(enum.Enum):
    READ_API = 'read:api'


class JWTBearer():
    def __init__(self, auth_config, permission: Permission, verify_exp=True):
        self.auth_config = auth_config
        self.permission = permission
        self.verify_exp = verify_exp

    def get_jwk_from_auth0API(self):
        # Auth0APIからJWK(公開鍵)を取得
        json_url = f"https://{self.auth_config['domain']}/.well-known/jwks.json"
        jwks = requests.get(json_url).json()
        return jwks

    def get_token_from_client(self, request: Request):
        # フロントエンドから送られてきたJWTの必要な部分を取得
        auth = request.headers.get("authorization", None)
        _, token = auth.split()
        return token

    def check_permissions(self, token):
        # RBAC用にpermissionsの確認
        permissions = jwt.get_unverified_claims(token)['permissions']
        if self.permission.value in permissions:
            return True
        else:
            return False

    async def __call__(self, request: Request):
        # このクラスを関数的に使えるようにする
        jwks = self.get_jwk_from_auth0API()
        token = self.get_token_from_client(request)
        unverified_header = jwt.get_unverified_header(token)
        rsa_key = {}

        for key in jwks['keys']:
            if key['kid'] == unverified_header['kid']:
                rsa_key = {
                    "kty": key["kty"],
                    "kid": key["kid"],
                    "use": key["use"],
                    "n": key["n"],
                    "e": key["e"]
                }
        if rsa_key:
            try:
                payload = jwt.decode(
                    token,
                    rsa_key,
                    algorithms=self.auth_config['algorithms'],
                    audience=self.auth_config['audience'],
                    issuer="https://" + self.auth_config['domain'] + "/",
                    options={'verify_exp': self.verify_exp}
                )
                if self.permission:
                    if self.check_permissions(token) is False:
                        raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="JWK invalid")
            except jwt.ExpiredSignatureError:
                    raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="JWK invalid")
            return payload
        raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="JWK invalid")

まとめ

Auth0は認証機能を作成したことがなかった自分でも簡単に扱えたので、すごいサービスだなと感じました。そして、公式ドキュメントに基礎から実装方法まで丁寧に書いてあるので勉強にもなります。

これからSPAやアプリに認証機能を追加したいと考えている方は使ってみてはいかがでしょうか?

さいごに

今回、Auth0の技術検証から発表、ブログ執筆までやらせていただいたのですが全ての段階でためになることばかりでした。技術検証では認証機能の裏で動いていることを知るきっかけになり、プログラミングでブラックボックスにしていた部分が一つ明るくなった気がします。

この場をお借りして、様々な知見を与えてくださったエンジニアの方々や発表を聞いてくださった方々、お世話になっている会社の方々にお礼を申し上げたいと思います。ありがとうございます!

明日の記事は ぬまっち(@nuMatch) さん の Flutter と Firebase Authentication の話です!