-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add Keycloak OAuth Provider for Enterprise Authentication and local dev #1937
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
stephaneberle9
wants to merge
48
commits into
jlowin:main
Choose a base branch
from
stephaneberle9:feat/keycloak-auth-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,196
−0
Open
Changes from 37 commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
6ce1157
Add AWS Cognito OAuth provider for FastMCP authentication
stephaneberle9 a218059
Add a comprehensive test suite for AWS Cognito OAuth provider
stephaneberle9 9afb431
Add complete documentation for AWS Cognito OAuth authentication in Fa…
stephaneberle9 d407330
Merge branch 'main' into feat/aws-cognito-auth-provider
stephaneberle9 c764664
Enhance AWS Cognito OAuth examples with enhanced user profile handling:
stephaneberle9 a523a96
Adjust versions in docs/integrations/aws-cognito.mdx
stephaneberle9 71a737c
Fix inconsistent tool name in example code snippet in docs/integratio…
stephaneberle9 0d5a819
Remove unnecessary hasattr check from client example and docs
stephaneberle9 dc4ed4a
Have all imports at the top of the file
stephaneberle9 6a4f2ca
Move more inlined imports to the top of the file
stephaneberle9 f4310a5
Align AWS Cognito provider with access token standards
stephaneberle9 c5d9b05
Refactor AWSCognitoTokenVerifier to extend JWTVerifier
stephaneberle9 593db58
Remove unused timeout_seconds parameter from AWS Cognito provider
stephaneberle9 f54ac37
Remove documentation of unused timeout_seconds parameter removed from…
stephaneberle9 385d013
Refactor AWS Cognito provider to use OIDC Discovery and eliminate dom…
stephaneberle9 121bee5
Update documentation to include AWS in OAuth provider mentions
stephaneberle9 f7f8faf
Add Keycloak OAuth authentication provider with complete example setup
stephaneberle9 19c56a8
Implement server-side scope injection and FastMCP compatibility modif…
stephaneberle9 fb8410a
Configure Keycloak realm for Dynamic Client Registration
stephaneberle9 4170258
Add comprehensive test suite for Keycloak OAuth authentication provider
stephaneberle9 8353dbb
Update Keycloak example configuration and realm file for improved
stephaneberle9 9ac8006
Add fix/workaround for "Client not found" error after
stephaneberle9 4f5dff5
Add comprehensive Keycloak OAuth integration documentation
stephaneberle9 e4a0b24
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 14174c1
Fix unit test failures
stephaneberle9 715aa92
Reorganize Keycloak auth example documentation and setup
stephaneberle9 1c0121d
Merge remote-tracking branch 'upstream/main' into feat/keycloak-auth-…
stephaneberle9 986b1f7
Fix KeycloakAuthProvider.get_routes() method signature
stephaneberle9 58decf3
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 321497d
Fix Keycloak realm import volume path
stephaneberle9 69c699c
Use correct path-scoped OAuth protected resource endpoint in Keycloak…
stephaneberle9 8d86a1a
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 edc7407
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 dc4ecae
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 3c8ed2b
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 def60e3
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 eb73267
Fix CodeRabbit review comments for Keycloak OAuth provider
stephaneberle9 6f5622c
Fix remaining CodeRabbit review issues for Keycloak provider
stephaneberle9 df735d2
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 ccc8b71
Fix type errors in KeycloakAuthProvider URL handling
stephaneberle9 67af227
Improve robustness and error handling in KeycloakAuthProvider
stephaneberle9 57648de
Fix HTTP proxying issues in KeycloakAuthProvider
stephaneberle9 0c767c7
Enforce provider-level required scopes with custom verifiers
stephaneberle9 6c55017
Fix proxy robustness issues in KeycloakAuthProvider
stephaneberle9 e75a34a
Add configurable audience validation to KeycloakAuthProvider
stephaneberle9 f474f1d
Fix minor issues in Keycloak integration docs and examples
stephaneberle9 3bcbb36
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 33ac4bd
Merge branch 'main' into feat/keycloak-auth-provider
stephaneberle9 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,283 @@ | ||
| --- | ||
| title: Keycloak OAuth 🤝 FastMCP | ||
| sidebarTitle: Keycloak | ||
| description: Secure your FastMCP server with Keycloak OAuth | ||
| icon: shield-check | ||
| tag: NEW | ||
| --- | ||
|
|
||
| import { VersionBadge } from "/snippets/version-badge.mdx" | ||
|
|
||
| <VersionBadge version="2.12.4" /> | ||
|
|
||
| This guide shows you how to secure your FastMCP server using **Keycloak OAuth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with Dynamic Client Registration (DCR), where Keycloak handles user login and your FastMCP server validates the tokens. | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| Before you begin, you will need: | ||
| 1. A **[Keycloak](https://keycloak.org/)** server instance running (can be localhost for development, e.g., `http://localhost:8080`) | ||
|
|
||
| <Tip> | ||
| To spin up Keycloak instantly on your local machine, use Docker: | ||
|
|
||
| ```bash | ||
| docker run --rm \ | ||
| --name keycloak-fastmcp \ | ||
| -p 8080:8080 \ | ||
| -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \ | ||
| -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin123 \ | ||
| quay.io/keycloak/keycloak:26.3 \ | ||
| start-dev | ||
| ``` | ||
|
|
||
| Then access the admin console at `http://localhost:8080` with username `admin` and password `admin123`. | ||
| </Tip> | ||
|
|
||
| <Tip> | ||
| If you prefer using Docker Compose instead, you may want to have a look at the [`docker-compose.yaml`](https://github.com/jlowin/fastmcp/blob/main/examples/auth/keycloak_auth/docker-compose.yml) file included in the Keycloak auth example. | ||
| </Tip> | ||
|
|
||
| 2. Administrative access to create and configure a Keycloak realm | ||
| 3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`) | ||
|
|
||
| ### Step 1: Configure Keycloak for Dynamic Client Registration (DCR) | ||
|
|
||
| <Steps> | ||
| <Step title="Prepare the Realm Configuration"> | ||
| Before importing, you should review and customize the pre-configured realm file: | ||
|
|
||
| 1. Download the FastMCP Keycloak realm configuration: [`realm-fastmcp.json`](https://github.com/jlowin/fastmcp/blob/main/examples/auth/keycloak_auth/keycloak/realm-fastmcp.json) | ||
| 2. Open the file in a text editor and customize as needed: | ||
| - **Realm name and display name**: Change `"realm": "fastmcp"` and `"displayName": "FastMCP Realm"` to match your project | ||
| - **Trusted hosts configuration**: Look for `"trusted-hosts"` section and update IP addresses if needed | ||
| - `localhost`: For local development | ||
| - `172.17.0.1`: Docker network gateway IP address (required when Keycloak is run with Docker and MCP server directly on localhost) | ||
| - `172.18.0.1`: Docker Compose network gateway IP address (required when Keycloak is run with Docker Compose and MCP server directly on localhost) | ||
| - For production, replace these with your actual domain names | ||
| 3. **Review the test user**: The file includes a test user (`testuser` with password `password123`). You may want to: | ||
| - Change the credentials for security | ||
| - Replace with more meaningful user accounts | ||
| - Or remove and create users later through the admin interface | ||
|
|
||
| <Warning> | ||
| **Production Security**: Always review and customize the configuration before importing, especially realm names, trusted hosts, and user credentials. | ||
| </Warning> | ||
| </Step> | ||
|
|
||
| <Step title="Import the Realm Configuration"> | ||
| <Note> | ||
| The following instructions are based on **Keycloak 26.3**. Menu items, tabs, and interface elements may be slightly different in other Keycloak versions, but the core configuration concepts remain the same. | ||
| </Note> | ||
|
|
||
| 1. In the left-side navigation, click **Manage realms** (if not visible, click the hamburger menu (☰) in the top-left corner to expand the navigation) | ||
| 2. Click **Create realm** | ||
| 3. In the "Create realm" dialog: | ||
| - Drag your `realm-fastmcp.json` file into the **Resource file** box (or use the "Browse" button to find and select it) | ||
| - Keycloak will automatically read the realm name (`fastmcp`) from the file | ||
| - Click the **Create** button | ||
|
|
||
| That's it! This single action will create the `fastmcp` realm and instantly configure everything from the file: | ||
| - The realm settings (including user registration policies) | ||
| - The test user with their credentials | ||
| - All the necessary Client Policies and Client Profiles required to support Dynamic Client Registration (DCR) | ||
| - Trusted hosts configuration for secure client registration | ||
|
|
||
| <Note> | ||
| You may see this warning in the Keycloak logs during import: | ||
| ``` | ||
| Failed to deserialize client policies in the realm fastmcp.Fallback to return empty profiles. | ||
| Details: Unrecognized field "profiles" (class org.keycloak.representations.idm.ClientPoliciesRepresentation), | ||
| not marked as ignorable (2 known properties: "policies","globalPolicies"]) | ||
| ``` | ||
| This is due to Keycloak's buggy/strict parser not recognizing valid older JSON formats but doesn't seem to impact functionality and can be safely ignored. | ||
| </Note> | ||
| </Step> | ||
|
|
||
| <Step title="Verify the Configuration"> | ||
| After import, verify your realm is properly configured: | ||
|
|
||
| 1. **Check the realm URL**: `http://localhost:8080/realms/fastmcp` | ||
| 2. **Verify DCR policies**: Navigate to **Clients** → **Client registration** to see the imported `"Trusted Hosts"` policy with the trusted hosts you have configured earlier | ||
| 3. **Test user access**: The imported test user can be used for initial testing | ||
|
|
||
| <Note> | ||
| Your realm is now ready for FastMCP integration with Dynamic Client Registration fully configured! | ||
| </Note> | ||
| </Step> | ||
| </Steps> | ||
|
|
||
| ### Step 2: FastMCP Configuration | ||
|
|
||
| Create your FastMCP server file and use the KeycloakAuthProvider to handle all the OAuth integration automatically: | ||
|
|
||
| ```python server.py | ||
| from fastmcp import FastMCP | ||
| from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider | ||
| from fastmcp.server.dependencies import get_access_token | ||
|
|
||
| # The KeycloakAuthProvider automatically discovers Keycloak endpoints | ||
| # and configures JWT token validation | ||
| auth_provider = KeycloakAuthProvider( | ||
| realm_url="http://localhost:8080/realms/fastmcp", # Your Keycloak realm URL | ||
| base_url="http://localhost:8000", # Your server's public URL | ||
| required_scopes=["openid", "profile"], # Required OAuth scopes | ||
| ) | ||
|
|
||
| # Create FastMCP server with auth | ||
| mcp = FastMCP(name="My Keycloak Protected Server", auth=auth_provider) | ||
|
|
||
| @mcp.tool | ||
| async def get_access_token_claims() -> dict: | ||
| """Get the authenticated user's access token claims.""" | ||
| token = get_access_token() | ||
| return { | ||
| "sub": token.claims.get("sub"), | ||
| "name": token.claims.get("name"), | ||
| "preferred_username": token.claims.get("preferred_username"), | ||
| "scope": token.claims.get("scope") | ||
| } | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the realm URL and base URL with your actual values!), you can run the following command: | ||
|
|
||
| ```bash | ||
| fastmcp run server.py --transport http --port 8000 | ||
| ``` | ||
|
|
||
| Now, you can use a FastMCP client to test that you can reach your server after authenticating: | ||
|
|
||
| ```python | ||
| import asyncio | ||
| from fastmcp import Client | ||
|
|
||
| async def main(): | ||
| async with Client("http://localhost:8000/mcp/", auth="oauth") as client: | ||
| # First-time connection will open Keycloak login in your browser | ||
| print("✓ Authenticated with Keycloak!") | ||
|
|
||
| # Test the protected tool | ||
| result = await client.call_tool("get_access_token_claims") | ||
| print(f"User: {result['preferred_username']}") | ||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) | ||
| ``` | ||
|
|
||
| When you run the client for the first time: | ||
| 1. Your browser will open to Keycloak's authorization page | ||
| 2. After you log in and authorize the app, you'll be redirected back | ||
| 3. The client receives the token and can make authenticated requests | ||
|
|
||
| <Info> | ||
| The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache. | ||
| </Info> | ||
|
|
||
| ### Troubleshooting: "Client not found" Error | ||
|
|
||
| <Warning> | ||
| If you restart Keycloak or change the realm configuration, you may end up seeing Keycloak showing a "Client not found" error instead of the login screen when running your client. This happens because FastMCP uses Dynamic Client Registration (DCR) and the client ID that was cached locally no longer exists on the Keycloak server. | ||
|
|
||
| **Keycloak error**: "We are sorry... Client not found." | ||
|
|
||
| **Solution**: Clear the local OAuth cache to force re-registration with Keycloak: | ||
|
|
||
| ```python | ||
| from fastmcp.client.auth.oauth import FileTokenStorage | ||
|
|
||
| # Clear OAuth cache for your specific MCP server | ||
| storage = FileTokenStorage("http://localhost:8000/mcp/") # Use your MCP server URL | ||
| storage.clear() | ||
|
|
||
| # Or clear all OAuth cache data for all MCP servers | ||
| FileTokenStorage.clear_all() | ||
| ``` | ||
|
|
||
| After clearing the cache, run your client again. It will automatically re-register with Keycloak and obtain new credentials. | ||
| </Warning> | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| For production deployments, use environment variables instead of hardcoding credentials. | ||
|
|
||
| ### Provider Selection | ||
|
|
||
| Setting this environment variable allows the Keycloak provider to be used automatically without explicitly instantiating it in code. | ||
|
|
||
| <Card> | ||
| <ParamField path="FASTMCP_SERVER_AUTH" default="Not set"> | ||
| Set to `fastmcp.server.auth.providers.keycloak.KeycloakAuthProvider` to use Keycloak authentication. | ||
| </ParamField> | ||
| </Card> | ||
|
|
||
| ### Keycloak-Specific Configuration | ||
|
|
||
| These environment variables provide default values for the Keycloak provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`. | ||
|
|
||
| <Card> | ||
| <ParamField path="FASTMCP_SERVER_AUTH_KEYCLOAK_REALM_URL" required> | ||
| Your Keycloak realm URL (e.g., `http://localhost:8080/realms/fastmcp` or `https://keycloak.example.com/realms/myrealm`) | ||
| </ParamField> | ||
|
|
||
| <ParamField path="FASTMCP_SERVER_AUTH_KEYCLOAK_BASE_URL" required> | ||
| Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000` for development) | ||
| </ParamField> | ||
|
|
||
| <ParamField path="FASTMCP_SERVER_AUTH_KEYCLOAK_REQUIRED_SCOPES" default="None"> | ||
| Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid profile` or `["openid","profile","email"]`) | ||
| </ParamField> | ||
| </Card> | ||
|
|
||
| Example `.env` file: | ||
| ```bash | ||
| # Use the Keycloak provider | ||
| FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.keycloak.KeycloakAuthProvider | ||
|
|
||
| # Keycloak configuration | ||
| FASTMCP_SERVER_AUTH_KEYCLOAK_REALM_URL=http://localhost:8080/realms/fastmcp | ||
| FASTMCP_SERVER_AUTH_KEYCLOAK_BASE_URL=https://your-server.com | ||
| FASTMCP_SERVER_AUTH_KEYCLOAK_REQUIRED_SCOPES=openid,profile,email | ||
| ``` | ||
|
|
||
| With environment variables set, your server code simplifies to: | ||
|
|
||
| ```python server.py | ||
| from fastmcp import FastMCP | ||
|
|
||
| # Authentication is automatically configured from environment | ||
| mcp = FastMCP(name="My Keycloak Protected Server") | ||
|
|
||
| @mcp.tool | ||
| async def protected_operation() -> str: | ||
| """Perform a protected operation.""" | ||
| # Your tool implementation here | ||
| return "Operation completed successfully" | ||
| ``` | ||
|
|
||
| ## Advanced Configuration | ||
|
|
||
| ### Custom Token Verifier | ||
|
|
||
| For advanced use cases, you can provide a custom token verifier: | ||
|
|
||
| ```python | ||
| from fastmcp.server.auth.providers.jwt import JWTVerifier | ||
| from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider | ||
|
|
||
| # Custom JWT verifier with specific audience | ||
| custom_verifier = JWTVerifier( | ||
| jwks_uri="http://localhost:8080/realms/fastmcp/.well-known/jwks.json", | ||
| issuer="http://localhost:8080/realms/fastmcp", | ||
| audience="my-specific-client", | ||
| required_scopes=["api:read", "api:write"] | ||
| ) | ||
|
|
||
| auth_provider = KeycloakAuthProvider( | ||
| realm_url="http://localhost:8080/realms/fastmcp", | ||
| base_url="http://localhost:8000", | ||
| token_verifier=custom_verifier | ||
| ) | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Keycloak Configuration | ||
| FASTMCP_SERVER_AUTH_KEYCLOAK_REALM_URL=http://localhost:8080/realms/fastmcp | ||
| FASTMCP_SERVER_AUTH_KEYCLOAK_BASE_URL=http://localhost:8000 | ||
|
|
||
| # Optional: Specific scopes | ||
| FASTMCP_SERVER_AUTH_KEYCLOAK_REQUIRED_SCOPES=openid,profile |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Keycloak OAuth Example | ||
|
|
||
| Demonstrates FastMCP server protection with Keycloak OAuth. | ||
|
|
||
| ## Setup | ||
|
|
||
| ### 1. Prepare the Realm Configuration | ||
|
|
||
| Review the realm configuration file: [`keycloak/realm-fastmcp.json`](keycloak/realm-fastmcp.json) | ||
|
|
||
| **Optional**: Customize the file for your environment: | ||
| - **Realm name**: Change `"realm": "fastmcp"` to match your project | ||
| - **Trusted hosts**: Update the `"trusted-hosts"` section for your environment | ||
| - **Test user**: Review credentials (`testuser` / `password123`) and change for security | ||
|
|
||
| ### 2. Set Up Keycloak | ||
|
|
||
| Choose one of the following options: | ||
|
|
||
| #### Option A: Local Keycloak Instance (Recommended for Testing) | ||
|
|
||
| See [keycloak/README.md](keycloak/README.md) for details. | ||
|
|
||
| **Note:** The realm will be automatically imported on startup. | ||
|
|
||
| #### Option B: Existing Keycloak Instance | ||
|
|
||
| Manually import the realm: | ||
| - Log in to your Keycloak Admin Console | ||
| - Click **Manage realms** → **Create realm** | ||
| - Drag the `realm-fastmcp.json` file into the **Resource file** box | ||
| - Click **Create** | ||
|
|
||
| ### 3. Run the Example | ||
|
|
||
| 1. Set environment variables: | ||
|
|
||
| ```bash | ||
| export FASTMCP_SERVER_AUTH_KEYCLOAK_REALM_URL="http://localhost:8080/realms/fastmcp" | ||
| export FASTMCP_SERVER_AUTH_KEYCLOAK_BASE_URL="http://localhost:8000" | ||
| ``` | ||
|
|
||
| 2. Run the server: | ||
|
|
||
| ```bash | ||
| python server.py | ||
| ``` | ||
|
|
||
| 3. In another terminal, run the client: | ||
|
|
||
| ```bash | ||
| python client.py | ||
| ``` | ||
|
|
||
| The client will open your browser for Keycloak authentication. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.