FastAPIでGoogle OAuthを実装する方法

## 🔹 概要
FastAPIを使用して，Google OAuth2.0を利用した認証方法を実装する．OAuth2.0を活用することによって，Googleアカウントを使ったログイン機能が可能となる．

## 🔹 主な手順
① Google CloudでクライアントIDを取得
↓
② FastAPIのセットアップ
↓
③ OAuth2の設定
↓
④ エンドポイントの実装

## ①Google CloudでクライアントIDを取得
1. [Google Cloud Console](https://console.cloud.google.com/welcome?inv=1&invt=Abqf9A&organizationId=0) にアクセスする  
2. 画面上部の **「プロジェクトの選択」** をクリックする  
3. 新しくプロジェクトを作成する  
4. 左側のメニューから **「APIとサービス」 → 「OAuth同意画面」** を選択する  
   - **「Google Auth Platform はまだ構成されていません」** という表示に従い、プロジェクト構成を作成する  
   ![OAuth同意画面の設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3906700/05b8b49c-9bb0-453a-ad65-d12662c229ad.png)

5. 左側のメニューから **「APIとサービス」 → 「認証情報」** を選択する  
   - 画面上部の **「認証情報を作成」 → 「OAuth クライアント ID の作成」** を選択する  
   - **「アプリケーションの種類」** は **「ウェブアプリケーション」** を選択する  
   - **「承認済みのリダイレクト URI」** に Google 認証後のリダイレクト先の URL を入力し、作成する  
   - 作成後、**クライアントID** と **クライアントシークレット** が表示されるため、メモしておく  
   ![クライアントIDとシークレット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3906700/8243b5a0-651f-4461-8719-04dbd72c67ce.png)

6. 左側のメニューから **「APIとサービス」 → 「OAuth同意画面」 → 「対象」** を選択し、テストするユーザーを追加する  

## ②FastAPIのセットアップ
### 必要なパッケージのインストール
以下のコマンドを実行して，FastAPIとOAuth2認証に必要なパッケージをインストールする．
```
pip install fastapi google-auth Jinja2 httpx python-dotenv uvicorn
```
`fastapi`: Webフレームワーク
`google-auth`: Google 認証および OAuth2.0 に関連する認証処理を提供
`Jinja2`: テンプレートエンジン（HTMLのレンダリングに使用）
`httpx`: HTTPクライアント
`python-dotenv`: 環境変数の管理
`uvicorn`: 非同期ASGIサーバー

### `.env`ファイルの作成
プロジェクトのルートディレクトリに.envファイルを作成し，以下のようにGoogle OAuthのクライアント情報を設定する．
```python:.env
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=your-google-redirect-url
```
`your-google-client-id`: OAuth クライアント ID の作成時に得たクライアントID
`your-google-client-secret`: OAuth クライアント ID の作成時に得たクライアントシークレット
`your-google-redirect-url`:OAuth クライアント ID の作成時に入力した承認済みのリダイレクト URI

## ③OAuth2の設定
### Google OAuth2.0の認証情報を取得する
Google OAuth 2.0 認証に必要な設定を環境変数から取得し、定数として定義する．`os.getenv`関数を使用して，環境変数 `GOOGLE_CLIENT_ID`．`GOOGLE_CLIENT_SECRET`，および `GOOGLE_REDIRECT_URI` の値を先ほど作成した`.env`から取得する．

また，`GOOGLE_AUTH_URL`，`GOOGLE_TOKEN_URL`，および `GOOGLE_USER_INFO_URL` は，Googleの認証，トークン取得，およびユーザ情報取得のためのエンドポイントURLを定義している．これにより、Google OAuth 2.0 認証フローを実装する際に必要な情報が一箇所にまとめられ，コードの可読性と保守性が向上する．
```python:auth.py
import os
import httpx
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import RedirectResponse
from google.oauth2 import id_token
from google.auth.transport import requests
from fastapi.security import OAuth2AuthorizationCodeBearer
from twilio.rest import Client
from dotenv import load_dotenv

load_dotenv()

router = APIRouter(prefix="/auth", tags=["authentication"])

# Google OAuth2.0の設定
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
GOOGLE_REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_USER_INFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo"
```
### ユーザーの認証とトークンの取得
`OAuth2AuthorizationCodeBearer`クラスのインスタンスを作成し，`oauth2_scheme`に代入している．このクラスは，OAuth2の認証コードフローを使用してベアラートークンを取得するための認証スキームを提供する．リクエストヘッダに含まれるベアラートークンを検証し，認証が成功した場合にトークンを返す．これにより，APIエンドポイントへのアクセス制御が可能になるのだ．具体的には，Google OAuth2.0の認証エンドポイントとトークンエンドポイントを使用することで，ユーザの認証とトークンの取得を行う．

`authorizationUrl` と `tokenUrl` の引数には，それぞれ `GOOGLE_AUTH_URL` と GOOGLE_TOKEN_URL が渡されている．これらのURLは，GoogleのOAuth2.0認証フローに必要なエンドポイントを指しており，`GOOGLE_AUTH_URL` はユーザが認証を行うためのURL，`GOOGLE_TOKEN_URL` は認証コードをトークンに交換するためのURLである．
```python:auth.py
oauth2_scheme = OAuth2AuthorizationCodeBearer(
    authorizationUrl=GOOGLE_AUTH_URL,
    tokenUrl=GOOGLE_TOKEN_URL,
)
```
## ④エンドポイントの実装
### Google OAuth 2.0認証のためのログインエンドポイントを定義
このエンドポイントにアクセスすると，`login`関数が非同期に実行される．`login`関数ではGoogleの認証URLを構築している．そして，`RedirectResponse`を使用して，ユーザをGoogleの認証ページにリダイレクトする．
`RedirectResponse`は，指定されたURLにリダイレクトするHTTPレスポンスを生成する．これにより，ユーザはGoogleの認証ページに移動し，認証を行うことが可能となる．認証が成功すると，指定されたリダイレクトURIに認証コードが返される．
```python:auth.py
@router.get("/login")
async def login():
    google_auth_url = f"{GOOGLE_AUTH_URL}?client_id={GOOGLE_CLIENT_ID}&redirect_uri={GOOGLE_REDIRECT_URI}&response_type=code&scope=openid%20email&access_type=offline&prompt=consent"
    return RedirectResponse(google_auth_url)
```

### Google OAuth 2.0 認証フローのコールバックエンドポイントを定義
ユーザがGoogleの認証ページで認証を行った後，Google認証コードをこのエンドポイントにリダイレクトする．このエンドポイントは，認証コードを受け取り，アクセストークンとIDトークンを取得するために使用される．

このエンドポイントにアクセスすると，`callback`関数が非同期に実行される．この関数の引数は，認証コードとリクエストオブジェクトである．認証コードはGoogleからリダイレクトされた際にクエリパラメータとして渡される．

次に，認証コードを使用してGoogleのトークンエンドポイントにPOSTリクエストを送信する．リクエストのデータには，認証コード，クライアントID，クライアントシークレット，リダイレクトURI，およびグラントタイプ（アクセス権の付与の方法）が含まれている．そして，`httpx.AsyncClient`を使用して非同期にリクエストを送信し，レスポンスを受け取る．レスポンスが成功したかどうかを確認し，成功した場合はレスポンスのJSONデータを取得する．

取得したトークンレスポンスからIDトークンを抽出し，IDトークンが存在しない場合はHTTP 400エラーを発生させる．IDトークンが存在する場合は，`id_token.verify_oauth2_token`関数を使用してトークンを検証し，ユーザ情報を取得する．ユーザーの名前をセッションに保存し，/v1/helloworldエンドポイントにリダイレクトする．

```python:auth.py
@router.get("/callback")
async def callback(code: str, request: Request):
    data = {
        "code": code,
        "client_id": GOOGLE_CLIENT_ID,
        "client_secret": GOOGLE_CLIENT_SECRET,
        "redirect_uri": GOOGLE_REDIRECT_URI,
        "grant_type": "authorization_code",
    }
    async with httpx.AsyncClient() as client:
        response = await client.post(GOOGLE_TOKEN_URL, data=data)
        response.raise_for_status()
        token_response = response.json()
    
    id_token_value = token_response.get("id_token")
    if id_token_value is None:
        raise HTTPException(status_code=400, detail="id_tokenが取得できませんでした")
    
    try:
        id_info = id_token.verify_oauth2_token(id_token_value, requests.Request(), GOOGLE_CLIENT_ID)
        
        name = id_info.get("name")
        request.session["user_name"] = name
        
        return RedirectResponse(url="/v1/helloworld")
    
    except ValueError as e:
        raise HTTPException(status_code=400, detail=f"id_tokenの検証に失敗しました: {str(e)}") 
    
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"エラーが発生しました: {str(e)}")
```

## 🔹 まとめ

**・Google CloudでOAuth2.0のクライアントIDとシークレットを取得**

**・FastAPIをセットアップし，OAuth2の認証エンドポイントを作成**

**・Google認証のリダイレクトを処理し，ユーザ情報を取得**

これにより，Googleアカウントでログインできるようになる．

## 🔹 参考文献
以下の文献を参考に実装を行いました．

https://parlak-deniss.medium.com/fastapi-authentication-with-google-oauth-2-0-9bb93b784eee

https://apidog.com/jp/blog/what-is-bearer-token/

https://qiita.com/KWS_0901/items/f5282d1056f8582d31ad