Google OpenID Connect (まとめ) - gosaaan1/hokulea-garage GitHub Wiki
Google OpenID Connectは、GoogleがOAuth 2.0プロトコルの上に構築した認証レイヤーです。これにより、クライアントアプリケーションはGoogleアカウントを使用してユーザーを安全に認証し、ユーザーの基本的なプロフィール情報を取得することができます。
OAuth 2.0には複数の認証フローがありますが、ここでは主に「認可コードフロー」と「IDトークンフロー」について説明します。
OAuth 2.0の主な認証フローの種類を以下に列挙します:
-
認可コードフロー(Authorization Code Flow)
- 標準的なサーバーサイドWebアプリケーション向け
-
暗黙的フロー(Implicit Flow)
- シングルページアプリケーション(SPA)向け
- 現在は非推奨
-
リソースオーナー・パスワード・クレデンシャルズ・フロー(Resource Owner Password Credentials Flow)
- ユーザー名とパスワードを直接使用する信頼されたアプリケーション向け
-
クライアント・クレデンシャルズ・フロー(Client Credentials Flow)
- マシン間通信やバックグラウンドプロセス向け
-
デバイス認可フロー(Device Authorization Flow)
- 入力制限のあるデバイス(スマートTV、IoTデバイスなど)向け
-
リフレッシュトークンフロー(Refresh Token Flow)
- アクセストークンの更新に使用
-
PKCE拡張を使用した認可コードフロー(Authorization Code Flow with Proof Key for Code Exchange)
- モバイルアプリやSPA向けのセキュアな認可コードフロー
-
ハイブリッドフロー(Hybrid Flow)
- 認可コードフローと暗黙的フローの特徴を組み合わせたフロー
-
バックチャネル認証フロー(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: ログイン成功表示
サーバーサイド(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>
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トークンフロー(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
サーバーサイド(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トークンを検証し、保護されたルートへのアクセスを制御しています。