🐈

【Keycloakによる認証BFF構築】3. Callback関数の実装

に公開

この記事について

この記事はKeycloakによる認証BFF構築に関する記事です。これまでの記事をまだお読みでない方はそちらを先にお読みになることをおすすめします。

今回の実装内容

今回は、バックエンドのcallback関数を実装していきます。

前回の記事でlogin関数を実装しました。

BFFの認証構成では、下記の流れでtoken(access_token, refresh_token)を保持します。

  • kycloakで認証情報(username, password)を認証し、Authorization codeを発行します。
  • バックエンド側ではこのAuthorization codeを用いてkeycloakからtokenを取り出します。
  • 取り出したtokenをサーバー再度のsession_storeに保存します。
  • ブラウザには、session_storeのキーであるsession_idのみをキャッシュに保存します。

この構成にすることで、第三者にブラウザからtokenを取得されるリスクを大幅に低減できます。

session_storeの作成

Keycloakから取り出すtokenの保存先として、session_storeを作成します。
本番環境ではRedisを使うことがデファクトスタンダードとなっていますが、
今回は実装を簡略化するためにインメモリのkey-valueストア(pythonディクショナリ)を使用します。

backend/app/session_store.py

session_store = {} 

Callback関数の実装

Callback関数は、Keycloakで認証時にリダイレクトされるバックエンドの関数です。

前述の通り、この関数内で下記の処理を行います。

  • Keycloakからtokenを取得する
  • session_storeにtokenを保存する
  • ブラウザCookieにsession_idを保存する

実装は下記の通りです。

backend/app/main.py

# 追記
import time
from fastapi import FastAPI, Request, HTTPException
from .session_store import session_store

...(中略)...

@app.get("/api/callback")  
async def callback( request: Request, code: str, state: str) -> Response:  
    # stateの検証  
    saved_state = request.session.get("state")  
  
    if not saved_state or saved_state != state:  
        raise HTTPException(status_code=400, detail="Invalid state")  
  
    # 認可コードでtokenを取得  
    tokens = keycloak_openid.token(  
        code=code,  
        grant_type="authorization_code",  
        redirect_uri=os.getenv("CALLBACK_URL"),  
    )  
	
    session_id = str(uuid.uuid4())  
	tokens["expires_at"] = time.time() + tokens["expires_in"]
    session_store[session_id] = tokens  
  
    # レスポンス先とcookieの設定  
    redirect_response = RedirectResponse(url=os.getenv("FRONTEND_URL"))  
    redirect_response.set_cookie("session_id", session_id, httponly=True, samesite="lax", secure=False)  
  
    return redirect_response

tokenの取得の処理

  • tokenを取得する際には、redirect_uriの設定が必要です。
  • これは、authorization_codeの検証時にどのredirect_uriに対して発行されたのかを検証するため、必須です。
  • この仕様は、Autorization codeが盗まれた場合に、不適切なサーバーに対してtokenを発行しないための防御策です。

tokenの内容

Keycloakから取得したtokensは下記の形式です。

{
	'access_token': 'eyJhb...MJV7Q', 
	'expires_in': 300, 
	'refresh_expires_in': 1800, 
	'refresh_token': 'eyJhb...OJHQg', 
	'token_type': 'Bearer', 
	'id_token': 'eyJhb...DNDpw', 
	'not-before-policy': 0, 
	'session_state': '5c16ef7c-14c4-45e0-a43a-afcfba9d61f2', 
	'scope': 'openid profile email'
}

つまり、session_storeには下記の形式でtokensが保存されます。

session_store = { '77eb20d8-xxxx-xxxx-xxxx-d84cbd7e796f': 
	{
		'access_token': 'eyJhb...MJV7Q', 
		'expires_in': 300, 
		'refresh_expires_in': 1800, 
		'refresh_token': 'eyJhb...OJHQg', 
		'token_type': 'Bearer', 
		'id_token': 'eyJhb...DNDpw', 
		'not-before-policy': 0, 
		'session_state': '5c16ef7c-14c4-45e0-a43a-afcfba9d61f2', 
		'scope': 'openid profile email',
		'expires_at': 1766893915.5203707
	}
 }

Response生成

  • callback関数の戻り値の型はRedirectResponseですが、RedirectResponseのオブジェクトを生成して、cookieをセットしています。
  • こうすることで、session_idをブラウザのcookieに保存することができます。

Keycloak Clientの設定

fastapi-clientのValid redirect URIsにhttp://localhost:8000/api/callbackを追記してください。(前回記事で追記済みですので、未設定の方は前回記事を参照ください。)

次に、ログイン用のテストユーザーを作成します。

  • Clients -> Users -> Add User -> 任意のユーザー名でユーザーを作成 -> credentials -> set passwordからパスワードを発行します。

今回使用しているKeycloak25.0では、first name, last nameは必須ではないにも関わらず、入力しないと初回ログイン時に入力を求められるので、入力する必要があるのでユーザー作成時に入力しておくことをお勧めします。

動作確認

ここまでできたら、http://localhost:8000/api/loginにアクセスして、認証情報を入力します。

  • username, passwordを入力すると、localhost:5173にリダイレクトされます。
  • session_idがCookieに保存されていることを確認します。

今回の記事はここまでです。
次回はKeycloakから取得したtokenの検証を行います。

Discussion