feat(mcp): optional JWT authentication for HTTP transports#627
Closed
nkanu17 wants to merge 6 commits into
Closed
Conversation
Add an optional auth config model for the MCP server's HTTP transports. Supports type none|jwt with JWKS or static public key, issuer, audience, required/read/write scopes. Validators enforce exactly one JWT key source and a required audience (RFC 8707) so tokens minted for other services cannot be replayed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose auth_* env fields on MCPSettings and an auth_overrides() helper that maps the non-None values to an MCPAuthConfig mapping, splitting comma- separated required scopes. Env values take precedence over YAML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add redisvl/mcp/auth.py: resolve_auth_config (env over YAML server.auth peek), build_auth_provider (returns a configured JWTVerifier; fastmcp imported in- function for the optional mcp extra), peek_yaml_auth, and the token_has_scope gate helper. Covered by unit tests plus an integration test that mints real RS256 tokens and asserts accept/reject on audience, issuer, expiry, and scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve auth at construction time and pass the provider to FastMCP, so HTTP transports validate bearer tokens. Expose auth_config and _auth_enabled. stdio is unaffected. Verified the server attaches a JWTVerifier for jwt and none for unset config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Start the server over streamable-http and assert that no-token, garbage, and wrong-audience requests are rejected while a valid scoped token can list tools and search. Tokens are minted with RSAKeyPair and validated against its static public key, so no network JWKS is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a how-to guide explaining JWT validation, audience binding, read/write scope gating, the configurable authorization claim, and the gateway boundary for per-tenant Redis ACL enforcement, with mermaid diagrams. Include the design spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🛡️ Jit Security Scan Results✅ No security findings were detected in this PR
Security scan by Jit
|
Contributor
There was a problem hiding this comment.
Pull request overview
Adds opt-in JWT authentication plumbing to the RedisVL MCP server so HTTP transports can validate bearer tokens based on config/env, alongside new docs/spec and unit/integration tests.
Changes:
- Introduces
MCPAuthConfig(YAMLserver.auth) +REDISVL_MCP_AUTH_*environment overrides, and resolves/builds a FastMCPauth=provider at server construction time. - Adds auth resolution/provider unit tests and JWT-focused integration tests (including streamable-http transport coverage).
- Adds a draft auth spec (
mcp-auth-spec.md) and a new user guide page documenting JWT authentication configuration.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
redisvl/mcp/settings.py |
Adds auth_* settings fields and an auth_overrides() helper for env-driven auth config. |
redisvl/mcp/config.py |
Adds MCPAuthConfig and wires it into MCPServerConfig for YAML validation. |
redisvl/mcp/auth.py |
New module to peek YAML auth, merge env/YAML, and build a FastMCP JWT auth provider. |
redisvl/mcp/server.py |
Resolves auth at construction time and passes auth= into FastMCP base constructor. |
tests/unit/test_mcp/test_auth_config.py |
Validates MCPAuthConfig contract/validation rules. |
tests/unit/test_mcp/test_auth_provider.py |
Tests building a FastMCP auth provider + a scope helper. |
tests/unit/test_mcp/test_auth_resolution.py |
Tests env-over-YAML auth config resolution logic. |
tests/unit/test_mcp/test_server_auth.py |
Tests wiring auth into RedisVLMCPServer construction. |
tests/integration/test_mcp/test_auth.py |
Crypto-level JWT verifier integration coverage (no Redis dependency). |
tests/integration/test_mcp/test_transport_auth.py |
End-to-end HTTP transport auth checks using FastMCP client/server. |
docs/user_guide/how_to_guides/mcp_authentication.md |
New documentation page describing JWT auth and (claimed) scope gating. |
mcp-auth-spec.md |
Draft design/spec document for MCP auth and rollout plan. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+56
to
+63
| merged: dict[str, Any] = {**yaml_auth, **env_auth} | ||
| if not merged: | ||
| return None | ||
|
|
||
| config = MCPAuthConfig.model_validate(merged) | ||
| if config.type == "none": | ||
| return None | ||
| return config |
Comment on lines
+74
to
+82
| # Auth is resolved at construction time (FastMCP needs the provider in | ||
| # its constructor), reading env vars and peeking the YAML server.auth | ||
| # block without running full startup. Applies only to HTTP transports. | ||
| auth_config = resolve_auth_config(settings, settings.config) | ||
| auth_provider = build_auth_provider(auth_config) | ||
| self.auth_config = auth_config | ||
| self._auth_enabled = auth_provider is not None | ||
|
|
||
| super().__init__("redisvl", lifespan=self._fastmcp_lifespan, auth=auth_provider) |
Comment on lines
+120
to
+127
| server: | ||
| auth: | ||
| type: jwt | ||
| # ... | ||
| authorization_claim: roles # default: scp | ||
| read_scope: kb.search.read | ||
| write_scope: kb.search.write | ||
| ``` |
Comment on lines
+32
to
+33
| - **Required scopes** to connect, and (optionally) a **read scope** to call | ||
| `search-records` and a **write scope** to call `upsert-records`. |
Comment on lines
+137
to
+147
| # Wrong audience is rejected. | ||
| bad_aud = key.create_token( | ||
| subject="nitin", | ||
| issuer=ISSUER, | ||
| audience="api://some-other-service", | ||
| scopes=[READ_SCOPE], | ||
| ) | ||
| with pytest.raises(Exception): | ||
| async with Client(url, auth=BearerAuth(bad_aud)) as client: | ||
| await client.list_tools() | ||
|
|
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Adds optional JWT authentication to the RedisVL MCP server's HTTP transports (
streamable-http,sse), plus coarse read/write authorization. Off by default, so existing deployments are unaffected.stdiois a local subprocess and is never authenticated.Previously the only access control was the
--read-onlyflag (which just hides the upsert tool). Binding the server to a port exposed the index to any client that could reach it. This PR lets operators require a valid bearer token before the server will serve requests.Closes #625.
How it works
The server validates a bearer JWT that an existing identity provider issued (it does not run an OAuth authorization server and does not mint tokens). On each request it checks:
iss)aud), so a token minted for a different service cannot be replayed (RFC 8707)search-recordsand a write scope forupsert-recordsRequest flow
sequenceDiagram actor User participant IdP as Identity Provider participant MCP as RedisVL MCP Server participant Redis User->>IdP: Authenticate IdP-->>User: Signed JWT (iss, aud, scopes/roles) User->>MCP: MCP request + Bearer JWT MCP->>MCP: Validate signature (JWKS / public key) MCP->>MCP: Check issuer + audience alt token invalid / wrong audience / missing connect scope MCP-->>User: 401 Unauthorized else token valid MCP->>MCP: Gate tool by read / write scope alt scope present MCP->>Redis: Search or upsert (single configured ACL user) Redis-->>MCP: Results MCP-->>User: Tool result else scope missing MCP-->>User: Forbidden end endConfigurable authorization claim
Standard OAuth carries authorization in
scp/scope. Some IdPs (for example Azure AD / Entra) carry app roles in arolesclaim, which does not appear in the standard scope set. Theauthorization_claimsetting (defaultscp) selects which claim the read/write gate reads.flowchart TD A[Validated JWT claims] --> B{authorization_claim} B -->|scp / scope| C["access.scopes"] B -->|roles| D["access.claims.roles"] C --> E[Check read_scope / write_scope] D --> E E -->|present| F[Allow tool] E -->|absent| G[Deny tool]Configuration
YAML (
server.auth), with${ENV}substitution for secrets:Every field is also settable via
REDISVL_MCP_AUTH_*environment variables, which take precedence over YAML.What was tested
Unit (config validation, provider building, env-over-YAML resolution, scope helpers), integration (real RS256 tokens minted with FastMCP's
RSAKeyPairand validated against a static public key), and end-to-end overstreamable-httpagainst real Redis:rolesclaim gates tools correctly (the Azure AD / Entra style)"Can a tenant id from the token drive Redis ACL enforcement?"
This was explicitly probed. Using a token shaped like a real enterprise OIDC access token (sanitized below), we confirmed:
roles,tid,oid,upnclaims are validated and available to the serverauthorization_claim: rolesWhat we found and decided: the
tid(tenant) claim is carried but not acted on. The server holds one Redis connection for one index, so it does not map a tenant/role claim to a per-request Redis ACL user, index, or query filter. That binding belongs in a gateway/policy layer (validate token -> look up claim-to-Redis-identity binding -> inject credentials and filters). See "Out of scope" below.Sanitized token used in tests:
{ "iss": "https://auth.example/{tenant}/v2.0", "aud": "api://redisvl-mcp", "sub": "nitin", "roles": ["kb.search.read"], "tid": "00000000-0000-0000-0000-000000000000", "scp": "kb.read" }Out of scope (intentional, may be future work)
OAuthProvider). RedisVL validates tokens; it does not issue them.Docs
docs/user_guide/how_to_guides/mcp_authentication.md(wired into the how-to index and toctree) with the diagrams above and the gateway-boundary explanationmcp.mdsecurity warning to point at the new guideconcepts/mcp.mdsphinxcontrib-mermaidso the diagrams render on the docs siteNotes
fastmcpprovider imports are deferred so the package stays importable without the optionalmcpextra.