VISASQ Dev Blog

ビザスク開発ブログ

Auth0による認証方法を完全に理解した

こんにちは!クライアント開発チームの山中です。

ビザスクでは認証にAuth0を使用しており、Auth0による認証の仕組みを調べる機会があったので、せっかくならテックブログにまとめよう!と思い立ち、Auth0を使った実装方法についてまとめてみました!

JWT認証とは

JWT(JSON Web Tokenの略、読み方はジョット)認証は、トークンベースの認証方式です。従来のcookieとDB側のセッション情報を使ったセッションベース認証とは異なり、API側でユーザーのセッション情報を保持しません。

従来のセッション認証との違い

項目 セッション認証 JWT認証
状態管理 ステートフル(サーバーがセッション情報を保持) ステートレス(サーバーがセッション情報を保持しない)
スケーラビリティ セッション情報の共有が必要 セッション情報の共有が不要
セキュリティ セッションID漏洩リスク トークン漏洩リスク

JWTの仕組み

JWTは事前に発行されたトークンをフロントエンドで保存しておき、リクエストのたびにそのトークンを送付します。API側では、そのJWTが正しいかどうかを毎回検証することで、認証されたユーザーからのリクエストかどうかを判定します。

JWTの構造

JWTは以下の3つの部分から構成されています

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  // ヘッダー
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9  // ペイロード
.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ  // 署名
  1. ヘッダー: アルゴリズムトークンタイプの情報
  2. ペイロード: ユーザー情報やクレーム(主張)を含むデータ
  3. 署名: ヘッダーとペイロード秘密鍵で署名したハッシュ値

JWT認証の方式

どちらの方式でも、ヘッダーとペイロードを元に鍵を使って署名が正しいかどうかを検証します。

共通鍵方式

1. ログイン成功 → サーバーで秘密鍵を使ってJWTに署名
2. リクエスト時 → JWTをヘッダーに付与
3. 検証時     → サーバー側で同じ秘密鍵を使って署名を検証

この方式では、JWT発行側と検証側が同じ秘密鍵を共有している必要があります。

公開鍵方式

Auth0では公開鍵方式が採用されています

1. ログイン成功 → Auth0が秘密鍵を使ってJWTに署名
2. リクエスト時 → JWTをヘッダーに付与
3. 検証時     → Auth0から公開鍵を取得し、署名を検証

この方式では、JWT発行側(Auth0)が秘密鍵を管理し、検証側(アプリケーション)は公開鍵のみで検証できます。

Auth0における認証フロー

Auth0では Authorization Code Flow が使用されます。以下がその流れです:

認証フローの詳細

1. ログインボタンクリック
   ↓
2. /authorize エンドポイントへリダイレクト
   (コールバック先もクエリパラメーターで指定)
   ↓
3. Auth0がユーザーをログイン画面へリダイレクト
   ↓
4. ユーザーがログイン情報を入力・送信
   ↓
5. 指定したコールバック先にcodeパラメーター付きでリダイレクト
   ↓
6. /oauth/token にcodeを渡してトークンを取得

この流れにより、セキュアな認証が実現されています。

auth0-spa-jsを使った実装

実際の実装においては、Auth0が提供しているライブラリを使用することで、1から実装することなく導入することができます。
実際の実装方法を見ていきましょう。

フロントエンド実装

Auth0Clientの初期化

import { Auth0Client } from '@auth0/auth0-spa-js';

const auth0 = new Auth0Client({
  domain: '<AUTH0_DOMAIN>',
  clientId: '<AUTH0_CLIENT_ID>',
  authorizationParams: {
    redirect_uri: '<MY_CALLBACK_URL>'
  }
});

ログイン処理

ログインボタンが押されたときの処理

// ユーザーを /authorize エンドポイントへリダイレクト
auth0.loginWithRedirect()

この関数により、ユーザーはAuth0のユニバーサルログイン画面へリダイレクトされます。

コールバック処理

ユーザーがログイン後、コールバックページで以下の処理を実行

// コールバック処理(code → token変換 & キャッシュ保存)
auth0.handleRedirectCallback()

handleRedirectCallback関数の内部では以下の処理が行われています https://github.com/auth0/auth0-spa-js/blob/main/src/Auth0Client.ts#L486C16-L486C38

  1. クエリパラメーターからcode、stateなどを取得
  2. codeを使ってtokenを取得
  3. tokenを保存

デフォルトではtokenはブラウザ上のメモリに保存され、リロードなどで失われます。
保存先をlocalStorageに指定することもでき、その場合はtokenを永続化されます。 https://auth0.com/docs/libraries/auth0-single-page-app-sdk#change-storage-options

トークンの取得と利用

// キャッシュからトークンを取得
const token = await auth0.getTokenSilently();

// APIリクエスト時にヘッダーに付与
const response = await fetch('/api/protected', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

Auth0のActionsを設定することで、JWTのペイロードにユーザーIDなどをカスタムクレームとして含めることも可能です。

サーバーサイド実装

Auth0は公開鍵認証方式でJWTを検証します

1. Auth0から公開鍵を取得
   ↓
2. 公開鍵を使ってJWTを認証
   ↓
3. ペイロードをデコードして、カスタムクレームからユーザーID等を取得

実装例(Python + FastAPI)

Claudeで生成したものなので、動作する保証はないですが、、

import jwt
import requests
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

app = FastAPI()
security = HTTPBearer()

# Auth0設定
AUTH0_DOMAIN = "YOUR_DOMAIN"
AUTH0_API_AUDIENCE = "YOUR_API_IDENTIFIER"

def get_public_key(token):
    """JWTのkidに基づいて適切な公開鍵を取得"""
    # JWTヘッダーからkid(Key ID)を取得
    unverified_header = jwt.get_unverified_header(token)
    kid = unverified_header.get("kid")
    
    # JWKSから対応する公開鍵を取得
    jwks_url = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
    response = requests.get(jwks_url)
    jwks = response.json()
    
    for key in jwks["keys"]:
        if key["kid"] == kid:
            return key

def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    """JWT検証"""
    try:
        # 公開鍵を取得してJWTを検証
        public_key = get_public_key()
        token = credentials.credentials
        
        payload = jwt.decode(
            token,
            jwt.algorithms.RSAAlgorithm.from_jwk(public_key),
            algorithms=["RS256"],
            audience=AUTH0_API_AUDIENCE,
            issuer=f"https://{AUTH0_DOMAIN}/"
        )
        return payload
    except Exception:
        raise HTTPException(status_code=401, detail="Invalid token")

@app.get("/protected")
async def protected_endpoint(current_user=Depends(verify_token)):
    """認証が必要なエンドポイント"""
    return {
        "message": "Hello, authenticated user!",
        "user_id": current_user.get("your_app/user_id") # Auth0側で指定したカスタムクレーム
    }

まとめ

JWT認証とAuth0を使った実装について説明しました。

Auth0を導入することで、認証周りの複雑な実装から解放され、より本質的な機能開発に集中できるようになります。
セキュリティ面でも専門性の高いAuth0に任せることで、安全性を高めることができます。

今回の実装方法が、JWT認証やAuth0の導入を検討されている方の参考になれば幸いです!

ビザスクではエンジニアの仲間を募集しています! 少しでもビザスク開発組織にご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう! recruit.visasq.co.jp