Keycloakを用いたAuthorization Code Grant + PKCE 検証環境構築方法 メモ


* 学習目的でKeycloakを使ったAuthorization Code Grant + PKCE を検証するための環境を構築したためメモとして残しておく。

* **<u>[過去に作成したコード](https://qiita.com/KWS_0901/items/bdf60a725064900eaad1)</u>**を修正して構築する。

  * 変更点を主として記述する。
  * docker-composeなどの設定ファイルはそのまま利用する。

  

## main.py修正

* code_verifier・code_challenge生成処理などを追加する。

  ```python
  import ast
  import urllib.parse as parse
  import urllib.request as req
  import urllib.error as error
  import os
  import requests
  import uvicorn
  from fastapi import FastAPI
  from starlette.requests import Request
  from starlette.responses import RedirectResponse
  import base64
  import re
  import hashlib
  # 環境変数取得
  # FastAPI アプリ用
  APP_BASE_URL = os.getenv("APP_BASE_URL")
  APP_CLIENT_ID = os.getenv("CLIENT_ID")
  APP_CLIENT_SECRET = os.getenv('CLIENT_SECRET')
  APP_REDIRECT_URI = os.getenv('REDIRECT_URI')
  
  # Keycloak用
  # Authorization Endpoint
  KEYCLOAK_BASE_URL_LOCALHOST = os.getenv("KEYCLOAK_BASE_URL_LOCALHOST")
  # "Master"は対象のレルム名を指定する。
  AUTH_BASE_URL = (
      f"{KEYCLOAK_BASE_URL_LOCALHOST}auth/realms/Master"
      "/protocol/openid-connect/auth"
  )
  # Token Endpoint
  # コンテナ間通信するためコンテナ名を指定
  # "Master"は対象のレルム名を指定する。
  KEYCLOAK_BASE_URL_CONTAINER_NAME = os.getenv(
      "KEYCLOAK_BASE_URL_CONTAINER_NAME")
  TOKEN_URL = (
      f"{KEYCLOAK_BASE_URL_CONTAINER_NAME}auth/realms/master"
      "/protocol/openid-connect/token"
  )
  
  app = FastAPI()
  
  # Keycloak Authorization Endpointへのリダイレクト
  @app.get("/auth/login")
  async def login() -> RedirectResponse:
      # ステート生成
      state = hashlib.sha256(os.urandom(32)).hexdigest()
  
      # 追加:code_verifier生成
      code_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode('utf-8')
      code_verifier = re.sub('[^a-zA-Z0-9]+', '', code_verifier)
      # 追加:code_challenge生成
      code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
      code_challenge = base64.urlsafe_b64encode(code_challenge).decode('utf-8')
      code_challenge = code_challenge.replace('=', '')
  
      # Authorization Endpointへリダイレクト
      AUTH_URL = AUTH_BASE_URL + '?{}'.format(parse.urlencode({
          'client_id': APP_CLIENT_ID,
          'redirect_uri': APP_REDIRECT_URI,
          'state': state,
          'response_type': 'code',
          'code_challenge':code_challenge, # 追加
          'code_challenge_method':'S256'  # 追加
      }))
      response = RedirectResponse(AUTH_URL)
      # ステート、code_verifier保存 ※保存方法要検討。暫定でcookieに保存。
      response.set_cookie(key="AUTH_STATE", value=state)
      response.set_cookie(key="AUTH_CODE_VERIFIER", value=code_verifier)
      return response
  
  
  # Token Request
  def get_token(code,code_verifier):
  
      params = {
          'client_id': APP_CLIENT_ID,
          'client_secret': APP_CLIENT_SECRET,
          'grant_type': 'authorization_code',
          'redirect_uri': APP_REDIRECT_URI,
          'code': code,
          'code_verifier': code_verifier #追加
      }
      x = requests.post(TOKEN_URL, params, verify=False).content.decode('utf-8')
      return ast.literal_eval(x)
  
  
  # Redirection Endpoint
  # ステートと認可コードを受け取る。
  # ステート検証後、トークンリクエストを実行する。
  @app.get("/auth/callback")
  async def auth(request: Request, code: str, state: str) -> RedirectResponse:
      # State検証
      if state != request.cookies.get("AUTH_STATE"):
          return {"error": "state_verification_failed"}
      # 追加:code_verifier設定
      return get_token(code,  request.cookies.get("AUTH_CODE_VERIFIER"))
  
  if __name__ == "__main__":
      uvicorn.run(app, port=8000, loop="asyncio")
  
  ```

  

## KeyCloak準備

* コンテナを起動する

  ```shell
  docker-compose up
  ```

* Keycloak Admin コンソールにアクセスする。

  ```
  http://localhost:8080
  ```

  ※ログイン情報は`docker-compose`に記載

* クライアント(`Clients`)を登録する。

  * **<u>「Advanced Settings」->「Proof Key for Code Exchange Code Challenge Method 」で「S256」を選択する。</u>**
  * その他の登録情報は、**[Keycloak: Authorization Code Grant Example](https://www.appsdeveloperblog.com/keycloak-authorization-code-grant-example/)**を参考に設定する。

* テストユーザー(`Users`)を登録する。

* コンテナを再起動する。

  ```shell
  docker-compose down
  docker-compose build
  docker-compose up
  ```
## 動作確認
* Keycloak Authorization Endpointリダイレクト用エンドポイントにアクセスする。

  ```
  http://localhost:8000/auth/login
  ```

* ユーザー認証を行う。

* FastAPI側のリダイレクトURIにリダイレクトされ、ブラウザに以下のようなトークンレスポンスJSONが表示される。

  ```json
  {    
      "access_token": "...",    
      "expires_in": 60,    
      "refresh_expires_in": 1800,    
      "refresh_token": "...",    
      "token_type": "Bearer",    
      "not-before-policy": 0,    
      "session_state": "...",    
      "scope": "profile email"
  }
  ```

  

## 参考情報

* [Keycloak: Authorization Code Grant Example](https://www.appsdeveloperblog.com/keycloak-authorization-code-grant-example/)
* [hkiang01](https://github.com/hkiang01)/[fastapi-keycloak-oidc-auth](https://github.com/hkiang01/fastapi-keycloak-oidc-auth) 
* [Step by Step OAuth 2.0 Authorization Code Flow with PKCE](https://www.stefaanlippens.net/oauth-code-flow-pkce.html)
* [Server Administration Guide](https://keycloak-documentation.openstandia.jp/master/ja_JP/server_admin/index.html)