Google OpenID Connect (まとめ) - gosaaan1/hokulea-garage GitHub Wiki

Google OpenID Connect

Google OpenID Connectとは

Google OpenID Connectは、GoogleがOAuth 2.0プロトコルの上に構築した認証レイヤーです。これにより、クライアントアプリケーションはGoogleアカウントを使用してユーザーを安全に認証し、ユーザーの基本的なプロフィール情報を取得することができます。

認証フローの種類

OAuth 2.0には複数の認証フローがありますが、ここでは主に「認可コードフロー」と「IDトークンフロー」について説明します。

OAuth 2.0の主な認証フローの種類を以下に列挙します:

  1. 認可コードフロー(Authorization Code Flow)

    • 標準的なサーバーサイドWebアプリケーション向け
  2. 暗黙的フロー(Implicit Flow)

    • シングルページアプリケーション(SPA)向け
    • 現在は非推奨
  3. リソースオーナー・パスワード・クレデンシャルズ・フロー(Resource Owner Password Credentials Flow)

    • ユーザー名とパスワードを直接使用する信頼されたアプリケーション向け
  4. クライアント・クレデンシャルズ・フロー(Client Credentials Flow)

    • マシン間通信やバックグラウンドプロセス向け
  5. デバイス認可フロー(Device Authorization Flow)

    • 入力制限のあるデバイス(スマートTV、IoTデバイスなど)向け
  6. リフレッシュトークンフロー(Refresh Token Flow)

    • アクセストークンの更新に使用
  7. PKCE拡張を使用した認可コードフロー(Authorization Code Flow with Proof Key for Code Exchange)

    • モバイルアプリやSPA向けのセキュアな認可コードフロー
  8. ハイブリッドフロー(Hybrid Flow)

    • 認可コードフローと暗黙的フローの特徴を組み合わせたフロー
  9. バックチャネル認証フロー(Backchannel Authentication Flow)

    • デバイス間認証やサイレント認証に使用

これらのフローは、アプリケーションの種類やセキュリティ要件に応じて選択されます。

認可コードフロー

認可コードフロー(Authorization Code Flow)は、主にサーバーサイドアプリケーション向けの認証フローです。

メリット:

  • セキュリティが高い(クライアントシークレットをサーバー側で管理)
  • リフレッシュトークンを使用して長期的なアクセスが可能

デメリット:

  • 実装がやや複雑
  • サーバーサイドの処理が必要

認証の流れ(シーケンス図)

sequenceDiagram
    participant User
    participant Client
    participant Server
    participant Google

    User->>Client: ログイン要求
    Client->>Server: ログインページ要求
    Server->>Google: 認可リクエスト
    Google-->>User: ログイン画面表示
    User->>Google: ログイン情報入力
    Google-->>Server: 認可コード
    Server->>Google: トークンリクエスト(認可コード、クライアントID、クライアントシークレット)
    Google-->>Server: アクセストークン、IDトークン
    Server->>Google: ユーザー情報リクエスト(アクセストークン)
    Google-->>Server: ユーザー情報
    Server->>Server: セッション作成
    Server-->>Client: リダイレクト(ログイン成功)
    Client-->>User: ログイン成功表示
Loading

実装例(FastAPI & vue3)

サーバーサイド(FastAPI):

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
import httpx

app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="YOUR_SECRET_KEY")

# Google OAuth設定
GOOGLE_CLIENT_ID = "YOUR_GOOGLE_CLIENT_ID"
GOOGLE_CLIENT_SECRET = "YOUR_GOOGLE_CLIENT_SECRET"
GOOGLE_REDIRECT_URI = "http://localhost:8000/auth/callback"

@app.get("/auth/login")
async def login():
    # 認可リクエストのパラメータを設定
    params = {
        "client_id": GOOGLE_CLIENT_ID,
        "redirect_uri": GOOGLE_REDIRECT_URI,
        "scope": "openid email profile",
        "response_type": "code"
    }
    # Google認証URLにリダイレクト
    return RedirectResponse(f"https://accounts.google.com/o/oauth2/auth?{'&'.join(f'{k}={v}' for k, v in params.items())}")

@app.get("/auth/callback")
async def auth_callback(code: str, request: Request):
    # 認可コードをアクセストークンに交換
    token_params = {
        "client_id": GOOGLE_CLIENT_ID,
        "client_secret": GOOGLE_CLIENT_SECRET,
        "code": code,
        "grant_type": "authorization_code",
        "redirect_uri": GOOGLE_REDIRECT_URI
    }
    async with httpx.AsyncClient() as client:
        token_response = await client.post("https://oauth2.googleapis.com/token", data=token_params)
        token_data = token_response.json()

    # アクセストークンを使用してユーザー情報を取得
    access_token = token_data["access_token"]
    async with httpx.AsyncClient() as client:
        userinfo_response = await client.get(
            "https://www.googleapis.com/oauth2/v2/userinfo",
            headers={"Authorization": f"Bearer {access_token}"}
        )
        userinfo = userinfo_response.json()

    # セッションにユーザー情報を保存
    request.session["user"] = userinfo

    return RedirectResponse(url="/")

@app.get("/auth/user")
async def get_user(request: Request):
    user = request.session.get("user")
    if not user:
        raise HTTPException(status_code=401, detail="Not authenticated")
    return user

クライアントサイド(Vue 3):

<template>
  <div>
    <h1>Google OAuth Example</h1>
    <div v-if="user">
      <p>Welcome, {{ user.name }}!</p>
      <button @click="logout">Logout</button>
    </div>
    <div v-else>
      <button @click="login">Login with Google</button>
    </div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const user = ref(null)

    const login = () => {
      window.location.href = 'http://localhost:8000/auth/login'
    }

    const logout = async () => {
      await fetch('http://localhost:8000/auth/logout', { credentials: 'include' })
      user.value = null
    }

    const fetchUser = async () => {
      try {
        const response = await fetch('http://localhost:8000/auth/user', { credentials: 'include' })
        if (response.ok) {
          user.value = await response.json()
        }
      } catch (error) {
        console.error('Failed to fetch user:', error)
      }
    }

    onMounted(fetchUser)

    return {
      user,
      login,
      logout
    }
  }
}
</script>

実装例(FastAPI + StarletteOAuth)

StarletteOAuthを使うと、実装が簡素化されます。

from fastapi import FastAPI, Depends, HTTPException
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from starlette_oauth2.client import OAuth2Client
from starlette_oauth2.middleware import OAuth2Middleware
from starlette.requests import Request

app = FastAPI()

app.add_middleware(SessionMiddleware, secret_key="YOUR_SECRET_KEY")

google_oauth = OAuth2Client(
    client_id="YOUR_GOOGLE_CLIENT_ID",
    client_secret="YOUR_GOOGLE_CLIENT_SECRET",
    authorize_endpoint="https://accounts.google.com/o/oauth2/auth",
    token_endpoint="https://oauth2.googleapis.com/token",
    userinfo_endpoint="https://www.googleapis.com/oauth2/v2/userinfo",
    scope=["openid", "email", "profile"],
)

app.add_middleware(
    OAuth2Middleware,
    clients={
        "google": google_oauth
    },
    redirect_uri="http://localhost:8000/auth/callback"
)

@app.get("/login")
async def login(request: Request):
    return await google_oauth.authorize(request, "http://localhost:8000/auth/callback")

@app.get("/auth/callback")
async def auth_callback(request: Request):
    token = await google_oauth.authorize_access_token(request)
    user = await google_oauth.parse_id_token(request, token)
    request.session["user"] = user
    return RedirectResponse(url="/")

@app.get("/user")
async def get_user(request: Request):
    user = request.session.get("user")
    if not user:
        raise HTTPException(status_code=401, detail="Not authenticated")
    return user

@app.get("/logout")
async def logout(request: Request):
    request.session.pop("user", None)
    return {"message": "Logged out successfully"}

ID トークンフロー

IDトークンフロー(Implicit Flow)は、主にシングルページアプリケーション(SPA)やモバイルアプリ向けの認証フローです。

メリット:

  • 実装が比較的簡単
  • サーバーサイドの処理が不要

デメリット:

  • セキュリティリスクがやや高い(トークンがクライアントサイドで扱われる)
  • リフレッシュトークンが使用できない

認証の流れ(シーケンス図)

sequenceDiagram
    participant User
    participant Client
    participant Google
    participant Server

    User->>Client: ログイン要求
    Client->>Google: Google Sign-In開始
    Google-->>User: ログイン画面表示
    User->>Google: ログイン情報入力
    Google-->>Client: ID Token返却
    Client->>Server: ID Tokenを送信
    Server->>Google: ID Tokenの検証
    Google-->>Server: 検証結果
    alt 検証成功
        Server-->>Client: 認証成功レスポンス
        Client-->>User: ログイン成功表示
    else 検証失敗
        Server-->>Client: 認証失敗レスポンス
        Client-->>User: エラー表示
    end
Loading

実装例(FastAPI & vue3)

サーバーサイド(FastAPI):

from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2AuthorizationCodeBearer
from jose import jwt
import httpx

app = FastAPI()

GOOGLE_CLIENT_ID = "YOUR_GOOGLE_CLIENT_ID"
GOOGLE_DISCOVERY_URL = "https://accounts.google.com/.well-known/openid-configuration"

oauth2_scheme = OAuth2AuthorizationCodeBearer(
    authorizationUrl="https://accounts.google.com/o/oauth2/auth",
    tokenUrl="https://oauth2.googleapis.com/token",
)

async def get_google_public_key(kid: str):
    async with httpx.AsyncClient() as client:
        resp = await client.get(GOOGLE_DISCOVERY_URL)
        data = resp.json()
        jwks_uri = data["jwks_uri"]
        resp = await client.get(jwks_uri)
        jwks = resp.json()
    
    for key in jwks["keys"]:
        if key["kid"] == kid:
            return key
    
    raise ValueError(f"Public key not found for kid: {kid}")

async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        header = jwt.get_unverified_header(token)
        kid = header["kid"]
        key = await get_google_public_key(kid)
        
        payload = jwt.decode(
            token,
            key,
            algorithms=["RS256"],
            audience=GOOGLE_CLIENT_ID,
        )
        
        return payload
    except Exception as e:
        raise HTTPException(
            status_code=401,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )

@app.get("/protected")
async def protected_route(current_user: dict = Depends(get_current_user)):
    return {"message": "This is a protected route", "user": current_user}

クライアントサイド(Vue 3):

<template>
  <div>
    <h1>Google Sign-In Example</h1>
    <g-signin-button
      :params="googleSignInParams"
      @success="onSignInSuccess"
      @error="onSignInError">
      Sign in with Google
    </g-signin-button>
    <div v-if="isSignedIn">
      <p>Welcome, {{ user.name }}!</p>
      <button @click="signOut">Sign Out</button>
    </div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
  name: 'App',
  setup() {
    const isSignedIn = ref(false)
    const user = ref(null)
    const googleSignInParams = {
      client_id: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
    }

    onMounted(() => {
      const script = document.createElement('script')
      script.src = 'https://apis.google.com/js/platform.js'
      script.async = true
      script.defer = true
      document.head.appendChild(script)

      script.onload = () => {
        window.gapi.load('auth2', () => {
          window.gapi.auth2.init(googleSignInParams)
        })
      }
    })

    const onSignInSuccess = (googleUser) => {
      const profile = googleUser.getBasicProfile()
      user.value = {
        name: profile.getName(),
        email: profile.getEmail(),
        imageUrl: profile.getImageUrl()
      }
      isSignedIn.value = true

      const id_token = googleUser.getAuthResponse().id_token
      sendTokenToBackend(id_token)
    }

    const onSignInError = (error) => {
      console.error('Error during sign in', error)
    }

    const signOut = () => {
      const auth2 = window.gapi.auth2.getAuthInstance()
      auth2.signOut().then(() => {
        isSignedIn.value = false
        user.value = null
      })
    }

    const sendTokenToBackend = async (token) => {
      try {
        const response = await fetch('http://your-backend-api/auth', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
          }
        })
        const data = await response.json()
        console.log('Backend response:', data)
      } catch (error) {
        console.error('Error sending token to backend:', error)
      }
    }

    return {
      isSignedIn,
      user,
      googleSignInParams,
      onSignInSuccess,
      onSignInError,
      signOut
    }
  }
}
</script>

この実装例では、クライアントサイドでGoogle Sign-Inを行い、取得したIDトークンをサーバーに送信して検証しています。サーバーサイドではIDトークンを検証し、保護されたルートへのアクセスを制御しています。

⚠️ **GitHub.com Fallback** ⚠️