Google OpenID Connect - gosaaan1/hokulea-garage GitHub Wiki
- サーバーサイドアプリケーション向け
- 認可コードを使用して、サーバーサイドでアクセストークンとID トークンを取得
- より安全だが、実装が複雑
もちろんです。サーバーフロー(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: ログイン成功表示
このシーケンス図は以下のステップを示しています:
-
ユーザーがクライアントアプリケーションでログインを要求します。
-
クライアントがサーバーにログインページを要求します。
-
サーバーがGoogleの認可エンドポイントにリダイレクトします(認可リクエスト)。
-
Googleがユーザーにログイン画面を表示します。
-
ユーザーがGoogleアカウントでログインし、アプリケーションへのアクセスを許可します。
-
Googleがサーバーに認可コードを送信します(通常はリダイレクトURIを介して)。
-
サーバーが認可コード、クライアントID、クライアントシークレットをGoogleのトークンエンドポイントに送信し、アクセストークンとIDトークンを要求します。
-
GoogleがサーバーにアクセストークンとIDトークンを返します。
-
サーバーがアクセストークンを使用してGoogleのユーザー情報エンドポイントにリクエストを送信します。
-
Googleがサーバーにユーザー情報を返します。
-
サーバーがユーザーセッションを作成し、ユーザー情報を保存します。
-
サーバーがクライアントにリダイレクトレスポンスを送信し、ログイン成功を通知します。
-
クライアントがユーザーにログイン成功を表示します。
この流れにより、クライアントは直接認証情報を扱わず、すべての機密情報(クライアントシークレット、トークンなど)はサーバー側で管理されます。これにより、セキュリティが向上し、クライアントサイドでの脆弱性のリスクが軽減されます。
もちろんです。「サーバーフロー」(Authorization Code Flow)の実装方法について説明します。この方法は、クライアント側とサーバー側の両方で実装が必要です。
以下に、FastAPIを使用したサーバー側の実装と、Vue.js 3を使用したクライアント側の実装例を示します。
- サーバー側(FastAPI)の実装:
まず、必要なライブラリをインストールします:
pip install fastapi uvicorn httpx itsdangerous
次に、FastAPIアプリケーションを作成します:
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from itsdangerous import URLSafeSerializer
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"
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"
# URLSafeSerializerの初期化
serializer = URLSafeSerializer("your-secret-key")
@app.get("/auth/login")
async def login(request: Request):
# 認可リクエストのパラメータを設定
params = {
"client_id": GOOGLE_CLIENT_ID,
"redirect_uri": GOOGLE_REDIRECT_URI,
"scope": "openid email profile",
"response_type": "code",
"state": serializer.dumps({"redirect_url": request.query_params.get("redirect_url", "/")})
}
# Google認証URLにリダイレクト
return RedirectResponse(f"{GOOGLE_AUTH_URL}?{'&'.join(f'{k}={v}' for k, v in params.items())}")
@app.get("/auth/callback")
async def auth_callback(code: str, state: str, request: Request):
# stateパラメータをデコード
state_data = serializer.loads(state)
redirect_url = state_data.get("redirect_url", "/")
# 認可コードをアクセストークンに交換
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(GOOGLE_TOKEN_URL, data=token_params)
token_data = token_response.json()
if "error" in token_data:
raise HTTPException(status_code=400, detail=token_data["error"])
# アクセストークンを使用してユーザー情報を取得
access_token = token_data["access_token"]
async with httpx.AsyncClient() as client:
userinfo_response = await client.get(
GOOGLE_USERINFO_URL,
headers={"Authorization": f"Bearer {access_token}"}
)
userinfo = userinfo_response.json()
# セッションにユーザー情報を保存
request.session["user"] = userinfo
# クライアントにリダイレクト
return RedirectResponse(url=redirect_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
@app.get("/auth/logout")
async def logout(request: Request):
request.session.pop("user", None)
return {"message": "Logged out successfully"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
- クライアント側(Vue.js 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>
この実装では以下のことを行っています:
-
サーバー側:
-
/auth/login
エンドポイントで Google 認証 URL を生成し、ユーザーをリダイレクトします。 -
/auth/callback
エンドポイントで認可コードを受け取り、アクセストークンに交換します。 - アクセストークンを使用してユーザー情報を取得し、セッションに保存します。
-
/auth/user
エンドポイントでログインしているユーザーの情報を返します。 -
/auth/logout
エンドポイントでユーザーをログアウトします。
-
-
クライアント側:
- ログインボタンをクリックすると、サーバーの
/auth/login
エンドポイントにリダイレクトします。 - コンポーネントがマウントされたときに、サーバーから現在のユーザー情報を取得します。
- ログアウトボタンをクリックすると、サーバーの
/auth/logout
エンドポイントを呼び出します。
- ログインボタンをクリックすると、サーバーの
この方法では、クライアントは直接 Google と通信せず、すべての OAuth 処理をサーバー側で行います。これにより、クライアントIDやシークレットなどの機密情報をクライアント側に露出させることなく、よりセキュアな実装が可能になります。
注意点:
- 実際の実装では、適切なエラーハンドリングとセキュリティ対策(HTTPS の使用、CSRF 対策など)を行ってください。
-
YOUR_SECRET_KEY
、YOUR_GOOGLE_CLIENT_ID
、YOUR_GOOGLE_CLIENT_SECRET
は適切な値に置き換えてください。 - クロスオリジンリクエストを処理するために、サーバー側で CORS の設定が必要になる場合があります。
Starlette OAuth パッケージを使用すると、サーバー側の実装がより簡潔になります。以下に、Starlette OAuth を使用した FastAPI での実装例を示します。
まず、必要なパッケージをインストールします:
pip install fastapi uvicorn starlette-oauth2 httpx
次に、FastAPI アプリケーションを作成します:
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 OAuth2 クライアントの設定
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"],
)
# OAuth2ミドルウェアの追加
app.add_middleware(
OAuth2Middleware,
clients={
"google": google_oauth
},
redirect_uri="http://localhost:8000/auth/callback"
)
@app.get("/")
async def root():
return {"message": "Hello World"}
@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"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
この実装では、以下のことを行っています:
-
OAuth2Client
を使用して Google OAuth2 クライアントを設定します。 -
OAuth2Middleware
を使用して OAuth2 認証フローを処理します。 -
/login
エンドポイントで Google 認証を開始します。 -
/auth/callback
エンドポイントで認証コールバックを処理し、ユーザー情報をセッションに保存します。 -
/user
エンドポイントで現在のユーザー情報を返します。 -
/logout
エンドポイントでユーザーをログアウトします。
Starlette OAuth を使用することで、以下の利点があります:
- コードがより簡潔になります。
- OAuth2 フローの複雑な部分が抽象化されます。
- トークンの取得やユーザー情報の解析が自動的に行われます。
- 複数の OAuth プロバイダーを簡単に追加できます。
注意点:
-
YOUR_SECRET_KEY
、YOUR_GOOGLE_CLIENT_ID
、YOUR_GOOGLE_CLIENT_SECRET
は適切な値に置き換えてください。 - 実際の実装では、適切なエラーハンドリングとセキュリティ対策(HTTPS の使用、CSRF 対策など)を行ってください。
- クロスオリジンリクエストを処理するために、CORS の設定が必要になる場合があります。
クライアント側の実装は、前回の例とほぼ同じですが、ログインURLを /login
に変更する必要があります。
この方法を使用すると、OAuth2 認証フローの実装がより簡単になり、コードの保守性も向上します。
- シングルページアプリケーション(SPA)向け
- アクセストークンをクライアントに直接返す
- ID トークンは通常含まれない
- セキュリティ上の懸念があるため、現在は推奨されていない
- サーバーフローとクライアントサイドフローの特徴を組み合わせたもの
- ID トークンフローはその一種で、クライアントがID トークンを直接受け取る
Google Open IDを使用したクライアントとサーバー間の認証プロセスのシーケンス図を以下に示します。
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
このシーケンス図は以下のステップを示しています:
-
ユーザーがクライアントアプリケーションでログインを要求します。
-
クライアントがGoogle Sign-Inプロセスを開始します。
-
Googleがユーザーにログイン画面を表示します。
-
ユーザーがGoogleアカウントでログインします。
-
認証が成功すると、GoogleがクライアントにID Tokenを返します。
-
クライアントは受け取ったID TokenをサーバーにHTTPリクエストで送信します。
-
サーバーは受け取ったID TokenをGoogleの公開エンドポイントで検証します。
-
Googleはサーバーに検証結果を返します。
-
検証が成功した場合:
- サーバーはクライアントに認証成功のレスポンスを送ります。
- クライアントはユーザーにログイン成功を表示します。
-
検証が失敗した場合:
- サーバーはクライアントに認証失敗のレスポンスを送ります。
- クライアントはユーザーにエラーを表示します。
この流れにより、サーバーはGoogleを信頼できる第三者として使用し、ユーザーの身元を確認します。ID Tokenの検証により、トークンが改ざんされていないこと、有効期限内であること、適切な発行者(Google)からのものであることを確認できます。
Vue 3でGoogle Sign-Inを実装する方法を説明します。以下は、Vue 3とGoogle Sign-In APIを使用した実装例です。
- まず、プロジェクトに必要なパッケージをインストールします:
npm install vue-google-signin-button
-
main.js
ファイルで、Google Sign-In APIをロードします:
import { createApp } from 'vue'
import App from './App.vue'
import GSignInButton from 'vue-google-signin-button'
const app = createApp(App)
app.use(GSignInButton)
app.mount('#app')
-
App.vue
または適切なコンポーネントファイルで、Google Sign-Inボタンを実装します:
<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(() => {
// Google Sign-In APIのスクリプトを動的にロード
const script = document.createElement('script')
script.src = 'https://apis.google.com/js/platform.js'
script.async = true
script.defer = true
document.head.appendChild(script)
// APIがロードされたら初期化
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
// IDトークンを取得してバックエンドに送信
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 APIをロードし初期化します。
- Google Sign-Inボタンを表示します。
- ユーザーがサインインに成功したら、プロフィール情報を取得し表示します。
- IDトークンを取得し、バックエンドAPIに送信します。
- サインアウト機能を提供します。
注意点:
-
YOUR_GOOGLE_CLIENT_ID
を、Google Cloud ConsoleでOAuth 2.0クライアントIDを作成した際に取得したクライアントIDに置き換えてください。 - バックエンドAPIのURLを適切なものに変更してください。
- 実際のアプリケーションでは、エラーハンドリングやユーザー体験の向上のための追加の処理が必要になる場合があります。
- セキュリティを強化するために、適切なCORS設定やCSRF対策を行うことを忘れないでください。
この実装により、ユーザーはGoogle Sign-Inを使用してアプリケーションに認証でき、バックエンドAPIにIDトークンを送信して認証を完了することができます。