Google OpenID Connect - gosaaan1/hokulea-garage GitHub Wiki

サーバーフロー(Authorization Code Flow)

  • サーバーサイドアプリケーション向け
  • 認可コードを使用して、サーバーサイドでアクセストークンと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: ログイン成功表示
Loading

このシーケンス図は以下のステップを示しています:

  1. ユーザーがクライアントアプリケーションでログインを要求します。

  2. クライアントがサーバーにログインページを要求します。

  3. サーバーがGoogleの認可エンドポイントにリダイレクトします(認可リクエスト)。

  4. Googleがユーザーにログイン画面を表示します。

  5. ユーザーがGoogleアカウントでログインし、アプリケーションへのアクセスを許可します。

  6. Googleがサーバーに認可コードを送信します(通常はリダイレクトURIを介して)。

  7. サーバーが認可コード、クライアントID、クライアントシークレットをGoogleのトークンエンドポイントに送信し、アクセストークンとIDトークンを要求します。

  8. GoogleがサーバーにアクセストークンとIDトークンを返します。

  9. サーバーがアクセストークンを使用してGoogleのユーザー情報エンドポイントにリクエストを送信します。

  10. Googleがサーバーにユーザー情報を返します。

  11. サーバーがユーザーセッションを作成し、ユーザー情報を保存します。

  12. サーバーがクライアントにリダイレクトレスポンスを送信し、ログイン成功を通知します。

  13. クライアントがユーザーにログイン成功を表示します。

この流れにより、クライアントは直接認証情報を扱わず、すべての機密情報(クライアントシークレット、トークンなど)はサーバー側で管理されます。これにより、セキュリティが向上し、クライアントサイドでの脆弱性のリスクが軽減されます。

実装例

もちろんです。「サーバーフロー」(Authorization Code Flow)の実装方法について説明します。この方法は、クライアント側とサーバー側の両方で実装が必要です。

以下に、FastAPIを使用したサーバー側の実装と、Vue.js 3を使用したクライアント側の実装例を示します。

  1. サーバー側(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)
  1. クライアント側(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>

この実装では以下のことを行っています:

  1. サーバー側:

    • /auth/login エンドポイントで Google 認証 URL を生成し、ユーザーをリダイレクトします。
    • /auth/callback エンドポイントで認可コードを受け取り、アクセストークンに交換します。
    • アクセストークンを使用してユーザー情報を取得し、セッションに保存します。
    • /auth/user エンドポイントでログインしているユーザーの情報を返します。
    • /auth/logout エンドポイントでユーザーをログアウトします。
  2. クライアント側:

    • ログインボタンをクリックすると、サーバーの /auth/login エンドポイントにリダイレクトします。
    • コンポーネントがマウントされたときに、サーバーから現在のユーザー情報を取得します。
    • ログアウトボタンをクリックすると、サーバーの /auth/logout エンドポイントを呼び出します。

この方法では、クライアントは直接 Google と通信せず、すべての OAuth 処理をサーバー側で行います。これにより、クライアントIDやシークレットなどの機密情報をクライアント側に露出させることなく、よりセキュアな実装が可能になります。

注意点:

  • 実際の実装では、適切なエラーハンドリングとセキュリティ対策(HTTPS の使用、CSRF 対策など)を行ってください。
  • YOUR_SECRET_KEYYOUR_GOOGLE_CLIENT_IDYOUR_GOOGLE_CLIENT_SECRET は適切な値に置き換えてください。
  • クロスオリジンリクエストを処理するために、サーバー側で CORS の設定が必要になる場合があります。

実装例 (Starlette OAuth)

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)

この実装では、以下のことを行っています:

  1. OAuth2Client を使用して Google OAuth2 クライアントを設定します。

  2. OAuth2Middleware を使用して OAuth2 認証フローを処理します。

  3. /login エンドポイントで Google 認証を開始します。

  4. /auth/callback エンドポイントで認証コールバックを処理し、ユーザー情報をセッションに保存します。

  5. /user エンドポイントで現在のユーザー情報を返します。

  6. /logout エンドポイントでユーザーをログアウトします。

Starlette OAuth を使用することで、以下の利点があります:

  • コードがより簡潔になります。
  • OAuth2 フローの複雑な部分が抽象化されます。
  • トークンの取得やユーザー情報の解析が自動的に行われます。
  • 複数の OAuth プロバイダーを簡単に追加できます。

注意点:

  • YOUR_SECRET_KEYYOUR_GOOGLE_CLIENT_IDYOUR_GOOGLE_CLIENT_SECRET は適切な値に置き換えてください。
  • 実際の実装では、適切なエラーハンドリングとセキュリティ対策(HTTPS の使用、CSRF 対策など)を行ってください。
  • クロスオリジンリクエストを処理するために、CORS の設定が必要になる場合があります。

クライアント側の実装は、前回の例とほぼ同じですが、ログインURLを /login に変更する必要があります。

この方法を使用すると、OAuth2 認証フローの実装がより簡単になり、コードの保守性も向上します。

クライアントサイドフロー / 暗黙的フロー(Implicit Flow)

  • シングルページアプリケーション(SPA)向け
  • アクセストークンをクライアントに直接返す
  • ID トークンは通常含まれない
  • セキュリティ上の懸念があるため、現在は推奨されていない

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
Loading

このシーケンス図は以下のステップを示しています:

  1. ユーザーがクライアントアプリケーションでログインを要求します。

  2. クライアントがGoogle Sign-Inプロセスを開始します。

  3. Googleがユーザーにログイン画面を表示します。

  4. ユーザーがGoogleアカウントでログインします。

  5. 認証が成功すると、GoogleがクライアントにID Tokenを返します。

  6. クライアントは受け取ったID TokenをサーバーにHTTPリクエストで送信します。

  7. サーバーは受け取ったID TokenをGoogleの公開エンドポイントで検証します。

  8. Googleはサーバーに検証結果を返します。

  9. 検証が成功した場合:

    • サーバーはクライアントに認証成功のレスポンスを送ります。
    • クライアントはユーザーにログイン成功を表示します。
  10. 検証が失敗した場合:

    • サーバーはクライアントに認証失敗のレスポンスを送ります。
    • クライアントはユーザーにエラーを表示します。

この流れにより、サーバーはGoogleを信頼できる第三者として使用し、ユーザーの身元を確認します。ID Tokenの検証により、トークンが改ざんされていないこと、有効期限内であること、適切な発行者(Google)からのものであることを確認できます。

実装例

Vue 3でGoogle Sign-Inを実装する方法を説明します。以下は、Vue 3とGoogle Sign-In APIを使用した実装例です。

  1. まず、プロジェクトに必要なパッケージをインストールします:
npm install vue-google-signin-button
  1. 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')
  1. 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>

このコードでは以下のことを行っています:

  1. Google Sign-In APIをロードし初期化します。
  2. Google Sign-Inボタンを表示します。
  3. ユーザーがサインインに成功したら、プロフィール情報を取得し表示します。
  4. IDトークンを取得し、バックエンドAPIに送信します。
  5. サインアウト機能を提供します。

注意点:

  • YOUR_GOOGLE_CLIENT_IDを、Google Cloud ConsoleでOAuth 2.0クライアントIDを作成した際に取得したクライアントIDに置き換えてください。
  • バックエンドAPIのURLを適切なものに変更してください。
  • 実際のアプリケーションでは、エラーハンドリングやユーザー体験の向上のための追加の処理が必要になる場合があります。
  • セキュリティを強化するために、適切なCORS設定やCSRF対策を行うことを忘れないでください。

この実装により、ユーザーはGoogle Sign-Inを使用してアプリケーションに認証でき、バックエンドAPIにIDトークンを送信して認証を完了することができます。

参考

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