はじめに
前回の第2回では、Keycloakの導入方法と、基本的なユーザ認証を実現するための設定方法について解説しました。第3回となる今回は、Keycloakで「OAuth 2.1」の認可コードフローを実装し、MCPサーバにアクセスする方法について解説します。
「OAuth 2.1」とは
「OAuth 2.1」は、業界標準の認可プロトコルである「OAuth 2.0」の後継として提案されている仕様です。OAuth 2.0は柔軟性が高い反面、不適切な実装が生じる可能性がありました。OAuth 2.1は、10年以上に及ぶOAuth 2.0の運用から得たセキュリティのベストプラクティスを統合し、開発者が安全な実装をしやすくする目的で提案されています。
OAuth 2.1における主な変更点を以下にまとめます。
- 認可コードフローにおけるPKCE(Proof Key for Code Exchange)の必須化
- リダイレクトURIの完全一致の必須化
- Implicit Grantの廃止
- Resource Owner Password Credentials Grantの廃止
- URIのクエリでのBearerトークンの送信の禁止
- パブリッククライアントに向けたリフレッシュトークンは、送信者制約またはワンタイム使用にすることを必須化
OAuth 2.1はOAuth 2.0のセキュリティを強化したサブセットであり、これにより開発者は安全な実装が可能になります。
Keycloakの「クライアントポリシー」
Keycloakには「クライアントポリシー」という、クライアントアプリケーションにセキュリティ要件や認証方式を柔軟に適用するための仕組みがあります。この仕組みにより、クライアントごとに個別設定を行う必要なく、あらかじめ定義したセキュリティプロファイルに基づいて複数のクライアントへ統一的に要件を一括適用できます。
KeycloakにはOAuth 2.1や「FAPI」といった標準化された認証仕様に準拠したセキュリティプロファイルが用意されており、クライアントポリシーを使うと、例えばFAPIであれば「PKCEの必須化」「Signed JWTの強制」「mTLSによる通信保護」といったセキュリティ要件を一括してクライアントに強制することが可能です。
クライアントポリシーは以下の4要素で構成され、管理者はこれらを組み合わせて制御を行います。
- クライアントポリシー(Client Policy):条件とクライアントプロファイルを組み合わせて、どのクライアントにどの要件を適用するかを定義したもの
- 条件(Condition):クライアントプロファイルを適用するクライアントの条件
- クライアントプロファイル(Client Profile):複数のExecutorをまとめた、複数のセキュリティ要件の設定テンプレート
- 実行ロジック(Executor):ポリシーが適用された際に、実際に強制するセキュリティ要件
これらはAdmin ConsoleやREST APIで設定でき、JSONによるIaC化も推奨されています。クライアントポリシーは、セキュリティ標準化と運用効率化を両立するKeycloakの強力な機能の1つです。
認可コードフローの実装
それでは、OAuth 2.1に準拠した認可コードフローを実装してみましょう。今回は、自作したMCPサーバに認可コードフローを用いてアクセスし、ツールの呼び出しを行います(図1)。
なお、OAuth 2.1仕様では、クライアントの認証情報や認可コードを扱うすべての通信にTLSの使用を必須としています。本記事のハンズオンはローカル環境での実装のため、簡易化のためにHTTPを使用しています。本番環境では必ずHTTPSを使用してください。
筆者の実行環境および使用したライブラリを以下に示します。
| 実行環境/ Pythonライブラリ |
Version | 備考 |
|---|---|---|
| OpenJDK | 24 | Keycloakの実行環境 |
| Keycloak | 26.3.3 | 認可サーバ |
| Python | 3.10.17 | トークン取得/MCPサーバ |
| uv | 0.6.16 | Pythonのパッケージ管理 |
| MCP | 1.18.0 | MCPの公式SDK |
| MCP Auth | 0.2.0b1 | MCP向けの認証ライブラリ |
| Starlette | 0.48.0 | Webアプリの定義 |
| Uvicorn | 0.38.0 | Starletteアプリの実行 |
| Node.js | 22.20.0 | MCP Inspector実行の前提 |
| MCP Inspector | 0.17.2 | MCP Inspector本体 |
MCPサーバの作成と動作確認
まずは、簡単なMCPサーバを作成し、MCPクライアントからアクセスしてみます。ここでは、認証認可不要のMCPサーバとして「server_noAuth.py」を用意します。
from fastmcp import FastMCP
mcp = FastMCP(
"Hello",
host="localhost",
port=3000,
)
@mcp.tool()
def hello():
return f"Hello."
if __name__ == "__main__":
mcp.run(transport="streamable-http")
このMCPサーバは“Hello.”という文字を出力するだけのツールを提供します。MCPでは現在「Stdio」「Streamable HTTP」という2つの通信プロトコルをサポートしています。今回はStreamable HTTPを使って接続を行います。
下記コマンドでMCPサーバを起動し、ログからhttp://localhost:3000で動いていることを確認します。
> uv run server_noAuth.py
INFO: Uvicorn running on http://localhost:3000 (Press CTRL+C to quit)
次に、MCPクライアントとして動作する「MCP Inspector」を起動します。以下のコマンドを入力すると、自動的にブラウザでMCP Inspectorが開きます(図2)。
> npx @modelcontextprotocol/inspector uv run dev
左ペインの“Transport Type”に「Streamable HTTP」を、“URL”に「http://localhost:3000/mcp」を入力し、“Connect”をクリックします。MCPサーバへの接続に成功するとConnectボタンがあった場所の下に“Connected”と表示され、右上にメニューが表示されます(図3)。
作成したMCPサーバが提供しているhelloツールを実行してみましょう。“Tools”メニューをクリックし、“List Tools”ボタンをクリックするとツール一覧の取得が実行されます(図4)。
表示された“hello”ツールを選択し、“Run Tools”ボタンをクリックするとツールが実行されます。実行結果に“Hello.”と出力されていることが分かります(図5)。
Keycloakの設定
それでは、認可コードフローでMCPサーバへアクセスするためにKeycloakを設定していきましょう。まず、MCPサーバー用の Keycloak クライアントを作成します。Keycloakの管理コンソールに管理者ユーザーでログインし、第2回で作成したレルム「myrealm」に、新たに「mcp」というクライアントを作成してください(図6)。
“Client authentication”を「ON」に変更し、“PKCE Method”は「S256」を選択します(図7)。
“Valid redirect URIs”には「”http://localhost:8888/callback」と入力し、“Save”ボタンをクリックします(図8)。“localhost:8888”は認可コードを受け取るコールバックサーバです。
Keycloakクライアントの作成が完了したら、“Credentials”タブから“Client Secret”の文字列をコピーしておきましょう(図9)。
次に「スコープ」を定義します。スコープはリソースへのアクセス同意の範囲を表す文字列です。ここでは、最初に作成したhelloツールを提供するMCPサーバへのアクセス同意を表す文字列として「hello」というスコープを定義します。
左ペインから“Client scopes”→“Create client scope”を選択して新たなscopeを定義します。Nameは「hello」、“Include in token scope”は「ON」に変更します(図10)。
続いて、定義したスコープをクライアントに付与します。“Clients”メニューから先ほど作成したクライアント(mcp)を選択し、“Client scopes”タブから“Add client scope”をクリックします。定義したhelloスコープを「Optional」で追加します(図11)。
クライアントプロファイルおよびクライアントポリシーを設定します。左ペインの“Realm settings”をクリックして“Client policies”タブを選択し、“Profiles”メニューが表示されていることを確認します。
まず、表示されているビルトインのプロファイルのうち“oauth-2-1-for-confidential-client”を選択し、内容を確認していきましょう。いくつかの“Executor”が表示されていることが分かります(図12)。
“secure-client-authenticator”というExecutorの中身を確認してみましょう。Allowed Client Authenticatorsに“client-jwt”と“client-x509”が指定されています(図13)。
これは、Keycloakのクライアント認証方法のうち「Private Key JWTおよびMutual TLS Client Authenticationのみを許可する」という制約が設定されることを意味します。
OAuth 2.1ではこれらのクライアント認証方法が推奨されていますが、今回はプログラムの実装をシンプルにするため「Client Secret」の認証方法を許容する設定を行います。
また、“holder-of-key-enforcer”というExecutorは「送信者制約(Holder-of-Key / HOK)」を強制するためのExecutorです。こちらもOAuth 2.1で推奨されていますが、実装をシンプルにするため、このExecutorは設定せずに認証します。これらの設定でもOAuth 2.1に準拠できます(OAuth 2.1で推奨されている認証フローの実装(client-jwt、mTLS、HOK等)については第5回で取り扱う予定です)。
それでは、今回使用するクライアントプロファイルを作成していきましょう。クライアントプロファイル一覧のページに戻り、“Create client profile”をクリックし、“Client profile name”に「oauth2.1-client-secret」と入力して“Save”ボタンをクリックします(図14)。
“Executors”が指定できるようになるので、ビルトインのプロファイル“oauth-2-1-for-confidential-client”を参考に、Executorを追加していきましょう。
“secure-client-authenticator”ではClient Secret認証を許容させるため、“Allowed Client Authenticators”に“client-secret”を追加します(図15)。また、前述した通り“holder-of-key-enforcer”というExecutorは追加不要です(図16)。
次に、このクライアントプロファイルをクライアントに適用させるためのクライアントポリシーを作成します(図17)。“Save”ボタンをクリックすると“Conditions”と”Client profiles”を指定できます。
“Add condition”をクリックして“Condition type”に「client-scopes」を、“Expected Scopes”に「hello」を、“Scope Type”に「Default」を指定し、“Add”ボタンをクリックします(図18)。
さらに“Add client profile”をクリックし、先ほど作成した“oauth2.1-client-secret”を選択して“Add”ボタンをクリックします(図19)。
これで、作成したクライアントプロファイルが作成したクライアント「mcp」に適用されます(図20)。以上でKeycloakの設定は完了です。
アクセストークン取得プログラムの作成
認可コードフローでKeycloakからアクセストークンを取得するためのPythonプログラム「get_token.py」を用意します。Keycloakの設定部分にて、“KEYCLOAK_CLIENT_SECRET”の値をコピーしておいたClient Secretの文字列に書き換えます。
# Keycloak configuration
KEYCLOAK_SERVER_URL = "http://localhost:8080"
KEYCLOAK_REALM = "myrealm"
KEYCLOAK_CLIENT_ID = "mcp"
KEYCLOAK_CLIENT_SECRET = "<コピーしておいたClient Secretの文字列>"
REDIRECT_URI = "http://localhost:8888/callback"
このプログラムは、Keycloakに認可コードリクエストを発行し、ユーザの認証・認可が完了するとアクセストークンを取得してコンソールに出力します。OAuth 2.1の要件の1つであったPKCEのパラメータ生成も、このプログラムに実装されています。
以下のコマンドでこのプログラムを実行してみましょう。
> uv run get_token.py
ブラウザでログイン画面が開くので、第2回で作成したユーザ情報でログインします(図21)。
ログインに成功すると認可取得画面が表示されます。先ほど追加した“hello”スコープが表示されていることが分かります(図22)。“Email address”“User profile”“User roles”はデフォルトで付与されているスコープです。
“Yes”ボタンを押すと認可コードフローが進み、アクセストークンの取得に成功するとコンソールに取得したアクセストークンが出力されます。このアクセストークンを用いてMCPサーバにアクセスします。
2025-11-26 19:27:57,692 - INFO - Access token: eyJhbGciOiJS…
MCPサーバへの認証機能の追加と動作確認
最後に、MCPサーバに認証機能を追加した「server_auth.py」を用意します。
このプログラムはMCP向け認証用ライブラリとして「MCP Auth」を使用しており、ツール実行時のスコープ検証処理や、アプリケーションにBearer認証のミドルウェアが適用されています。
def has_required_scopes(user_scopes: List[str], required_scopes: List[str]) -> bool:
return all(scope in user_scopes for scope in required_scopes)
@mcp.tool()
def hello():
auth_info = mcp_auth.auth_info
user_scopes = auth_info.scopes if auth_info else []
if not has_required_scopes(user_scopes, ["hello"]):
raise MCPAuthBearerAuthException(BearerAuthExceptionCode.MISSING_REQUIRED_SCOPES)
return f"Hello."
bearer_auth = Middleware(mcp_auth.bearer_auth_middleware('jwt', resource=resource_id))
app = Starlette(
routes=[
*mcp_auth.resource_metadata_router().routes,
Mount("/", app=mcp.streamable_http_app(), middleware=[bearer_auth]),
],
lifespan=lifespan,
)
それでは、下記のコマンドでこのMCPサーバを起動しましょう。サーバがhttp://localhost:3001で動いていることを確認します。
> uv run server_auth.py
INFO: Uvicorn running on http://localhost:3001 (Press CTRL+C to quit)
次にMCP Inspectorを開き、URLを「http://localhost:3001/mcp」に変更します。さらに“Authentication”メニューを開き、“Custom Header”にBearer認証のためのヘッダを追加します。ヘッダ名は「Authorization」、トークンは「Bearer <取得したアクセストークン>”の形式で指定します(図23)。
前述した通り、OAuth 2.1ではURIクエリでのBearerトークンの送信は禁止されているため、このようにAuthorizationヘッダでトークンを送信します。