Jeremiah Lowin commited on
Commit
b6176a5
·
unverified ·
1 Parent(s): 9a6f944

Add complete support for TokenVerifier protocol (#1297)

Browse files
docs/docs.json CHANGED
@@ -65,10 +65,7 @@
65
  {
66
  "group": "Essentials",
67
  "icon": "cube",
68
- "pages": [
69
- "servers/server",
70
- "deployment/running-server"
71
- ]
72
  },
73
  {
74
  "group": "Core Components",
@@ -96,9 +93,7 @@
96
  {
97
  "group": "Authentication",
98
  "icon": "shield-check",
99
- "pages": [
100
- "servers/auth/bearer"
101
- ]
102
  }
103
  ]
104
  },
@@ -108,10 +103,7 @@
108
  {
109
  "group": "Essentials",
110
  "icon": "cube",
111
- "pages": [
112
- "clients/client",
113
- "clients/transports"
114
- ]
115
  },
116
  {
117
  "group": "Core Operations",
@@ -137,10 +129,7 @@
137
  {
138
  "group": "Authentication",
139
  "icon": "user-shield",
140
- "pages": [
141
- "clients/auth/oauth",
142
- "clients/auth/bearer"
143
- ]
144
  }
145
  ]
146
  },
@@ -186,17 +175,12 @@
186
  },
187
  {
188
  "anchor": "What's New",
189
- "pages": [
190
- "updates",
191
- "changelog"
192
- ]
193
  },
194
  {
195
  "anchor": "Community",
196
  "icon": "users",
197
- "pages": [
198
- "community/showcase"
199
- ]
200
  }
201
  ]
202
  },
 
65
  {
66
  "group": "Essentials",
67
  "icon": "cube",
68
+ "pages": ["servers/server", "deployment/running-server"]
 
 
 
69
  },
70
  {
71
  "group": "Core Components",
 
93
  {
94
  "group": "Authentication",
95
  "icon": "shield-check",
96
+ "pages": ["servers/auth/verifiers"]
 
 
97
  }
98
  ]
99
  },
 
103
  {
104
  "group": "Essentials",
105
  "icon": "cube",
106
+ "pages": ["clients/client", "clients/transports"]
 
 
 
107
  },
108
  {
109
  "group": "Core Operations",
 
129
  {
130
  "group": "Authentication",
131
  "icon": "user-shield",
132
+ "pages": ["clients/auth/oauth", "clients/auth/bearer"]
 
 
 
133
  }
134
  ]
135
  },
 
175
  },
176
  {
177
  "anchor": "What's New",
178
+ "pages": ["updates", "changelog"]
 
 
 
179
  },
180
  {
181
  "anchor": "Community",
182
  "icon": "users",
183
+ "pages": ["community/showcase"]
 
 
184
  }
185
  ]
186
  },
docs/integrations/anthropic.mdx CHANGED
@@ -125,7 +125,7 @@ For this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPa
125
  We'll start by creating an RSA key pair to sign and verify tokens.
126
 
127
  ```python
128
- from fastmcp.server.auth.providers.bearer import RSAKeyPair
129
 
130
  key_pair = RSAKeyPair.generate()
131
  access_token = key_pair.create_token(audience="dice-server")
@@ -135,13 +135,13 @@ access_token = key_pair.create_token(audience="dice-server")
135
  FastMCP's `RSAKeyPair` utility is for development and testing only.
136
  </Warning>
137
 
138
- Next, we'll create a `BearerAuthProvider` to authenticate the server.
139
 
140
  ```python
141
  from fastmcp import FastMCP
142
- from fastmcp.server.auth import BearerAuthProvider
143
 
144
- auth = BearerAuthProvider(
145
  public_key=key_pair.public_key,
146
  audience="dice-server",
147
  )
@@ -153,14 +153,14 @@ Here is a complete example that you can copy/paste. For simplicity and the purpo
153
 
154
  ```python server.py [expandable]
155
  from fastmcp import FastMCP
156
- from fastmcp.server.auth import BearerAuthProvider
157
- from fastmcp.server.auth.providers.bearer import RSAKeyPair
158
  import random
159
 
160
  key_pair = RSAKeyPair.generate()
161
  access_token = key_pair.create_token(audience="dice-server")
162
 
163
- auth = BearerAuthProvider(
164
  public_key=key_pair.public_key,
165
  audience="dice-server",
166
  )
 
125
  We'll start by creating an RSA key pair to sign and verify tokens.
126
 
127
  ```python
128
+ from fastmcp.server.auth.verifiers import RSAKeyPair
129
 
130
  key_pair = RSAKeyPair.generate()
131
  access_token = key_pair.create_token(audience="dice-server")
 
135
  FastMCP's `RSAKeyPair` utility is for development and testing only.
136
  </Warning>
137
 
138
+ Next, we'll create a `JWTVerifier` to authenticate the server.
139
 
140
  ```python
141
  from fastmcp import FastMCP
142
+ from fastmcp.server.auth import JWTVerifier
143
 
144
+ auth = JWTVerifier(
145
  public_key=key_pair.public_key,
146
  audience="dice-server",
147
  )
 
153
 
154
  ```python server.py [expandable]
155
  from fastmcp import FastMCP
156
+ from fastmcp.server.auth import JWTVerifier
157
+ from fastmcp.server.auth.verifiers import RSAKeyPair
158
  import random
159
 
160
  key_pair = RSAKeyPair.generate()
161
  access_token = key_pair.create_token(audience="dice-server")
162
 
163
+ auth = JWTVerifier(
164
  public_key=key_pair.public_key,
165
  audience="dice-server",
166
  )
docs/integrations/openai.mdx CHANGED
@@ -123,7 +123,7 @@ For this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPa
123
  We'll start by creating an RSA key pair to sign and verify tokens.
124
 
125
  ```python
126
- from fastmcp.server.auth.providers.bearer import RSAKeyPair
127
 
128
  key_pair = RSAKeyPair.generate()
129
  access_token = key_pair.create_token(audience="dice-server")
@@ -133,13 +133,13 @@ access_token = key_pair.create_token(audience="dice-server")
133
  FastMCP's `RSAKeyPair` utility is for development and testing only.
134
  </Warning>
135
 
136
- Next, we'll create a `BearerAuthProvider` to authenticate the server.
137
 
138
  ```python
139
  from fastmcp import FastMCP
140
- from fastmcp.server.auth import BearerAuthProvider
141
 
142
- auth = BearerAuthProvider(
143
  public_key=key_pair.public_key,
144
  audience="dice-server",
145
  )
@@ -151,14 +151,14 @@ Here is a complete example that you can copy/paste. For simplicity and the purpo
151
 
152
  ```python server.py [expandable]
153
  from fastmcp import FastMCP
154
- from fastmcp.server.auth import BearerAuthProvider
155
- from fastmcp.server.auth.providers.bearer import RSAKeyPair
156
  import random
157
 
158
  key_pair = RSAKeyPair.generate()
159
  access_token = key_pair.create_token(audience="dice-server")
160
 
161
- auth = BearerAuthProvider(
162
  public_key=key_pair.public_key,
163
  audience="dice-server",
164
  )
 
123
  We'll start by creating an RSA key pair to sign and verify tokens.
124
 
125
  ```python
126
+ from fastmcp.server.auth.verifiers import RSAKeyPair
127
 
128
  key_pair = RSAKeyPair.generate()
129
  access_token = key_pair.create_token(audience="dice-server")
 
133
  FastMCP's `RSAKeyPair` utility is for development and testing only.
134
  </Warning>
135
 
136
+ Next, we'll create a `JWTVerifier` to authenticate the server.
137
 
138
  ```python
139
  from fastmcp import FastMCP
140
+ from fastmcp.server.auth import JWTVerifier
141
 
142
+ auth = JWTVerifier(
143
  public_key=key_pair.public_key,
144
  audience="dice-server",
145
  )
 
151
 
152
  ```python server.py [expandable]
153
  from fastmcp import FastMCP
154
+ from fastmcp.server.auth import JWTVerifier
155
+ from fastmcp.server.auth.verifiers import RSAKeyPair
156
  import random
157
 
158
  key_pair = RSAKeyPair.generate()
159
  access_token = key_pair.create_token(audience="dice-server")
160
 
161
+ auth = JWTVerifier(
162
  public_key=key_pair.public_key,
163
  audience="dice-server",
164
  )
docs/servers/auth/{bearer.mdx → verifiers.mdx} RENAMED
@@ -1,22 +1,19 @@
1
  ---
2
- title: Bearer Token Authentication
3
- sidebarTitle: Bearer Auth
4
- description: Secure your FastMCP server's HTTP endpoints by validating JWT Bearer tokens.
5
  icon: key
6
  tag: NEW
7
  ---
8
 
9
  import { VersionBadge } from "/snippets/version-badge.mdx"
10
 
11
- <VersionBadge version="2.6.0" />
 
12
  <Tip>
13
  Authentication and authorization are only relevant for HTTP-based transports.
14
  </Tip>
15
 
16
- <Note>
17
- The [MCP specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) requires servers to implement full OAuth 2.1 authorization flows with dynamic client registration, server metadata discovery, and complete token endpoints. FastMCP's Bearer Token authentication provides a simpler, more practical alternative by directly validating pre-issued JWT tokens—ideal for service-to-service communication and programmatic environments where full OAuth flows may be impractical, and in accordance with how the MCP ecosystem is pragmatically evolving. However, please note that since it doesn't implement the full OAuth 2.1 flow, this implementation does not strictly comply with the MCP specification.
18
- </Note>
19
-
20
  Bearer Token authentication is a common way to secure HTTP-based APIs. In this model, the client sends a token (usually a JSON Web Token or JWT) in the `Authorization` header with the "Bearer" scheme. The server then validates this token to grant or deny access.
21
 
22
  FastMCP supports Bearer Token authentication for its HTTP-based transports (`http` and `sse`), allowing you to protect your server from unauthorized access.
@@ -32,37 +29,29 @@ FastMCP uses **asymmetric encryption** for token validation, which provides a cl
32
 
33
  This design allows you to integrate FastMCP servers into existing authentication infrastructures without compromising security boundaries.
34
 
35
- ## Configuration
36
 
37
- To enable Bearer Token validation on your FastMCP server, use the `BearerAuthProvider` class. This provider validates incoming JWTs by verifying signatures, checking expiration, and optionally validating claims.
38
 
39
- <Warning>
40
- The `BearerAuthProvider` validates tokens; it does **not** issue them (or implement any part of an OAuth flow). You'll need to generate tokens separately, either using FastMCP utilities or an external Identity Provider (IdP) or OAuth 2.1 Authorization Server.
41
- </Warning>
42
-
43
- ### Basic Setup
44
 
45
- To configure bearer token authentication, instantiate a `BearerAuthProvider` instance and pass it to the `auth` parameter of the `FastMCP` instance.
 
46
 
47
- The `BearerAuthProvider` requires either a static public key or a JWKS URI (but not both!) in order to verify the token's signature. All other parameters are optional -- if they are provided, they will be used as additional validation criteria.
48
-
49
- ```python {2, 10}
50
- from fastmcp import FastMCP
51
- from fastmcp.server.auth import BearerAuthProvider
52
 
53
- auth = BearerAuthProvider(
54
- jwks_uri="https://my-identity-provider.com/.well-known/jwks.json",
55
- issuer="https://my-identity-provider.com/",
56
- algorithm="RS512",
57
- audience="my-mcp-server"
58
- )
59
 
60
- mcp = FastMCP(name="My MCP Server", auth=auth)
61
- ```
62
 
63
  ### Configuration Parameters
64
 
65
- <Card icon="code" title="BearerAuthProvider Configuration">
 
 
66
  <ParamField body="public_key" type="str">
67
  RSA public key in PEM format for static key validation. Required if `jwks_uri` is not provided
68
  </ParamField>
@@ -87,13 +76,66 @@ mcp = FastMCP(name="My MCP Server", auth=auth)
87
  Global scopes required for all requests
88
  </ParamField>
89
  </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- #### Public Key
 
 
92
 
93
- If you have a public key in PEM format, you can provide it to the `BearerAuthProvider` as a string.
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  ```python {12}
96
- from fastmcp.server.auth import BearerAuthProvider
97
  import inspect
98
 
99
  public_key_pem = inspect.cleandoc(
@@ -104,13 +146,13 @@ public_key_pem = inspect.cleandoc(
104
  """
105
  )
106
 
107
- auth = BearerAuthProvider(public_key=public_key_pem)
108
  ```
109
 
110
- #### JWKS URI
111
 
112
  ```python
113
- provider = BearerAuthProvider(
114
  jwks_uri="https://idp.example.com/.well-known/jwks.json"
115
  )
116
  ```
@@ -119,6 +161,58 @@ provider = BearerAuthProvider(
119
  JWKS is recommended for production as it supports automatic key rotation and multiple signing keys.
120
  </Note>
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  ## Generating Tokens
123
 
124
  For development and testing, FastMCP provides the `RSAKeyPair` utility class to generate tokens without needing an external OAuth provider.
@@ -130,14 +224,13 @@ The `RSAKeyPair` utility is intended for development and testing only. For produ
130
 
131
  ```python
132
  from fastmcp import FastMCP
133
- from fastmcp.server.auth import BearerAuthProvider
134
- from fastmcp.server.auth.providers.bearer import RSAKeyPair
135
 
136
  # Generate a new key pair
137
  key_pair = RSAKeyPair.generate()
138
 
139
- # Configure the auth provider with the public key
140
- auth = BearerAuthProvider(
141
  public_key=key_pair.public_key,
142
  issuer="https://dev.example.com",
143
  audience="my-dev-server"
@@ -191,6 +284,7 @@ The `create_token()` method accepts these parameters:
191
  </Card>
192
 
193
 
 
194
  ## Accessing Token Claims
195
 
196
  Once authenticated, your tools, resources, or prompts can access token information using the `get_access_token()` dependency function:
 
1
  ---
2
+ title: Token Verification
3
+ sidebarTitle: Token Verification
4
+ description: Secure your FastMCP server's HTTP endpoints by validating JWT tokens.
5
  icon: key
6
  tag: NEW
7
  ---
8
 
9
  import { VersionBadge } from "/snippets/version-badge.mdx"
10
 
11
+ <VersionBadge version="2.11.0" />
12
+
13
  <Tip>
14
  Authentication and authorization are only relevant for HTTP-based transports.
15
  </Tip>
16
 
 
 
 
 
17
  Bearer Token authentication is a common way to secure HTTP-based APIs. In this model, the client sends a token (usually a JSON Web Token or JWT) in the `Authorization` header with the "Bearer" scheme. The server then validates this token to grant or deny access.
18
 
19
  FastMCP supports Bearer Token authentication for its HTTP-based transports (`http` and `sse`), allowing you to protect your server from unauthorized access.
 
29
 
30
  This design allows you to integrate FastMCP servers into existing authentication infrastructures without compromising security boundaries.
31
 
32
+ ## Token Verification Approaches
33
 
34
+ FastMCP provides three token verification approaches:
35
 
36
+ ### JWTVerifier
37
+ Validates JWT tokens using public key cryptography. Use when you have JWT tokens issued by an external identity provider (Auth0, Okta, Keycloak, etc.) and want self-contained validation without network calls.
 
 
 
38
 
39
+ ### IntrospectionTokenVerifier
40
+ Validates tokens by calling a remote OAuth 2.0 authorization server's introspection endpoint (RFC 7662). Use when your authorization server is separate from your FastMCP server, you're using opaque tokens, or you need real-time token revocation.
41
 
42
+ ### StaticTokenVerifier
43
+ Validates tokens against a predefined dictionary. Use for development and testing only - never in production.
 
 
 
44
 
45
+ <Warning>
46
+ These verifiers validate tokens; they do **not** issue them (or implement any part of an OAuth flow). You'll need to generate tokens separately, either using FastMCP utilities or an external Identity Provider (IdP) or OAuth 2.1 Authorization Server.
47
+ </Warning>
 
 
 
48
 
 
 
49
 
50
  ### Configuration Parameters
51
 
52
+ <Tabs>
53
+ <Tab title="JWTVerifier">
54
+ <Card icon="code" title="JWTVerifier Configuration">
55
  <ParamField body="public_key" type="str">
56
  RSA public key in PEM format for static key validation. Required if `jwks_uri` is not provided
57
  </ParamField>
 
76
  Global scopes required for all requests
77
  </ParamField>
78
  </Card>
79
+ </Tab>
80
+
81
+ <Tab title="IntrospectionTokenVerifier">
82
+ <Card icon="code" title="IntrospectionTokenVerifier Configuration">
83
+ <ParamField body="introspection_endpoint" type="str">
84
+ OAuth 2.0 Token Introspection endpoint URL (RFC 7662)
85
+ </ParamField>
86
+
87
+ <ParamField body="client_id" type="str">
88
+ Resource server client ID for introspection authentication
89
+ </ParamField>
90
+
91
+ <ParamField body="client_secret" type="str">
92
+ Resource server client secret for introspection authentication
93
+ </ParamField>
94
+
95
+ <ParamField body="required_scopes" type="list[str] | None">
96
+ Global scopes required for all requests
97
+ </ParamField>
98
+ </Card>
99
+ </Tab>
100
+
101
+ <Tab title="StaticTokenVerifier">
102
+ <Card icon="code" title="StaticTokenVerifier Configuration">
103
+ <ParamField body="valid_tokens" type="dict[str, dict]">
104
+ Mapping of valid tokens to their claims. Each token maps to a dictionary containing token metadata like `sub`, `scope`, etc.
105
+ </ParamField>
106
+
107
+ <ParamField body="required_scopes" type="list[str] | None">
108
+ Global scopes required for all requests
109
+ </ParamField>
110
+ </Card>
111
+ </Tab>
112
+ </Tabs>
113
+
114
+ ## JWT Verification
115
+
116
+ The `JWTVerifier` validates JWT tokens using public key cryptography. Use this when you have JWT tokens issued by an external identity provider and want self-contained validation without network calls.
117
 
118
+ ```python
119
+ from fastmcp import FastMCP
120
+ from fastmcp.server.auth.verifiers import JWTVerifier
121
 
122
+ verifier = JWTVerifier(
123
+ jwks_uri="https://my-identity-provider.com/.well-known/jwks.json",
124
+ issuer="https://my-identity-provider.com/",
125
+ audience="my-mcp-server"
126
+ )
127
+
128
+ mcp = FastMCP(name="My MCP Server", auth=verifier)
129
+ ```
130
+
131
+ ### Public Key Configuration
132
+
133
+ #### Using a Static Public Key
134
+
135
+ If you have a public key in PEM format, you can provide it to the `JWTVerifier` as a string.
136
 
137
  ```python {12}
138
+ from fastmcp.server.auth.verifiers import JWTVerifier
139
  import inspect
140
 
141
  public_key_pem = inspect.cleandoc(
 
146
  """
147
  )
148
 
149
+ auth = JWTVerifier(public_key=public_key_pem)
150
  ```
151
 
152
+ #### Using JWKS URI
153
 
154
  ```python
155
+ verifier = JWTVerifier(
156
  jwks_uri="https://idp.example.com/.well-known/jwks.json"
157
  )
158
  ```
 
161
  JWKS is recommended for production as it supports automatic key rotation and multiple signing keys.
162
  </Note>
163
 
164
+ ## OAuth 2.0 Token Introspection
165
+
166
+ The `IntrospectionTokenVerifier` validates tokens by calling an OAuth 2.0 authorization server's introspection endpoint (RFC 7662). This is useful when your authorization server is separate from your FastMCP server, you're using opaque tokens, or you need real-time token validation with immediate revocation support.
167
+
168
+ ```python
169
+ from fastmcp.server.auth.verifiers import IntrospectionTokenVerifier
170
+
171
+ verifier = IntrospectionTokenVerifier(
172
+ introspection_endpoint="https://auth.company.com/oauth/introspect",
173
+ server_url="https://mcp.company.com", # This server's URL
174
+ client_id="mcp-resource-server",
175
+ client_secret="your-secret",
176
+ required_scopes=["mcp:access"]
177
+ )
178
+
179
+ mcp = FastMCP(name="MCP Server", auth=verifier)
180
+ ```
181
+
182
+ For each request, the verifier makes an HTTP call to the introspection endpoint to check if the token is valid and active. This provides real-time validation but requires network connectivity.
183
+
184
+ ## Static Token Verification
185
+
186
+ The `StaticTokenVerifier` validates tokens against a predefined dictionary of token strings and claims. Use this for development and testing when you need predictable tokens without setting up a real OAuth server.
187
+
188
+ ```python
189
+ from fastmcp.server.auth.verifiers import StaticTokenVerifier
190
+
191
+ verifier = StaticTokenVerifier(
192
+ tokens={
193
+ "dev-token-123": {
194
+ "client_id": "dev-user",
195
+ "scopes": ["read", "write"],
196
+ "sub": "developer@example.com"
197
+ },
198
+ "readonly-token": {
199
+ "client_id": "readonly-user",
200
+ "scopes": ["read"],
201
+ "expires_at": 1735689600 # Optional expiration
202
+ }
203
+ },
204
+ required_scopes=["read"]
205
+ )
206
+
207
+ mcp = FastMCP(name="Development Server", auth=verifier)
208
+ ```
209
+
210
+ Token claims can include `client_id` (required), `scopes`, `sub`, `expires_at`, and any custom metadata your application needs.
211
+
212
+ <Warning>
213
+ Never use StaticTokenVerifier in production - tokens are stored in plain text.
214
+ </Warning>
215
+
216
  ## Generating Tokens
217
 
218
  For development and testing, FastMCP provides the `RSAKeyPair` utility class to generate tokens without needing an external OAuth provider.
 
224
 
225
  ```python
226
  from fastmcp import FastMCP
227
+ from fastmcp.server.auth.verifiers import JWTVerifier, RSAKeyPair
 
228
 
229
  # Generate a new key pair
230
  key_pair = RSAKeyPair.generate()
231
 
232
+ # Configure the auth verifier with the public key
233
+ auth = JWTVerifier(
234
  public_key=key_pair.public_key,
235
  issuer="https://dev.example.com",
236
  audience="my-dev-server"
 
284
  </Card>
285
 
286
 
287
+
288
  ## Accessing Token Claims
289
 
290
  Once authenticated, your tools, resources, or prompts can access token information using the `get_access_token()` dependency function:
docs/servers/server.mdx CHANGED
@@ -40,6 +40,10 @@ The `FastMCP` constructor accepts several arguments:
40
  Description of how to interact with this server. These instructions help clients understand the server's purpose and available functionality
41
  </ParamField>
42
 
 
 
 
 
43
  <ParamField body="lifespan" type="AsyncContextManager | None">
44
  An async context manager function for server startup and shutdown logic
45
  </ParamField>
 
40
  Description of how to interact with this server. These instructions help clients understand the server's purpose and available functionality
41
  </ParamField>
42
 
43
+ <ParamField body="auth" type="OAuthProvider | TokenVerifier | None">
44
+ Authentication provider for securing HTTP-based transports. See [Bearer Token Authentication](/servers/auth/bearer) for configuration options
45
+ </ParamField>
46
+
47
  <ParamField body="lifespan" type="AsyncContextManager | None">
48
  An async context manager function for server startup and shutdown logic
49
  </ParamField>
src/fastmcp/contrib/component_manager/example.py CHANGED
@@ -1,10 +1,10 @@
1
  from fastmcp import FastMCP
2
  from fastmcp.contrib.component_manager import set_up_component_manager
3
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
4
 
5
  key_pair = RSAKeyPair.generate()
6
 
7
- auth = BearerAuthProvider(
8
  public_key=key_pair.public_key,
9
  issuer="https://dev.example.com",
10
  audience="my-dev-server",
 
1
  from fastmcp import FastMCP
2
  from fastmcp.contrib.component_manager import set_up_component_manager
3
+ from fastmcp.server.auth.verifiers import JWTVerifier, RSAKeyPair
4
 
5
  key_pair = RSAKeyPair.generate()
6
 
7
+ auth = JWTVerifier(
8
  public_key=key_pair.public_key,
9
  issuer="https://dev.example.com",
10
  audience="my-dev-server",
src/fastmcp/server/auth/__init__.py CHANGED
@@ -1,4 +1,20 @@
1
- from .providers.bearer import BearerAuthProvider
 
2
 
3
 
4
- __all__ = ["BearerAuthProvider"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .auth import OAuthProvider, TokenVerifier
2
+ from .verifiers import IntrospectionTokenVerifier, JWTVerifier, StaticTokenVerifier
3
 
4
 
5
+ __all__ = [
6
+ "OAuthProvider",
7
+ "TokenVerifier",
8
+ "IntrospectionTokenVerifier",
9
+ "JWTVerifier",
10
+ "StaticTokenVerifier",
11
+ ]
12
+
13
+
14
+ def __getattr__(name: str):
15
+ # Defer import because it raises a deprecation warning
16
+ if name == "BearerAuthProvider":
17
+ from .providers.bearer import BearerAuthProvider
18
+
19
+ return BearerAuthProvider
20
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
src/fastmcp/server/auth/auth.py CHANGED
@@ -4,6 +4,9 @@ from mcp.server.auth.provider import (
4
  OAuthAuthorizationServerProvider,
5
  RefreshToken,
6
  )
 
 
 
7
  from mcp.server.auth.settings import (
8
  ClientRegistrationOptions,
9
  RevocationOptions,
@@ -11,6 +14,35 @@ from mcp.server.auth.settings import (
11
  from pydantic import AnyHttpUrl
12
 
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  class OAuthProvider(
15
  OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]
16
  ):
@@ -44,21 +76,3 @@ class OAuthProvider(
44
  self.client_registration_options = client_registration_options
45
  self.revocation_options = revocation_options
46
  self.required_scopes = required_scopes
47
- self.resource_server_url = (
48
- AnyHttpUrl(resource_server_url) if resource_server_url else None
49
- )
50
-
51
- async def verify_token(self, token: str) -> AccessToken | None:
52
- """
53
- Verify a bearer token and return access info if valid.
54
-
55
- This method implements the TokenVerifier protocol by delegating
56
- to our existing load_access_token method.
57
-
58
- Args:
59
- token: The token string to validate
60
-
61
- Returns:
62
- AccessToken object if valid, None if invalid or expired
63
- """
64
- return await self.load_access_token(token)
 
4
  OAuthAuthorizationServerProvider,
5
  RefreshToken,
6
  )
7
+ from mcp.server.auth.provider import (
8
+ TokenVerifier as TokenVerifierProtocol,
9
+ )
10
  from mcp.server.auth.settings import (
11
  ClientRegistrationOptions,
12
  RevocationOptions,
 
14
  from pydantic import AnyHttpUrl
15
 
16
 
17
+ class TokenVerifier(TokenVerifierProtocol):
18
+ """Base class for token verifiers (Resource Servers)."""
19
+
20
+ def __init__(
21
+ self,
22
+ resource_server_url: AnyHttpUrl | str | None = None,
23
+ required_scopes: list[str] | None = None,
24
+ ):
25
+ """
26
+ Initialize the token verifier.
27
+
28
+ Args:
29
+ resource_server_url: The URL of this resource server (for RFC 8707 resource indicators)
30
+ required_scopes: Scopes that are required for all requests
31
+ """
32
+ self.resource_server_url: AnyHttpUrl | None
33
+ if resource_server_url is None:
34
+ self.resource_server_url = None
35
+ elif isinstance(resource_server_url, str):
36
+ self.resource_server_url = AnyHttpUrl(resource_server_url)
37
+ else:
38
+ self.resource_server_url = resource_server_url
39
+ self.required_scopes = required_scopes or []
40
+
41
+ async def verify_token(self, token: str) -> AccessToken | None:
42
+ """Verify a bearer token and return access info if valid."""
43
+ raise NotImplementedError("Subclasses must implement verify_token")
44
+
45
+
46
  class OAuthProvider(
47
  OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]
48
  ):
 
76
  self.client_registration_options = client_registration_options
77
  self.revocation_options = revocation_options
78
  self.required_scopes = required_scopes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/server/auth/providers/bearer.py CHANGED
@@ -1,482 +1,25 @@
1
- import time
2
- from dataclasses import dataclass
3
- from typing import Any
4
-
5
- import httpx
6
- from authlib.jose import JsonWebKey, JsonWebToken
7
- from authlib.jose.errors import JoseError
8
- from cryptography.hazmat.primitives import serialization
9
- from cryptography.hazmat.primitives.asymmetric import rsa
10
- from mcp.server.auth.provider import (
11
- AccessToken,
12
- AuthorizationCode,
13
- AuthorizationParams,
14
- RefreshToken,
15
- )
16
- from mcp.shared.auth import (
17
- OAuthClientInformationFull,
18
- OAuthToken,
19
- )
20
- from pydantic import AnyHttpUrl, SecretStr, ValidationError
21
- from typing_extensions import TypedDict
22
-
23
- from fastmcp.server.auth.auth import (
24
- ClientRegistrationOptions,
25
- OAuthProvider,
26
- RevocationOptions,
27
- )
28
- from fastmcp.utilities.logging import get_logger
29
-
30
-
31
- class JWKData(TypedDict, total=False):
32
- """JSON Web Key data structure."""
33
-
34
- kty: str # Key type (e.g., "RSA") - required
35
- kid: str # Key ID (optional but recommended)
36
- use: str # Usage (e.g., "sig")
37
- alg: str # Algorithm (e.g., "RS256")
38
- n: str # Modulus (for RSA keys)
39
- e: str # Exponent (for RSA keys)
40
- x5c: list[str] # X.509 certificate chain (for JWKs)
41
- x5t: str # X.509 certificate thumbprint (for JWKs)
42
-
43
-
44
- class JWKSData(TypedDict):
45
- """JSON Web Key Set data structure."""
46
-
47
- keys: list[JWKData]
48
-
49
-
50
- @dataclass(frozen=True, kw_only=True, repr=False)
51
- class RSAKeyPair:
52
- private_key: SecretStr
53
- public_key: str
54
-
55
- @classmethod
56
- def generate(cls) -> "RSAKeyPair":
57
- """
58
- Generate an RSA key pair for testing.
59
-
60
- Returns:
61
- tuple: (private_key_pem, public_key_pem)
62
- """
63
- # Generate private key
64
- private_key = rsa.generate_private_key(
65
- public_exponent=65537,
66
- key_size=2048,
67
- )
68
-
69
- # Get public key
70
- public_key = private_key.public_key()
71
-
72
- # Serialize private key to PEM format
73
- private_pem = private_key.private_bytes(
74
- encoding=serialization.Encoding.PEM,
75
- format=serialization.PrivateFormat.PKCS8,
76
- encryption_algorithm=serialization.NoEncryption(),
77
- ).decode("utf-8")
78
-
79
- # Serialize public key to PEM format
80
- public_pem = public_key.public_bytes(
81
- encoding=serialization.Encoding.PEM,
82
- format=serialization.PublicFormat.SubjectPublicKeyInfo,
83
- ).decode("utf-8")
84
-
85
- return cls(
86
- private_key=SecretStr(private_pem),
87
- public_key=public_pem,
88
- )
89
-
90
- def create_token(
91
- self,
92
- subject: str = "fastmcp-user",
93
- issuer: str = "https://fastmcp.example.com",
94
- audience: str | list[str] | None = None,
95
- scopes: list[str] | None = None,
96
- expires_in_seconds: int = 3600,
97
- additional_claims: dict[str, Any] | None = None,
98
- kid: str | None = None,
99
- ) -> str:
100
- """
101
- Generate a test JWT token for testing purposes.
102
-
103
- Args:
104
- private_key_pem: RSA private key in PEM format
105
- subject: Subject claim (usually user ID)
106
- issuer: Issuer claim
107
- audience: Audience claim - can be a string or list of strings (optional)
108
- scopes: List of scopes to include
109
- expires_in_seconds: Token expiration time in seconds
110
- additional_claims: Any additional claims to include
111
- kid: Key ID for JWKS lookup (optional)
112
-
113
- Returns:
114
- Signed JWT token string
115
- """
116
- # TODO : Add support for configurable algorithms
117
- jwt = JsonWebToken(["RS256"])
118
-
119
- now = int(time.time())
120
-
121
- # Build payload
122
- payload = {
123
- "iss": issuer,
124
- "sub": subject,
125
- "iat": now,
126
- "exp": now + expires_in_seconds,
127
- }
128
-
129
- if audience:
130
- payload["aud"] = audience
131
-
132
- if scopes:
133
- payload["scope"] = " ".join(scopes)
134
-
135
- if additional_claims:
136
- payload.update(additional_claims)
137
-
138
- # Create header
139
- header = {"alg": "RS256"}
140
- if kid:
141
- header["kid"] = kid
142
-
143
- # Sign and return token
144
- token_bytes = jwt.encode(
145
- header,
146
- payload,
147
- key=self.private_key.get_secret_value(),
148
- )
149
- return token_bytes.decode("utf-8")
150
-
151
-
152
- class BearerAuthProvider(OAuthProvider):
153
- """
154
- Simple JWT Bearer Token validator for hosted MCP servers.
155
- Uses RS256 asymmetric encryption by default but supports all JWA algorithms. Supports either static public key
156
- or JWKS URI for key rotation.
157
-
158
- Note that this provider DOES NOT permit client registration or revocation, or any OAuth flows.
159
- It is intended to be used with a control plane that manages clients and tokens.
160
- """
161
-
162
- def __init__(
163
- self,
164
- public_key: str | None = None,
165
- jwks_uri: str | None = None,
166
- issuer: str | None = None,
167
- algorithm: str | None = None,
168
- audience: str | list[str] | None = None,
169
- required_scopes: list[str] | None = None,
170
- resource_server: str | None = None,
171
- ):
172
- """
173
- Initialize the provider. Either public_key or jwks_uri must be provided.
174
-
175
- Args:
176
- public_key: RSA public key in PEM format (for static key)
177
- jwks_uri: URI to fetch keys from (for key rotation)
178
- issuer: Expected issuer claim (optional)
179
- algorithm: Algorithm to use for verification (optional, defaults to RS256)
180
- audience: Expected audience claim - can be a string or list of strings (optional)
181
- required_scopes: List of required scopes for access (optional)
182
- """
183
- if not (public_key or jwks_uri):
184
- raise ValueError("Either public_key or jwks_uri must be provided")
185
- if public_key and jwks_uri:
186
- raise ValueError("Provide either public_key or jwks_uri, not both")
187
-
188
- if not algorithm:
189
- algorithm = "RS256"
190
- if algorithm not in {
191
- "HS256",
192
- "HS384",
193
- "HS512",
194
- "RS256",
195
- "RS384",
196
- "RS512",
197
- "ES256",
198
- "ES384",
199
- "ES512",
200
- "PS256",
201
- "PS384",
202
- "PS512",
203
- }:
204
- raise ValueError(f"Unsupported algorithm: {algorithm}.")
205
-
206
- # Only pass issuer to parent if it's a valid URL, otherwise use default
207
- # This allows the issuer claim validation to work with string issuers per RFC 7519
208
- try:
209
- issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com"
210
- except ValidationError:
211
- # Issuer is not a valid URL, use default for parent class
212
- issuer_url = "https://fastmcp.example.com"
213
-
214
- try:
215
- resource_server_url = (
216
- AnyHttpUrl(resource_server) if resource_server else None
217
- )
218
- except ValidationError:
219
- resource_server_url = None
220
-
221
- super().__init__(
222
- issuer_url=issuer_url,
223
- client_registration_options=ClientRegistrationOptions(enabled=False),
224
- revocation_options=RevocationOptions(enabled=False),
225
- required_scopes=required_scopes,
226
- resource_server_url=resource_server_url,
227
- )
228
-
229
- self.algorithm = algorithm
230
- self.issuer = issuer
231
- self.audience = audience
232
- self.public_key = public_key
233
- self.jwks_uri = jwks_uri
234
- self.jwt = JsonWebToken([self.algorithm]) # Use RS256 by default
235
- self.logger = get_logger(__name__)
236
-
237
- # Simple JWKS cache
238
- self._jwks_cache: dict[str, str] = {}
239
- self._jwks_cache_time: float = 0
240
- self._cache_ttl = 3600 # 1 hour
241
-
242
- async def _get_verification_key(self, token: str) -> str:
243
- """Get the verification key for the token."""
244
- if self.public_key:
245
- return self.public_key
246
-
247
- # Extract kid from token header for JWKS lookup
248
- try:
249
- import base64
250
- import json
251
-
252
- header_b64 = token.split(".")[0]
253
- header_b64 += "=" * (4 - len(header_b64) % 4) # Add padding
254
- header = json.loads(base64.urlsafe_b64decode(header_b64))
255
- kid = header.get("kid")
256
-
257
- return await self._get_jwks_key(kid)
258
-
259
- except Exception as e:
260
- raise ValueError(f"Failed to extract key ID from token: {e}")
261
-
262
- async def _get_jwks_key(self, kid: str | None) -> str:
263
- """Fetch key from JWKS with simple caching."""
264
- if not self.jwks_uri:
265
- raise ValueError("JWKS URI not configured")
266
-
267
- current_time = time.time()
268
-
269
- # Check cache first
270
- if current_time - self._jwks_cache_time < self._cache_ttl:
271
- if kid and kid in self._jwks_cache:
272
- return self._jwks_cache[kid]
273
- elif not kid and len(self._jwks_cache) == 1:
274
- # If no kid but only one key cached, use it
275
- return next(iter(self._jwks_cache.values()))
276
-
277
- # Fetch JWKS
278
- try:
279
- async with httpx.AsyncClient() as client:
280
- response = await client.get(self.jwks_uri)
281
- response.raise_for_status()
282
- jwks_data = response.json()
283
-
284
- # Cache all keys
285
- self._jwks_cache = {}
286
- for key_data in jwks_data.get("keys", []):
287
- key_kid = key_data.get("kid")
288
- jwk = JsonWebKey.import_key(key_data)
289
- public_key = jwk.get_public_key() # type: ignore
290
-
291
- if key_kid:
292
- self._jwks_cache[key_kid] = public_key
293
- else:
294
- # Key without kid - use a default identifier
295
- self._jwks_cache["_default"] = public_key
296
-
297
- self._jwks_cache_time = current_time
298
-
299
- # Select the appropriate key
300
- if kid:
301
- if kid not in self._jwks_cache:
302
- self.logger.debug(
303
- "JWKS key lookup failed: key ID '%s' not found", kid
304
- )
305
- raise ValueError(f"Key ID '{kid}' not found in JWKS")
306
- return self._jwks_cache[kid]
307
- else:
308
- # No kid in token - only allow if there's exactly one key
309
- if len(self._jwks_cache) == 1:
310
- return next(iter(self._jwks_cache.values()))
311
- elif len(self._jwks_cache) > 1:
312
- raise ValueError(
313
- "Multiple keys in JWKS but no key ID (kid) in token"
314
- )
315
- else:
316
- raise ValueError("No keys found in JWKS")
317
-
318
- except Exception as e:
319
- self.logger.debug("JWKS fetch failed: %s", str(e))
320
- raise ValueError(f"Failed to fetch JWKS: {e}")
321
-
322
- async def load_access_token(self, token: str) -> AccessToken | None:
323
- """
324
- Validates the provided JWT bearer token.
325
-
326
- Args:
327
- token: The JWT token string to validate
328
-
329
- Returns:
330
- AccessToken object if valid, None if invalid or expired
331
- """
332
- try:
333
- # Get verification key (static or from JWKS)
334
- verification_key = await self._get_verification_key(token)
335
-
336
- # Decode and verify the JWT token
337
- claims = self.jwt.decode(token, verification_key)
338
-
339
- # Extract client ID early for logging
340
- client_id = claims.get("client_id") or claims.get("sub") or "unknown"
341
-
342
- # Validate expiration
343
- exp = claims.get("exp")
344
- if exp and exp < time.time():
345
- self.logger.debug(
346
- "Token validation failed: expired token for client %s", client_id
347
- )
348
- self.logger.info("Bearer token rejected for client %s", client_id)
349
- return None
350
-
351
- # Validate issuer - note we use issuer instead of issuer_url here because
352
- # issuer is optional, allowing users to make this check optional
353
- if self.issuer:
354
- if claims.get("iss") != self.issuer:
355
- self.logger.debug(
356
- "Token validation failed: issuer mismatch for client %s",
357
- client_id,
358
- )
359
- self.logger.info("Bearer token rejected for client %s", client_id)
360
- return None
361
-
362
- # Validate audience if configured
363
- if self.audience:
364
- aud = claims.get("aud")
365
-
366
- # Handle different combinations of audience types
367
- audience_valid = False
368
- if isinstance(self.audience, list):
369
- # self.audience is a list - check if any expected audience is present
370
- if isinstance(aud, list):
371
- # Both are lists - check for intersection
372
- audience_valid = any(
373
- expected in aud for expected in self.audience
374
- )
375
- else:
376
- # aud is a string - check if it's in our expected list
377
- audience_valid = aud in self.audience
378
- else:
379
- # self.audience is a string - use original logic
380
- if isinstance(aud, list):
381
- audience_valid = self.audience in aud
382
- else:
383
- audience_valid = aud == self.audience
384
-
385
- if not audience_valid:
386
- self.logger.debug(
387
- "Token validation failed: audience mismatch for client %s",
388
- client_id,
389
- )
390
- self.logger.info("Bearer token rejected for client %s", client_id)
391
- return None
392
-
393
- # Extract scopes
394
- scopes = self._extract_scopes(claims)
395
-
396
- return AccessToken(
397
- token=token,
398
- client_id=str(client_id),
399
- scopes=scopes,
400
- expires_at=int(exp) if exp else None,
401
- )
402
-
403
- except JoseError:
404
- self.logger.debug("Token validation failed: JWT signature/format invalid")
405
- return None
406
- except Exception as e:
407
- self.logger.debug("Token validation failed: %s", str(e))
408
- return None
409
-
410
- def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
411
- """
412
- Extract scopes from JWT claims. Supports both 'scope' and 'scp'
413
- claims.
414
-
415
- Checks the `scope` claim first (standard OAuth2 claim), then the `scp`
416
- claim (used by some Identity Providers).
417
- """
418
-
419
- for claim in ["scope", "scp"]:
420
- if claim in claims:
421
- if isinstance(claims[claim], str):
422
- return claims[claim].split()
423
- elif isinstance(claims[claim], list):
424
- return claims[claim]
425
-
426
- return []
427
-
428
- async def verify_token(self, token: str) -> AccessToken | None:
429
- """
430
- Verify a bearer token and return access info if valid.
431
-
432
- This method implements the TokenVerifier protocol by delegating
433
- to our existing load_access_token method.
434
-
435
- Args:
436
- token: The JWT token string to validate
437
-
438
- Returns:
439
- AccessToken object if valid, None if invalid or expired
440
- """
441
- return await self.load_access_token(token)
442
-
443
- # --- Unused OAuth server methods ---
444
- async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
445
- raise NotImplementedError("Client management not supported")
446
-
447
- async def register_client(self, client_info: OAuthClientInformationFull) -> None:
448
- raise NotImplementedError("Client registration not supported")
449
-
450
- async def authorize(
451
- self, client: OAuthClientInformationFull, params: AuthorizationParams
452
- ) -> str:
453
- raise NotImplementedError("Authorization flow not supported")
454
-
455
- async def load_authorization_code(
456
- self, client: OAuthClientInformationFull, authorization_code: str
457
- ) -> AuthorizationCode | None:
458
- raise NotImplementedError("Authorization code flow not supported")
459
-
460
- async def exchange_authorization_code(
461
- self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
462
- ) -> OAuthToken:
463
- raise NotImplementedError("Authorization code exchange not supported")
464
-
465
- async def load_refresh_token(
466
- self, client: OAuthClientInformationFull, refresh_token: str
467
- ) -> RefreshToken | None:
468
- raise NotImplementedError("Refresh token flow not supported")
469
-
470
- async def exchange_refresh_token(
471
- self,
472
- client: OAuthClientInformationFull,
473
- refresh_token: RefreshToken,
474
- scopes: list[str],
475
- ) -> OAuthToken:
476
- raise NotImplementedError("Refresh token exchange not supported")
477
-
478
- async def revoke_token(
479
- self,
480
- token: AccessToken | RefreshToken,
481
- ) -> None:
482
- raise NotImplementedError("Token revocation not supported")
 
1
+ """Backwards compatibility shim for BearerAuthProvider.
2
+
3
+ The BearerAuthProvider class has been moved to fastmcp.server.auth.verifiers.JWTVerifier
4
+ for better organization. This module provides a backwards-compatible import.
5
+ """
6
+
7
+ import warnings
8
+
9
+ import fastmcp
10
+ from fastmcp.server.auth.verifiers import JWKData, JWKSData, RSAKeyPair
11
+ from fastmcp.server.auth.verifiers import JWTVerifier as BearerAuthProvider
12
+
13
+ # Re-export for backwards compatibility
14
+ __all__ = ["BearerAuthProvider", "RSAKeyPair", "JWKData", "JWKSData"]
15
+
16
+ # Deprecated in 2.11
17
+ if fastmcp.settings.deprecation_warnings:
18
+ warnings.warn(
19
+ "The `fastmcp.server.auth.providers.bearer` module is deprecated "
20
+ "and will be removed in a future version. "
21
+ "Please use `fastmcp.server.auth.verifiers.JWTVerifier` "
22
+ "instead of this module's BearerAuthProvider.",
23
+ DeprecationWarning,
24
+ stacklevel=2,
25
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/server/auth/providers/bearer_env.py DELETED
@@ -1,65 +0,0 @@
1
- from types import EllipsisType
2
-
3
- from pydantic_settings import BaseSettings, SettingsConfigDict
4
-
5
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider
6
-
7
-
8
- class EnvBearerAuthProviderSettings(BaseSettings):
9
- """Settings for the BearerAuthProvider."""
10
-
11
- model_config = SettingsConfigDict(
12
- env_prefix="FASTMCP_AUTH_BEARER_",
13
- env_file=".env",
14
- extra="ignore",
15
- )
16
-
17
- public_key: str | None = None
18
- jwks_uri: str | None = None
19
- issuer: str | None = None
20
- algorithm: str | None = None
21
- audience: str | None = None
22
- required_scopes: list[str] | None = None
23
-
24
-
25
- class EnvBearerAuthProvider(BearerAuthProvider):
26
- """
27
- A BearerAuthProvider that loads settings from environment variables. Any
28
- providing setting will always take precedence over the environment
29
- variables.
30
- """
31
-
32
- def __init__(
33
- self,
34
- public_key: str | None | EllipsisType = ...,
35
- jwks_uri: str | None | EllipsisType = ...,
36
- issuer: str | None | EllipsisType = ...,
37
- algorithm: str | None | EllipsisType = ...,
38
- audience: str | None | EllipsisType = ...,
39
- required_scopes: list[str] | None | EllipsisType = ...,
40
- resource_server: str | None | EllipsisType = ...,
41
- ):
42
- """
43
- Initialize the provider.
44
-
45
- Args:
46
- public_key: RSA public key in PEM format (for static key)
47
- jwks_uri: URI to fetch keys from (for key rotation)
48
- issuer: Expected issuer claim (optional)
49
- algorithm: Algorithm to use for verification (optional)
50
- audience: Expected audience claim (optional)
51
- required_scopes: List of required scopes for access (optional)
52
- """
53
- kwargs = {
54
- "public_key": public_key,
55
- "jwks_uri": jwks_uri,
56
- "issuer": issuer,
57
- "algorithm": algorithm,
58
- "audience": audience,
59
- "required_scopes": required_scopes,
60
- "resource_server": resource_server,
61
- }
62
- settings = EnvBearerAuthProviderSettings(
63
- **{k: v for k, v in kwargs.items() if v is not ...}
64
- )
65
- super().__init__(**settings.model_dump())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/server/auth/verifiers.py ADDED
@@ -0,0 +1,718 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TokenVerifier implementations for FastMCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ import httpx
10
+ from authlib.jose import JsonWebKey, JsonWebToken
11
+ from authlib.jose.errors import JoseError
12
+ from cryptography.hazmat.primitives import serialization
13
+ from cryptography.hazmat.primitives.asymmetric import rsa
14
+ from mcp.server.auth.provider import AccessToken
15
+ from pydantic import AnyHttpUrl, SecretStr, ValidationError
16
+ from pydantic_settings import BaseSettings, SettingsConfigDict
17
+ from typing_extensions import TypedDict
18
+
19
+ from fastmcp.server.auth.auth import TokenVerifier
20
+ from fastmcp.utilities.logging import get_logger
21
+ from fastmcp.utilities.types import NotSet, NotSetT
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class JWKData(TypedDict, total=False):
27
+ """JSON Web Key data structure."""
28
+
29
+ kty: str # Key type (e.g., "RSA") - required
30
+ kid: str # Key ID (optional but recommended)
31
+ use: str # Usage (e.g., "sig")
32
+ alg: str # Algorithm (e.g., "RS256")
33
+ n: str # Modulus (for RSA keys)
34
+ e: str # Exponent (for RSA keys)
35
+ x5c: list[str] # X.509 certificate chain (for JWKs)
36
+ x5t: str # X.509 certificate thumbprint (for JWKs)
37
+
38
+
39
+ class JWKSData(TypedDict):
40
+ """JSON Web Key Set data structure."""
41
+
42
+ keys: list[JWKData]
43
+
44
+
45
+ @dataclass(frozen=True, kw_only=True, repr=False)
46
+ class RSAKeyPair:
47
+ """RSA key pair for JWT testing."""
48
+
49
+ private_key: SecretStr
50
+ public_key: str
51
+
52
+ @classmethod
53
+ def generate(cls) -> RSAKeyPair:
54
+ """
55
+ Generate an RSA key pair for testing.
56
+
57
+ Returns:
58
+ RSAKeyPair: Generated key pair
59
+ """
60
+ # Generate private key
61
+ private_key = rsa.generate_private_key(
62
+ public_exponent=65537,
63
+ key_size=2048,
64
+ )
65
+
66
+ # Serialize private key to PEM format
67
+ private_pem = private_key.private_bytes(
68
+ encoding=serialization.Encoding.PEM,
69
+ format=serialization.PrivateFormat.PKCS8,
70
+ encryption_algorithm=serialization.NoEncryption(),
71
+ ).decode("utf-8")
72
+
73
+ # Serialize public key to PEM format
74
+ public_pem = (
75
+ private_key.public_key()
76
+ .public_bytes(
77
+ encoding=serialization.Encoding.PEM,
78
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
79
+ )
80
+ .decode("utf-8")
81
+ )
82
+
83
+ return cls(
84
+ private_key=SecretStr(private_pem),
85
+ public_key=public_pem,
86
+ )
87
+
88
+ def create_token(
89
+ self,
90
+ subject: str = "fastmcp-user",
91
+ issuer: str = "https://fastmcp.example.com",
92
+ audience: str | list[str] | None = None,
93
+ scopes: list[str] | None = None,
94
+ expires_in_seconds: int = 3600,
95
+ additional_claims: dict[str, Any] | None = None,
96
+ kid: str | None = None,
97
+ ) -> str:
98
+ """
99
+ Generate a test JWT token for testing purposes.
100
+
101
+ Args:
102
+ subject: Subject claim (usually user ID)
103
+ issuer: Issuer claim
104
+ audience: Audience claim - can be a string or list of strings (optional)
105
+ scopes: List of scopes to include
106
+ expires_in_seconds: Token expiration time in seconds
107
+ additional_claims: Any additional claims to include
108
+ kid: Key ID to include in header
109
+ """
110
+ import time
111
+
112
+ # Create header
113
+ header = {"alg": "RS256"}
114
+ if kid:
115
+ header["kid"] = kid
116
+
117
+ # Create payload
118
+ payload = {
119
+ "sub": subject,
120
+ "iss": issuer,
121
+ "iat": int(time.time()),
122
+ "exp": int(time.time()) + expires_in_seconds,
123
+ }
124
+
125
+ if audience:
126
+ payload["aud"] = audience
127
+
128
+ if scopes:
129
+ payload["scope"] = " ".join(scopes)
130
+
131
+ if additional_claims:
132
+ payload.update(additional_claims)
133
+
134
+ # Create JWT
135
+ jwt_lib = JsonWebToken(["RS256"])
136
+ token_bytes = jwt_lib.encode(
137
+ header, payload, self.private_key.get_secret_value()
138
+ )
139
+ return (
140
+ token_bytes.decode("utf-8")
141
+ if isinstance(token_bytes, bytes)
142
+ else token_bytes
143
+ )
144
+
145
+
146
+ class JWTVerifier(TokenVerifier):
147
+ """
148
+ JWT token verifier using public key or JWKS.
149
+
150
+ This verifier validates JWT tokens signed by an external issuer. It's ideal for
151
+ scenarios where you have a centralized identity provider (like Auth0, Okta, or
152
+ your own OAuth server) that issues JWTs, and your FastMCP server acts as a
153
+ resource server validating those tokens.
154
+
155
+ Use this when:
156
+ - You have JWT tokens issued by an external service
157
+ - You want asymmetric key verification (public/private key pairs)
158
+ - You need JWKS support for automatic key rotation
159
+ - Your tokens contain standard OAuth scopes and claims
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ public_key: str | None = None,
165
+ jwks_uri: str | None = None,
166
+ issuer: str | None = None,
167
+ audience: str | list[str] | None = None,
168
+ algorithm: str | None = None,
169
+ required_scopes: list[str] | None = None,
170
+ resource_server_url: AnyHttpUrl | str | None = None,
171
+ ):
172
+ """
173
+ Initialize the JWT token verifier.
174
+
175
+ Args:
176
+ public_key: PEM-encoded public key for verification
177
+ jwks_uri: URI to fetch JSON Web Key Set
178
+ issuer: Expected issuer claim
179
+ audience: Expected audience claim(s)
180
+ algorithm: JWT signing algorithm (default: RS256)
181
+ required_scopes: Required scopes for all tokens
182
+ resource_server_url: Resource server URL for TokenVerifier protocol
183
+ """
184
+ if not public_key and not jwks_uri:
185
+ raise ValueError("Either public_key or jwks_uri must be provided")
186
+
187
+ if public_key and jwks_uri:
188
+ raise ValueError("Provide either public_key or jwks_uri, not both")
189
+
190
+ if not algorithm:
191
+ algorithm = "RS256"
192
+ if algorithm not in {
193
+ "HS256",
194
+ "HS384",
195
+ "HS512",
196
+ "RS256",
197
+ "RS384",
198
+ "RS512",
199
+ "ES256",
200
+ "ES384",
201
+ "ES512",
202
+ "PS256",
203
+ "PS384",
204
+ "PS512",
205
+ }:
206
+ raise ValueError(f"Unsupported algorithm: {algorithm}.")
207
+
208
+ # Initialize parent TokenVerifier
209
+ super().__init__(
210
+ resource_server_url=resource_server_url, required_scopes=required_scopes
211
+ )
212
+
213
+ self.algorithm = algorithm
214
+ self.issuer = issuer
215
+ self.audience = audience
216
+ self.public_key = public_key
217
+ self.jwks_uri = jwks_uri
218
+ self.jwt = JsonWebToken([self.algorithm])
219
+ self.logger = get_logger(__name__)
220
+
221
+ # Simple JWKS cache
222
+ self._jwks_cache: dict[str, str] = {}
223
+ self._jwks_cache_time: float = 0
224
+ self._cache_ttl = 3600 # 1 hour
225
+
226
+ async def _get_verification_key(self, token: str) -> str:
227
+ """Get the verification key for the token."""
228
+ if self.public_key:
229
+ return self.public_key
230
+
231
+ # Extract kid from token header for JWKS lookup
232
+ try:
233
+ import base64
234
+ import json
235
+
236
+ header_b64 = token.split(".")[0]
237
+ header_b64 += "=" * (4 - len(header_b64) % 4) # Add padding
238
+ header = json.loads(base64.urlsafe_b64decode(header_b64))
239
+ kid = header.get("kid")
240
+
241
+ return await self._get_jwks_key(kid)
242
+
243
+ except Exception as e:
244
+ raise ValueError(f"Failed to extract key ID from token: {e}")
245
+
246
+ async def _get_jwks_key(self, kid: str | None) -> str:
247
+ """Fetch key from JWKS with simple caching."""
248
+ if not self.jwks_uri:
249
+ raise ValueError("JWKS URI not configured")
250
+
251
+ current_time = time.time()
252
+
253
+ # Check cache first
254
+ if current_time - self._jwks_cache_time < self._cache_ttl:
255
+ if kid and kid in self._jwks_cache:
256
+ return self._jwks_cache[kid]
257
+ elif not kid and len(self._jwks_cache) == 1:
258
+ # If no kid but only one key cached, use it
259
+ return next(iter(self._jwks_cache.values()))
260
+
261
+ # Fetch JWKS
262
+ try:
263
+ async with httpx.AsyncClient() as client:
264
+ response = await client.get(self.jwks_uri)
265
+ response.raise_for_status()
266
+ jwks_data = response.json()
267
+
268
+ # Cache all keys
269
+ self._jwks_cache = {}
270
+ for key_data in jwks_data.get("keys", []):
271
+ key_kid = key_data.get("kid")
272
+ jwk = JsonWebKey.import_key(key_data)
273
+ public_key = jwk.get_public_key() # type: ignore
274
+
275
+ if key_kid:
276
+ self._jwks_cache[key_kid] = public_key
277
+ else:
278
+ # Key without kid - use a default identifier
279
+ self._jwks_cache["_default"] = public_key
280
+
281
+ self._jwks_cache_time = current_time
282
+
283
+ # Select the appropriate key
284
+ if kid:
285
+ if kid not in self._jwks_cache:
286
+ self.logger.debug(
287
+ "JWKS key lookup failed: key ID '%s' not found", kid
288
+ )
289
+ raise ValueError(f"Key ID '{kid}' not found in JWKS")
290
+ return self._jwks_cache[kid]
291
+ else:
292
+ # No kid in token - only allow if there's exactly one key
293
+ if len(self._jwks_cache) == 1:
294
+ return next(iter(self._jwks_cache.values()))
295
+ elif len(self._jwks_cache) > 1:
296
+ raise ValueError(
297
+ "Multiple keys in JWKS but no key ID (kid) in token"
298
+ )
299
+ else:
300
+ raise ValueError("No keys found in JWKS")
301
+
302
+ except httpx.HTTPError as e:
303
+ raise ValueError(f"Failed to fetch JWKS: {e}")
304
+ except Exception as e:
305
+ self.logger.debug(f"JWKS fetch failed: {e}")
306
+ raise ValueError(f"Failed to fetch JWKS: {e}")
307
+
308
+ def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
309
+ """
310
+ Extract scopes from JWT claims. Supports both 'scope' and 'scp'
311
+ claims.
312
+
313
+ Checks the `scope` claim first (standard OAuth2 claim), then the `scp`
314
+ claim (used by some Identity Providers).
315
+ """
316
+ for claim in ["scope", "scp"]:
317
+ if claim in claims:
318
+ if isinstance(claims[claim], str):
319
+ return claims[claim].split()
320
+ elif isinstance(claims[claim], list):
321
+ return claims[claim]
322
+
323
+ return []
324
+
325
+ async def load_access_token(self, token: str) -> AccessToken | None:
326
+ """
327
+ Validates the provided JWT bearer token.
328
+
329
+ Args:
330
+ token: The JWT token string to validate
331
+
332
+ Returns:
333
+ AccessToken object if valid, None if invalid or expired
334
+ """
335
+ try:
336
+ # Get verification key (static or from JWKS)
337
+ verification_key = await self._get_verification_key(token)
338
+
339
+ # Decode and verify the JWT token
340
+ claims = self.jwt.decode(token, verification_key)
341
+
342
+ # Extract client ID early for logging
343
+ client_id = claims.get("client_id") or claims.get("sub") or "unknown"
344
+
345
+ # Validate expiration
346
+ exp = claims.get("exp")
347
+ if exp and exp < time.time():
348
+ self.logger.debug(
349
+ "Token validation failed: expired token for client %s", client_id
350
+ )
351
+ self.logger.info("Bearer token rejected for client %s", client_id)
352
+ return None
353
+
354
+ # Validate issuer - note we use issuer instead of issuer_url here because
355
+ # issuer is optional, allowing users to make this check optional
356
+ if self.issuer:
357
+ if claims.get("iss") != self.issuer:
358
+ self.logger.debug(
359
+ "Token validation failed: issuer mismatch for client %s",
360
+ client_id,
361
+ )
362
+ self.logger.info("Bearer token rejected for client %s", client_id)
363
+ return None
364
+
365
+ # Validate audience if configured
366
+ if self.audience:
367
+ aud = claims.get("aud")
368
+
369
+ # Handle different combinations of audience types
370
+ audience_valid = False
371
+ if isinstance(self.audience, list):
372
+ # self.audience is a list - check if any expected audience is present
373
+ if isinstance(aud, list):
374
+ # Both are lists - check for intersection
375
+ audience_valid = any(
376
+ expected in aud for expected in self.audience
377
+ )
378
+ else:
379
+ # aud is a string - check if it's in our expected list
380
+ audience_valid = aud in self.audience
381
+ else:
382
+ # self.audience is a string - use original logic
383
+ if isinstance(aud, list):
384
+ audience_valid = self.audience in aud
385
+ else:
386
+ audience_valid = aud == self.audience
387
+
388
+ if not audience_valid:
389
+ self.logger.debug(
390
+ "Token validation failed: audience mismatch for client %s",
391
+ client_id,
392
+ )
393
+ self.logger.info("Bearer token rejected for client %s", client_id)
394
+ return None
395
+
396
+ # Extract scopes
397
+ scopes = self._extract_scopes(claims)
398
+
399
+ # Check required scopes
400
+ if self.required_scopes:
401
+ token_scopes = set(scopes)
402
+ required_scopes = set(self.required_scopes)
403
+ if not required_scopes.issubset(token_scopes):
404
+ self.logger.debug(
405
+ "Token missing required scopes. Has: %s, Required: %s",
406
+ token_scopes,
407
+ required_scopes,
408
+ )
409
+ self.logger.info("Bearer token rejected for client %s", client_id)
410
+ return None
411
+
412
+ return AccessToken(
413
+ token=token,
414
+ client_id=str(client_id),
415
+ scopes=scopes,
416
+ expires_at=int(exp) if exp else None,
417
+ )
418
+
419
+ except JoseError:
420
+ self.logger.debug("Token validation failed: JWT signature/format invalid")
421
+ return None
422
+ except Exception as e:
423
+ self.logger.debug("Token validation failed: %s", str(e))
424
+ return None
425
+
426
+ async def verify_token(self, token: str) -> AccessToken | None:
427
+ """
428
+ Verify a bearer token and return access info if valid.
429
+
430
+ This method implements the TokenVerifier protocol by delegating
431
+ to our existing load_access_token method.
432
+
433
+ Args:
434
+ token: The JWT token string to validate
435
+
436
+ Returns:
437
+ AccessToken object if valid, None if invalid or expired
438
+ """
439
+ return await self.load_access_token(token)
440
+
441
+
442
+ class JWTVerifierSettings(BaseSettings):
443
+ """Settings for the BearerAuthProvider."""
444
+
445
+ model_config = SettingsConfigDict(
446
+ env_prefix="FASTMCP_AUTH_JWT_",
447
+ env_file=".env",
448
+ extra="ignore",
449
+ )
450
+
451
+ public_key: str | None = None
452
+ jwks_uri: str | None = None
453
+ issuer: str | None = None
454
+ algorithm: str | None = None
455
+ audience: str | None = None
456
+ required_scopes: list[str] | None = None
457
+ resource_server_url: AnyHttpUrl | str | None = None
458
+
459
+
460
+ class EnvJWTVerifier(JWTVerifier):
461
+ def __init__(
462
+ self,
463
+ public_key: str | None | NotSetT = NotSet,
464
+ jwks_uri: str | None | NotSetT = NotSet,
465
+ issuer: str | None | NotSetT = NotSet,
466
+ audience: str | list[str] | None | NotSetT = NotSet,
467
+ algorithm: str | None | NotSetT = NotSet,
468
+ required_scopes: list[str] | None | NotSetT = NotSet,
469
+ resource_server_url: AnyHttpUrl | str | None | NotSetT = NotSet,
470
+ ):
471
+ kwargs = {
472
+ "public_key": public_key,
473
+ "jwks_uri": jwks_uri,
474
+ "issuer": issuer,
475
+ "algorithm": algorithm,
476
+ "audience": audience,
477
+ "required_scopes": required_scopes,
478
+ "resource_server_url": resource_server_url,
479
+ }
480
+ settings = JWTVerifierSettings(
481
+ **{k: v for k, v in kwargs.items() if v is not NotSet}
482
+ )
483
+ super().__init__(**settings.model_dump())
484
+
485
+
486
+ class IntrospectionTokenVerifier(TokenVerifier):
487
+ """
488
+ OAuth 2.0 Token Introspection verifier (RFC 7662).
489
+
490
+ This verifier validates tokens by making real-time calls to an OAuth 2.0
491
+ authorization server's introspection endpoint. Unlike JWT verification, this
492
+ approach works with both opaque tokens and JWTs, and provides real-time
493
+ validation including immediate revocation support.
494
+
495
+ Use this when:
496
+ - Your authorization server is separate from your FastMCP server
497
+ - You're using opaque (non-JWT) tokens
498
+ - You need real-time token validation and revocation support
499
+ - Your authorization server supports RFC 7662 introspection
500
+ - You want centralized token management without sharing secrets
501
+ """
502
+
503
+ def __init__(
504
+ self,
505
+ introspection_endpoint: AnyHttpUrl | str,
506
+ server_url: AnyHttpUrl | str,
507
+ client_id: str | None = None,
508
+ client_secret: str | None = None,
509
+ validate_resource: bool = False,
510
+ required_scopes: list[str] | None = None,
511
+ timeout: float = 10.0,
512
+ ):
513
+ """
514
+ Initialize the introspection token verifier.
515
+
516
+ Args:
517
+ introspection_endpoint: OAuth 2.0 introspection endpoint URL
518
+ server_url: This server's URL for resource validation
519
+ client_id: Client ID for introspection authentication
520
+ client_secret: Client secret for introspection authentication
521
+ validate_resource: Whether to validate RFC 8707 resource parameter
522
+ required_scopes: Required scopes for all tokens
523
+ timeout: HTTP request timeout in seconds
524
+ """
525
+ try:
526
+ self.introspection_endpoint = AnyHttpUrl(introspection_endpoint)
527
+ server_url_validated = AnyHttpUrl(server_url)
528
+ except ValidationError as e:
529
+ raise ValueError(f"Invalid URL provided: {e}") from e
530
+
531
+ # Basic SSRF protection - reject private/localhost URLs
532
+ if self._is_private_url(str(self.introspection_endpoint)):
533
+ raise ValueError("Introspection endpoint cannot be a private/localhost URL")
534
+
535
+ # Initialize parent TokenVerifier with the resource server URL
536
+ super().__init__(
537
+ resource_server_url=server_url_validated, required_scopes=required_scopes
538
+ )
539
+
540
+ self.client_id = client_id
541
+ self.client_secret = client_secret
542
+ self.validate_resource = validate_resource
543
+ self.timeout = timeout
544
+
545
+ # Create HTTP client with security settings
546
+ self._client = httpx.AsyncClient(
547
+ timeout=timeout,
548
+ verify=True, # Always verify SSL
549
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
550
+ )
551
+
552
+ @property
553
+ def server_url(self) -> AnyHttpUrl:
554
+ """The resource server URL for this verifier."""
555
+ if self.resource_server_url is None:
556
+ raise ValueError("Resource server URL not set")
557
+ return self.resource_server_url
558
+
559
+ def _is_private_url(self, url: str) -> bool:
560
+ """Check if URL points to private/localhost addresses (basic SSRF protection)."""
561
+ import ipaddress
562
+ from urllib.parse import urlparse
563
+
564
+ parsed = urlparse(url)
565
+ hostname = parsed.hostname
566
+
567
+ if not hostname:
568
+ return False
569
+
570
+ # Check for localhost
571
+ if hostname.lower() in ("localhost", "127.0.0.1", "::1"):
572
+ return True
573
+
574
+ # Check for private IP ranges
575
+ try:
576
+ ip = ipaddress.ip_address(hostname)
577
+ return ip.is_private or ip.is_loopback
578
+ except ValueError:
579
+ # Not an IP address, assume it's a hostname
580
+ return False
581
+
582
+ async def verify_token(self, token: str) -> AccessToken | None:
583
+ """Verify token using OAuth 2.0 introspection."""
584
+ try:
585
+ # Prepare introspection request
586
+ data = {"token": token}
587
+
588
+ # Add resource parameter if validation is enabled (RFC 8707)
589
+ if self.validate_resource and self.resource_server_url:
590
+ data["resource"] = str(self.resource_server_url)
591
+
592
+ # Prepare authentication and make introspection request
593
+ if self.client_id and self.client_secret:
594
+ response = await self._client.post(
595
+ str(self.introspection_endpoint),
596
+ data=data,
597
+ auth=(self.client_id, self.client_secret),
598
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
599
+ )
600
+ else:
601
+ response = await self._client.post(
602
+ str(self.introspection_endpoint),
603
+ data=data,
604
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
605
+ )
606
+ response.raise_for_status()
607
+
608
+ introspection_response = response.json()
609
+
610
+ # Check if token is active
611
+ if not introspection_response.get("active", False):
612
+ return None
613
+
614
+ # Extract token information
615
+ client_id = introspection_response.get("client_id", "unknown")
616
+ scopes = (
617
+ introspection_response.get("scope", "").split()
618
+ if introspection_response.get("scope")
619
+ else []
620
+ )
621
+ exp = introspection_response.get("exp")
622
+
623
+ # Check required scopes
624
+ if self.required_scopes:
625
+ token_scopes = set(scopes)
626
+ required_scopes = set(self.required_scopes)
627
+ if not required_scopes.issubset(token_scopes):
628
+ logger.debug(
629
+ f"Token missing required scopes. Has: {token_scopes}, Required: {required_scopes}"
630
+ )
631
+ return None
632
+
633
+ return AccessToken(
634
+ token=token,
635
+ client_id=client_id,
636
+ scopes=scopes,
637
+ expires_at=exp,
638
+ resource=str(self.resource_server_url)
639
+ if self.resource_server_url
640
+ else None,
641
+ )
642
+
643
+ except Exception as e:
644
+ logger.debug(f"Introspection verification failed: {e}")
645
+ return None
646
+
647
+ async def __aenter__(self):
648
+ """Async context manager entry."""
649
+ return self
650
+
651
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
652
+ """Async context manager exit."""
653
+ await self._client.aclose()
654
+
655
+
656
+ class StaticTokenVerifier(TokenVerifier):
657
+ """
658
+ Simple static token verifier for testing and development.
659
+
660
+ This verifier validates tokens against a predefined dictionary of valid token
661
+ strings and their associated claims. When a token string matches a key in the
662
+ dictionary, the verifier returns the corresponding claims as if the token was
663
+ validated by a real authorization server.
664
+
665
+ Use this when:
666
+ - You're developing or testing locally without a real OAuth server
667
+ - You need predictable tokens for automated testing
668
+ - You want to simulate different users/scopes without complex setup
669
+ - You're prototyping and need simple API key-style authentication
670
+
671
+ WARNING: Never use this in production - tokens are stored in plain text!
672
+ """
673
+
674
+ def __init__(
675
+ self,
676
+ tokens: dict[str, dict[str, Any]],
677
+ required_scopes: list[str] | None = None,
678
+ ):
679
+ """
680
+ Initialize the static token verifier.
681
+
682
+ Args:
683
+ tokens: Dict mapping token strings to token metadata
684
+ Each token should have: client_id, scopes, expires_at (optional)
685
+ required_scopes: Required scopes for all tokens
686
+ """
687
+ super().__init__(required_scopes=required_scopes)
688
+ self.tokens = tokens
689
+
690
+ async def verify_token(self, token: str) -> AccessToken | None:
691
+ """Verify token against static token dictionary."""
692
+ token_data = self.tokens.get(token)
693
+ if not token_data:
694
+ return None
695
+
696
+ # Check expiration if present
697
+ expires_at = token_data.get("expires_at")
698
+ if expires_at is not None and expires_at < time.time():
699
+ return None
700
+
701
+ scopes = token_data.get("scopes", [])
702
+
703
+ # Check required scopes
704
+ if self.required_scopes:
705
+ token_scopes = set(scopes)
706
+ required_scopes = set(self.required_scopes)
707
+ if not required_scopes.issubset(token_scopes):
708
+ logger.debug(
709
+ f"Token missing required scopes. Has: {token_scopes}, Required: {required_scopes}"
710
+ )
711
+ return None
712
+
713
+ return AccessToken(
714
+ token=token,
715
+ client_id=token_data["client_id"],
716
+ scopes=scopes,
717
+ expires_at=expires_at,
718
+ )
src/fastmcp/server/http.py CHANGED
@@ -3,13 +3,14 @@ from __future__ import annotations
3
  from collections.abc import AsyncGenerator, Callable, Generator
4
  from contextlib import asynccontextmanager, contextmanager
5
  from contextvars import ContextVar
6
- from typing import TYPE_CHECKING
7
 
8
  from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
9
  from mcp.server.auth.middleware.bearer_auth import (
10
  BearerAuthBackend,
11
  RequireAuthMiddleware,
12
  )
 
13
  from mcp.server.auth.routes import create_auth_routes
14
  from mcp.server.lowlevel.server import LifespanResultT
15
  from mcp.server.sse import SseServerTransport
@@ -24,7 +25,7 @@ from starlette.responses import Response
24
  from starlette.routing import BaseRoute, Mount, Route
25
  from starlette.types import Lifespan, Receive, Scope, Send
26
 
27
- from fastmcp.server.auth.auth import OAuthProvider
28
  from fastmcp.utilities.logging import get_logger
29
 
30
  if TYPE_CHECKING:
@@ -71,39 +72,45 @@ class RequestContextMiddleware:
71
 
72
 
73
  def setup_auth_middleware_and_routes(
74
- auth: OAuthProvider,
75
  ) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
76
  """Set up authentication middleware and routes if auth is enabled.
77
 
78
  Args:
79
- auth: The OAuthProvider authorization server provider
80
 
81
  Returns:
82
  Tuple of (middleware, auth_routes, required_scopes)
83
  """
84
- middleware: list[Middleware] = []
85
- auth_routes: list[BaseRoute] = []
86
- required_scopes: list[str] = []
87
-
88
- middleware = [
89
  Middleware(
90
  AuthenticationMiddleware,
91
- backend=BearerAuthBackend(auth),
92
  ),
93
  Middleware(AuthContextMiddleware),
94
  ]
95
 
96
- required_scopes = auth.required_scopes or []
 
97
 
98
- auth_routes.extend(
99
- create_auth_routes(
100
- provider=auth,
101
- issuer_url=auth.issuer_url,
102
- service_documentation_url=auth.service_documentation_url,
103
- client_registration_options=auth.client_registration_options,
104
- revocation_options=auth.revocation_options,
 
 
 
 
 
 
105
  )
106
- )
 
 
 
107
 
108
  return middleware, auth_routes, required_scopes
109
 
@@ -140,7 +147,7 @@ def create_sse_app(
140
  server: FastMCP[LifespanResultT],
141
  message_path: str,
142
  sse_path: str,
143
- auth: OAuthProvider | None = None,
144
  debug: bool = False,
145
  routes: list[BaseRoute] | None = None,
146
  middleware: list[Middleware] | None = None,
@@ -151,7 +158,7 @@ def create_sse_app(
151
  server: The FastMCP server instance
152
  message_path: Path for SSE messages
153
  sse_path: Path for SSE connections
154
- auth: Optional auth provider
155
  debug: Whether to enable debug mode
156
  routes: Optional list of custom routes
157
  middleware: Optional list of middleware
@@ -176,8 +183,6 @@ def create_sse_app(
176
  return Response()
177
 
178
  # Get auth middleware and routes
179
-
180
- # Add SSE routes with or without auth
181
  if auth:
182
  auth_middleware, auth_routes, required_scopes = (
183
  setup_auth_middleware_and_routes(auth)
@@ -185,18 +190,32 @@ def create_sse_app(
185
 
186
  server_routes.extend(auth_routes)
187
  server_middleware.extend(auth_middleware)
 
 
 
 
 
 
 
 
 
 
188
  # Auth is enabled, wrap endpoints with RequireAuthMiddleware
189
  server_routes.append(
190
  Route(
191
  sse_path,
192
- endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
 
 
193
  methods=["GET"],
194
  )
195
  )
196
  server_routes.append(
197
  Mount(
198
  message_path,
199
- app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
 
 
200
  )
201
  )
202
  else:
@@ -244,7 +263,7 @@ def create_streamable_http_app(
244
  server: FastMCP[LifespanResultT],
245
  streamable_http_path: str,
246
  event_store: EventStore | None = None,
247
- auth: OAuthProvider | None = None,
248
  json_response: bool = False,
249
  stateless_http: bool = False,
250
  debug: bool = False,
@@ -257,7 +276,7 @@ def create_streamable_http_app(
257
  server: The FastMCP server instance
258
  streamable_http_path: Path for StreamableHTTP connections
259
  event_store: Optional event store for session management
260
- auth: Optional auth provider
261
  json_response: Whether to use JSON response format
262
  stateless_http: Whether to use stateless mode (new transport per request)
263
  debug: Whether to enable debug mode
@@ -310,7 +329,7 @@ def create_streamable_http_app(
310
  if auth:
311
  resource_metadata_url = None
312
 
313
- if auth.resource_server_url:
314
  resource_metadata_url = AnyHttpUrl(
315
  str(auth.resource_server_url).rstrip("/")
316
  + "/.well-known/oauth-protected-resource"
@@ -323,6 +342,15 @@ def create_streamable_http_app(
323
  server_routes.extend(auth_routes)
324
  server_middleware.extend(auth_middleware)
325
 
 
 
 
 
 
 
 
 
 
326
  # Auth is enabled, wrap endpoint with RequireAuthMiddleware
327
  server_routes.append(
328
  Mount(
 
3
  from collections.abc import AsyncGenerator, Callable, Generator
4
  from contextlib import asynccontextmanager, contextmanager
5
  from contextvars import ContextVar
6
+ from typing import TYPE_CHECKING, cast
7
 
8
  from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
9
  from mcp.server.auth.middleware.bearer_auth import (
10
  BearerAuthBackend,
11
  RequireAuthMiddleware,
12
  )
13
+ from mcp.server.auth.provider import TokenVerifier as TokenVerifierProtocol
14
  from mcp.server.auth.routes import create_auth_routes
15
  from mcp.server.lowlevel.server import LifespanResultT
16
  from mcp.server.sse import SseServerTransport
 
25
  from starlette.routing import BaseRoute, Mount, Route
26
  from starlette.types import Lifespan, Receive, Scope, Send
27
 
28
+ from fastmcp.server.auth.auth import OAuthProvider, TokenVerifier
29
  from fastmcp.utilities.logging import get_logger
30
 
31
  if TYPE_CHECKING:
 
72
 
73
 
74
  def setup_auth_middleware_and_routes(
75
+ auth: OAuthProvider | TokenVerifier,
76
  ) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
77
  """Set up authentication middleware and routes if auth is enabled.
78
 
79
  Args:
80
+ auth: Either an OAuthProvider or TokenVerifier for authentication
81
 
82
  Returns:
83
  Tuple of (middleware, auth_routes, required_scopes)
84
  """
85
+ middleware: list[Middleware] = [
 
 
 
 
86
  Middleware(
87
  AuthenticationMiddleware,
88
+ backend=BearerAuthBackend(cast(TokenVerifierProtocol, auth)),
89
  ),
90
  Middleware(AuthContextMiddleware),
91
  ]
92
 
93
+ auth_routes: list[BaseRoute] = []
94
+ required_scopes: list[str] = []
95
 
96
+ # Handle TokenVerifier vs OAuthProvider
97
+ # Check if it's an OAuthProvider by looking for issuer_url attribute
98
+ if hasattr(auth, "issuer_url"):
99
+ # OAuthProvider: create auth routes and get required scopes
100
+ # We know this is an OAuthProvider because it has issuer_url
101
+ auth_routes = list(
102
+ create_auth_routes(
103
+ provider=auth, # type: ignore[arg-type]
104
+ issuer_url=auth.issuer_url, # type: ignore[attr-defined]
105
+ service_documentation_url=auth.service_documentation_url, # type: ignore[attr-defined]
106
+ client_registration_options=auth.client_registration_options, # type: ignore[attr-defined]
107
+ revocation_options=auth.revocation_options, # type: ignore[attr-defined]
108
+ )
109
  )
110
+ required_scopes = auth.required_scopes or [] # type: ignore[attr-defined]
111
+ else:
112
+ # TokenVerifier: no auth routes but may have required scopes
113
+ required_scopes = getattr(auth, "required_scopes", None) or []
114
 
115
  return middleware, auth_routes, required_scopes
116
 
 
147
  server: FastMCP[LifespanResultT],
148
  message_path: str,
149
  sse_path: str,
150
+ auth: OAuthProvider | TokenVerifier | None = None,
151
  debug: bool = False,
152
  routes: list[BaseRoute] | None = None,
153
  middleware: list[Middleware] | None = None,
 
158
  server: The FastMCP server instance
159
  message_path: Path for SSE messages
160
  sse_path: Path for SSE connections
161
+ auth: Optional authentication provider (OAuthProvider or TokenVerifier)
162
  debug: Whether to enable debug mode
163
  routes: Optional list of custom routes
164
  middleware: Optional list of middleware
 
183
  return Response()
184
 
185
  # Get auth middleware and routes
 
 
186
  if auth:
187
  auth_middleware, auth_routes, required_scopes = (
188
  setup_auth_middleware_and_routes(auth)
 
190
 
191
  server_routes.extend(auth_routes)
192
  server_middleware.extend(auth_middleware)
193
+
194
+ # Determine resource_metadata_url for TokenVerifier
195
+ resource_metadata_url = None
196
+ if isinstance(auth, TokenVerifier) and auth.resource_server_url:
197
+ # Add .well-known path for RFC 9728 compliance
198
+ resource_metadata_url = AnyHttpUrl(
199
+ str(auth.resource_server_url).rstrip("/")
200
+ + "/.well-known/oauth-protected-resource"
201
+ )
202
+
203
  # Auth is enabled, wrap endpoints with RequireAuthMiddleware
204
  server_routes.append(
205
  Route(
206
  sse_path,
207
+ endpoint=RequireAuthMiddleware(
208
+ handle_sse, required_scopes, resource_metadata_url
209
+ ),
210
  methods=["GET"],
211
  )
212
  )
213
  server_routes.append(
214
  Mount(
215
  message_path,
216
+ app=RequireAuthMiddleware(
217
+ sse.handle_post_message, required_scopes, resource_metadata_url
218
+ ),
219
  )
220
  )
221
  else:
 
263
  server: FastMCP[LifespanResultT],
264
  streamable_http_path: str,
265
  event_store: EventStore | None = None,
266
+ auth: OAuthProvider | TokenVerifier | None = None,
267
  json_response: bool = False,
268
  stateless_http: bool = False,
269
  debug: bool = False,
 
276
  server: The FastMCP server instance
277
  streamable_http_path: Path for StreamableHTTP connections
278
  event_store: Optional event store for session management
279
+ auth: Optional authentication provider (OAuthProvider or TokenVerifier)
280
  json_response: Whether to use JSON response format
281
  stateless_http: Whether to use stateless mode (new transport per request)
282
  debug: Whether to enable debug mode
 
329
  if auth:
330
  resource_metadata_url = None
331
 
332
+ if isinstance(auth, TokenVerifier) and auth.resource_server_url:
333
  resource_metadata_url = AnyHttpUrl(
334
  str(auth.resource_server_url).rstrip("/")
335
  + "/.well-known/oauth-protected-resource"
 
342
  server_routes.extend(auth_routes)
343
  server_middleware.extend(auth_middleware)
344
 
345
+ # Determine resource_metadata_url for TokenVerifier
346
+ resource_metadata_url = None
347
+ if isinstance(auth, TokenVerifier) and auth.resource_server_url:
348
+ # Add .well-known path for RFC 9728 compliance
349
+ resource_metadata_url = AnyHttpUrl(
350
+ str(auth.resource_server_url).rstrip("/")
351
+ + "/.well-known/oauth-protected-resource"
352
+ )
353
+
354
  # Auth is enabled, wrap endpoint with RequireAuthMiddleware
355
  server_routes.append(
356
  Mount(
src/fastmcp/server/server.py CHANGED
@@ -50,8 +50,8 @@ from fastmcp.prompts import Prompt, PromptManager
50
  from fastmcp.prompts.prompt import FunctionPrompt
51
  from fastmcp.resources import Resource, ResourceManager
52
  from fastmcp.resources.template import ResourceTemplate
53
- from fastmcp.server.auth.auth import OAuthProvider
54
- from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
55
  from fastmcp.server.http import (
56
  StarletteWithLifespan,
57
  create_sse_app,
@@ -133,7 +133,7 @@ class FastMCP(Generic[LifespanResultT]):
133
  instructions: str | None = None,
134
  *,
135
  version: str | None = None,
136
- auth: OAuthProvider | None = None,
137
  middleware: list[Middleware] | None = None,
138
  lifespan: (
139
  Callable[
@@ -205,8 +205,9 @@ class FastMCP(Generic[LifespanResultT]):
205
  lifespan=_lifespan_wrapper(self, lifespan),
206
  )
207
 
208
- if auth is None and fastmcp.settings.default_auth_provider == "bearer_env":
209
- auth = EnvBearerAuthProvider()
 
210
  self.auth = auth
211
 
212
  if tools:
 
50
  from fastmcp.prompts.prompt import FunctionPrompt
51
  from fastmcp.resources import Resource, ResourceManager
52
  from fastmcp.resources.template import ResourceTemplate
53
+ from fastmcp.server.auth.auth import OAuthProvider, TokenVerifier
54
+ from fastmcp.server.auth.verifiers import EnvJWTVerifier
55
  from fastmcp.server.http import (
56
  StarletteWithLifespan,
57
  create_sse_app,
 
133
  instructions: str | None = None,
134
  *,
135
  version: str | None = None,
136
+ auth: OAuthProvider | TokenVerifier | None = None,
137
  middleware: list[Middleware] | None = None,
138
  lifespan: (
139
  Callable[
 
205
  lifespan=_lifespan_wrapper(self, lifespan),
206
  )
207
 
208
+ if auth is None and fastmcp.settings.default_auth_provider == "jwt-env":
209
+ auth = EnvJWTVerifier()
210
+
211
  self.auth = auth
212
 
213
  if tools:
src/fastmcp/settings.py CHANGED
@@ -260,7 +260,7 @@ class Settings(BaseSettings):
260
 
261
  # Auth settings
262
  default_auth_provider: Annotated[
263
- Literal["bearer_env"] | None,
264
  Field(
265
  description=inspect.cleandoc(
266
  """
 
260
 
261
  # Auth settings
262
  default_auth_provider: Annotated[
263
+ Literal["jwt-env"] | None,
264
  Field(
265
  description=inspect.cleandoc(
266
  """
tests/auth/providers/test_token_verifier.py CHANGED
@@ -3,12 +3,12 @@
3
  import pytest
4
  from mcp.server.auth.provider import AccessToken
5
 
6
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
7
  from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
 
8
 
9
 
10
- class TestBearerAuthProviderTokenVerifier:
11
- """Test that BearerAuthProvider implements TokenVerifier protocol correctly."""
12
 
13
  @pytest.fixture
14
  def rsa_key_pair(self) -> RSAKeyPair:
@@ -16,9 +16,9 @@ class TestBearerAuthProviderTokenVerifier:
16
  return RSAKeyPair.generate()
17
 
18
  @pytest.fixture
19
- def bearer_provider(self, rsa_key_pair: RSAKeyPair) -> BearerAuthProvider:
20
- """Create BearerAuthProvider for testing."""
21
- return BearerAuthProvider(
22
  public_key=rsa_key_pair.public_key,
23
  issuer="https://test.example.com",
24
  audience="https://api.example.com",
@@ -45,10 +45,10 @@ class TestBearerAuthProviderTokenVerifier:
45
  )
46
 
47
  async def test_verify_token_with_valid_token(
48
- self, bearer_provider: BearerAuthProvider, valid_token: str
49
  ):
50
  """Test that verify_token returns AccessToken for valid token."""
51
- result = await bearer_provider.verify_token(valid_token)
52
 
53
  assert result is not None
54
  assert isinstance(result, AccessToken)
@@ -58,33 +58,29 @@ class TestBearerAuthProviderTokenVerifier:
58
  assert "write" in result.scopes
59
 
60
  async def test_verify_token_with_expired_token(
61
- self, bearer_provider: BearerAuthProvider, expired_token: str
62
  ):
63
  """Test that verify_token returns None for expired token."""
64
- result = await bearer_provider.verify_token(expired_token)
65
  assert result is None
66
 
67
- async def test_verify_token_with_invalid_token(
68
- self, bearer_provider: BearerAuthProvider
69
- ):
70
  """Test that verify_token returns None for invalid token."""
71
- result = await bearer_provider.verify_token("invalid.token.here")
72
  assert result is None
73
 
74
- async def test_verify_token_with_malformed_token(
75
- self, bearer_provider: BearerAuthProvider
76
- ):
77
  """Test that verify_token returns None for malformed token."""
78
- result = await bearer_provider.verify_token("not-a-jwt")
79
  assert result is None
80
 
81
  async def test_verify_token_delegation_to_load_access_token(
82
- self, bearer_provider: BearerAuthProvider, valid_token: str
83
  ):
84
  """Test that verify_token delegates to load_access_token."""
85
  # Both methods should return the same result
86
- verify_result = await bearer_provider.verify_token(valid_token)
87
- load_result = await bearer_provider.load_access_token(valid_token)
88
 
89
  assert verify_result == load_result
90
  if verify_result is not None and load_result is not None:
@@ -162,9 +158,9 @@ class TestTokenVerifierProtocolCompliance:
162
  """Test that our providers properly implement the TokenVerifier protocol."""
163
 
164
  async def test_bearer_provider_implements_protocol(self):
165
- """Test that BearerAuthProvider can be used as TokenVerifier."""
166
  key_pair = RSAKeyPair.generate()
167
- provider = BearerAuthProvider(public_key=key_pair.public_key)
168
 
169
  # Should have the required method for TokenVerifier protocol
170
  assert hasattr(provider, "verify_token")
 
3
  import pytest
4
  from mcp.server.auth.provider import AccessToken
5
 
 
6
  from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
7
+ from fastmcp.server.auth.verifiers import JWTVerifier, RSAKeyPair
8
 
9
 
10
+ class TestJWTVerifierTokenVerifier:
11
+ """Test that JWTVerifier implements TokenVerifier protocol correctly."""
12
 
13
  @pytest.fixture
14
  def rsa_key_pair(self) -> RSAKeyPair:
 
16
  return RSAKeyPair.generate()
17
 
18
  @pytest.fixture
19
+ def jwt_verifier(self, rsa_key_pair: RSAKeyPair) -> JWTVerifier:
20
+ """Create JWTVerifier for testing."""
21
+ return JWTVerifier(
22
  public_key=rsa_key_pair.public_key,
23
  issuer="https://test.example.com",
24
  audience="https://api.example.com",
 
45
  )
46
 
47
  async def test_verify_token_with_valid_token(
48
+ self, jwt_verifier: JWTVerifier, valid_token: str
49
  ):
50
  """Test that verify_token returns AccessToken for valid token."""
51
+ result = await jwt_verifier.verify_token(valid_token)
52
 
53
  assert result is not None
54
  assert isinstance(result, AccessToken)
 
58
  assert "write" in result.scopes
59
 
60
  async def test_verify_token_with_expired_token(
61
+ self, jwt_verifier: JWTVerifier, expired_token: str
62
  ):
63
  """Test that verify_token returns None for expired token."""
64
+ result = await jwt_verifier.verify_token(expired_token)
65
  assert result is None
66
 
67
+ async def test_verify_token_with_invalid_token(self, jwt_verifier: JWTVerifier):
 
 
68
  """Test that verify_token returns None for invalid token."""
69
+ result = await jwt_verifier.verify_token("invalid.token.here")
70
  assert result is None
71
 
72
+ async def test_verify_token_with_malformed_token(self, jwt_verifier: JWTVerifier):
 
 
73
  """Test that verify_token returns None for malformed token."""
74
+ result = await jwt_verifier.verify_token("not-a-jwt")
75
  assert result is None
76
 
77
  async def test_verify_token_delegation_to_load_access_token(
78
+ self, jwt_verifier: JWTVerifier, valid_token: str
79
  ):
80
  """Test that verify_token delegates to load_access_token."""
81
  # Both methods should return the same result
82
+ verify_result = await jwt_verifier.verify_token(valid_token)
83
+ load_result = await jwt_verifier.load_access_token(valid_token)
84
 
85
  assert verify_result == load_result
86
  if verify_result is not None and load_result is not None:
 
158
  """Test that our providers properly implement the TokenVerifier protocol."""
159
 
160
  async def test_bearer_provider_implements_protocol(self):
161
+ """Test that JWTVerifier can be used as TokenVerifier."""
162
  key_pair = RSAKeyPair.generate()
163
+ provider = JWTVerifier(public_key=key_pair.public_key)
164
 
165
  # Should have the required method for TokenVerifier protocol
166
  assert hasattr(provider, "verify_token")
tests/auth/{providers/test_bearer_env.py → verifiers/test_env_jwt.py} RENAMED
@@ -1,9 +1,8 @@
1
  import pytest
2
- from pydantic import AnyHttpUrl, ValidationError
3
 
4
  from fastmcp import FastMCP
5
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider
6
- from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
7
  from fastmcp.settings import Settings
8
  from fastmcp.utilities.tests import temporary_settings
9
 
@@ -12,19 +11,19 @@ def test_load_bearer_env_from_env_var(monkeypatch):
12
  mcp = FastMCP()
13
  assert mcp.auth is None
14
 
15
- monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
16
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
17
 
18
  with temporary_settings(**Settings().model_dump()):
19
  mcp_with_auth = FastMCP()
20
- assert isinstance(mcp_with_auth.auth, EnvBearerAuthProvider)
21
 
22
 
23
  def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatch):
24
  mcp = FastMCP()
25
  assert mcp.auth is None
26
 
27
- monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
28
 
29
  with temporary_settings(**Settings().model_dump()):
30
  with pytest.raises(
@@ -34,26 +33,25 @@ def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatc
34
 
35
 
36
  def test_configure_bearer_env_from_env_var(monkeypatch):
37
- monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
38
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
39
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_ISSUER", "http://test-issuer")
40
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_AUDIENCE", "test-audience")
41
  monkeypatch.setenv(
42
- "FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]'
43
  )
44
 
45
  with temporary_settings(**Settings().model_dump()):
46
  mcp = FastMCP()
47
- assert isinstance(mcp.auth, EnvBearerAuthProvider)
48
  assert mcp.auth.public_key == "test-public-key"
49
- assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer")
50
  assert mcp.auth.audience == "test-audience"
51
  assert mcp.auth.required_scopes == ["test-scope1", "test-scope2"]
52
 
53
 
54
  def test_list_of_scopes_must_be_a_list(monkeypatch):
55
- monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
56
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
57
 
58
  with temporary_settings(**Settings().model_dump()):
59
  with pytest.raises(ValidationError, match="Input should be a valid list"):
@@ -61,19 +59,19 @@ def test_list_of_scopes_must_be_a_list(monkeypatch):
61
 
62
 
63
  def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
64
- monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
65
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
66
 
67
  with temporary_settings(**Settings().model_dump()):
68
  mcp = FastMCP()
69
- assert isinstance(mcp.auth, EnvBearerAuthProvider)
70
  assert mcp.auth.jwks_uri == "test-jwks-uri"
71
 
72
 
73
  def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
74
- monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
75
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
76
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
77
 
78
  with temporary_settings(**Settings().model_dump()):
79
  with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"):
@@ -81,11 +79,11 @@ def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
81
 
82
 
83
  def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
84
- monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
85
- monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
86
 
87
  with temporary_settings(**Settings().model_dump()):
88
- mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))
89
- assert isinstance(mcp.auth, BearerAuthProvider)
90
- assert not isinstance(mcp.auth, EnvBearerAuthProvider)
91
  assert mcp.auth.public_key == "test-public-key-2"
 
1
  import pytest
2
+ from pydantic import ValidationError
3
 
4
  from fastmcp import FastMCP
5
+ from fastmcp.server.auth.verifiers import EnvJWTVerifier, JWTVerifier
 
6
  from fastmcp.settings import Settings
7
  from fastmcp.utilities.tests import temporary_settings
8
 
 
11
  mcp = FastMCP()
12
  assert mcp.auth is None
13
 
14
+ monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "jwt-env")
15
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_PUBLIC_KEY", "test-public-key")
16
 
17
  with temporary_settings(**Settings().model_dump()):
18
  mcp_with_auth = FastMCP()
19
+ assert isinstance(mcp_with_auth.auth, EnvJWTVerifier)
20
 
21
 
22
  def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatch):
23
  mcp = FastMCP()
24
  assert mcp.auth is None
25
 
26
+ monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "jwt-env")
27
 
28
  with temporary_settings(**Settings().model_dump()):
29
  with pytest.raises(
 
33
 
34
 
35
  def test_configure_bearer_env_from_env_var(monkeypatch):
36
+ monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "jwt-env")
37
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_PUBLIC_KEY", "test-public-key")
38
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_ISSUER", "http://test-issuer")
39
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_AUDIENCE", "test-audience")
40
  monkeypatch.setenv(
41
+ "FASTMCP_AUTH_JWT_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]'
42
  )
43
 
44
  with temporary_settings(**Settings().model_dump()):
45
  mcp = FastMCP()
46
+ assert isinstance(mcp.auth, EnvJWTVerifier)
47
  assert mcp.auth.public_key == "test-public-key"
 
48
  assert mcp.auth.audience == "test-audience"
49
  assert mcp.auth.required_scopes == ["test-scope1", "test-scope2"]
50
 
51
 
52
  def test_list_of_scopes_must_be_a_list(monkeypatch):
53
+ monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "jwt-env")
54
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_REQUIRED_SCOPES", "test-scope1")
55
 
56
  with temporary_settings(**Settings().model_dump()):
57
  with pytest.raises(ValidationError, match="Input should be a valid list"):
 
59
 
60
 
61
  def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
62
+ monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "jwt-env")
63
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_JWKS_URI", "test-jwks-uri")
64
 
65
  with temporary_settings(**Settings().model_dump()):
66
  mcp = FastMCP()
67
+ assert isinstance(mcp.auth, EnvJWTVerifier)
68
  assert mcp.auth.jwks_uri == "test-jwks-uri"
69
 
70
 
71
  def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
72
+ monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "jwt-env")
73
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_PUBLIC_KEY", "test-public-key")
74
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_JWKS_URI", "test-jwks-uri")
75
 
76
  with temporary_settings(**Settings().model_dump()):
77
  with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"):
 
79
 
80
 
81
  def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
82
+ monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "jwt-env")
83
+ monkeypatch.setenv("FASTMCP_AUTH_JWT_PUBLIC_KEY", "test-public-key")
84
 
85
  with temporary_settings(**Settings().model_dump()):
86
+ mcp = FastMCP(auth=JWTVerifier(public_key="test-public-key-2"))
87
+ assert isinstance(mcp.auth, JWTVerifier)
88
+ assert not isinstance(mcp.auth, EnvJWTVerifier)
89
  assert mcp.auth.public_key == "test-public-key-2"
tests/auth/{providers/test_bearer.py → verifiers/test_jwt_verifier.py} RENAMED
@@ -7,12 +7,7 @@ from pytest_httpx import HTTPXMock
7
 
8
  from fastmcp import Client, FastMCP
9
  from fastmcp.client.auth.bearer import BearerAuth
10
- from fastmcp.server.auth.providers.bearer import (
11
- BearerAuthProvider,
12
- JWKData,
13
- JWKSData,
14
- RSAKeyPair,
15
- )
16
  from fastmcp.utilities.tests import run_server_in_process
17
 
18
 
@@ -31,8 +26,8 @@ def bearer_token(rsa_key_pair: RSAKeyPair) -> str:
31
 
32
 
33
  @pytest.fixture
34
- def bearer_provider(rsa_key_pair: RSAKeyPair) -> BearerAuthProvider:
35
- return BearerAuthProvider(
36
  public_key=rsa_key_pair.public_key,
37
  issuer="https://test.example.com",
38
  audience="https://api.example.com",
@@ -47,7 +42,7 @@ def run_mcp_server(
47
  run_kwargs: dict[str, Any] | None = None,
48
  ) -> None:
49
  mcp = FastMCP(
50
- auth=BearerAuthProvider(
51
  public_key=public_key,
52
  **auth_kwargs or {},
53
  )
@@ -113,9 +108,9 @@ class TestBearerTokenJWKS:
113
  """Tests for JWKS URI functionality."""
114
 
115
  @pytest.fixture
116
- def jwks_provider(self, rsa_key_pair: RSAKeyPair) -> BearerAuthProvider:
117
  """Provider configured with JWKS URI."""
118
- return BearerAuthProvider(
119
  jwks_uri="https://test.example.com/.well-known/jwks.json",
120
  issuer="https://test.example.com",
121
  audience="https://api.example.com",
@@ -137,7 +132,7 @@ class TestBearerTokenJWKS:
137
  async def test_jwks_token_validation(
138
  self,
139
  rsa_key_pair: RSAKeyPair,
140
- jwks_provider: BearerAuthProvider,
141
  mock_jwks_data: JWKSData,
142
  httpx_mock: HTTPXMock,
143
  ):
@@ -159,7 +154,7 @@ class TestBearerTokenJWKS:
159
  async def test_jwks_token_validation_with_invalid_key(
160
  self,
161
  rsa_key_pair: RSAKeyPair,
162
- jwks_provider: BearerAuthProvider,
163
  mock_jwks_data: JWKSData,
164
  httpx_mock: HTTPXMock,
165
  ):
@@ -179,7 +174,7 @@ class TestBearerTokenJWKS:
179
  async def test_jwks_token_validation_with_kid(
180
  self,
181
  rsa_key_pair: RSAKeyPair,
182
- jwks_provider: BearerAuthProvider,
183
  mock_jwks_data: JWKSData,
184
  httpx_mock: HTTPXMock,
185
  ):
@@ -202,7 +197,7 @@ class TestBearerTokenJWKS:
202
  async def test_jwks_token_validation_with_kid_and_no_kid_in_token(
203
  self,
204
  rsa_key_pair: RSAKeyPair,
205
- jwks_provider: BearerAuthProvider,
206
  mock_jwks_data: JWKSData,
207
  httpx_mock: HTTPXMock,
208
  ):
@@ -224,7 +219,7 @@ class TestBearerTokenJWKS:
224
  async def test_jwks_token_validation_with_no_kid_and_kid_in_jwks(
225
  self,
226
  rsa_key_pair: RSAKeyPair,
227
- jwks_provider: BearerAuthProvider,
228
  mock_jwks_data: JWKSData,
229
  httpx_mock: HTTPXMock,
230
  ):
@@ -246,7 +241,7 @@ class TestBearerTokenJWKS:
246
  async def test_jwks_token_validation_with_kid_mismatch(
247
  self,
248
  rsa_key_pair: RSAKeyPair,
249
- jwks_provider: BearerAuthProvider,
250
  mock_jwks_data: JWKSData,
251
  httpx_mock: HTTPXMock,
252
  ):
@@ -268,7 +263,7 @@ class TestBearerTokenJWKS:
268
  async def test_jwks_token_validation_with_multiple_keys_and_no_kid_in_token(
269
  self,
270
  rsa_key_pair: RSAKeyPair,
271
- jwks_provider: BearerAuthProvider,
272
  mock_jwks_data: JWKSData,
273
  httpx_mock: HTTPXMock,
274
  ):
@@ -300,7 +295,7 @@ class TestBearerTokenJWKS:
300
  class TestBearerToken:
301
  def test_initialization_with_public_key(self, rsa_key_pair: RSAKeyPair):
302
  """Test provider initialization with public key."""
303
- provider = BearerAuthProvider(
304
  public_key=rsa_key_pair.public_key, issuer="https://test.example.com"
305
  )
306
 
@@ -310,7 +305,7 @@ class TestBearerToken:
310
 
311
  def test_initialization_with_jwks_uri(self):
312
  """Test provider initialization with JWKS URI."""
313
- provider = BearerAuthProvider(
314
  jwks_uri="https://test.example.com/.well-known/jwks.json",
315
  issuer="https://test.example.com",
316
  )
@@ -324,21 +319,21 @@ class TestBearerToken:
324
  with pytest.raises(
325
  ValueError, match="Either public_key or jwks_uri must be provided"
326
  ):
327
- BearerAuthProvider(issuer="https://test.example.com")
328
 
329
  def test_initialization_rejects_both_key_and_uri(self, rsa_key_pair: RSAKeyPair):
330
  """Test that both public_key and jwks_uri cannot be provided."""
331
  with pytest.raises(
332
  ValueError, match="Provide either public_key or jwks_uri, not both"
333
  ):
334
- BearerAuthProvider(
335
  public_key=rsa_key_pair.public_key,
336
  jwks_uri="https://test.example.com/.well-known/jwks.json",
337
  issuer="https://test.example.com",
338
  )
339
 
340
  async def test_valid_token_validation(
341
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
342
  ):
343
  """Test validation of a valid token."""
344
  token = rsa_key_pair.create_token(
@@ -357,7 +352,7 @@ class TestBearerToken:
357
  assert access_token.expires_at is not None
358
 
359
  async def test_expired_token_rejection(
360
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
361
  ):
362
  """Test rejection of expired tokens."""
363
  token = rsa_key_pair.create_token(
@@ -371,7 +366,7 @@ class TestBearerToken:
371
  assert access_token is None
372
 
373
  async def test_invalid_issuer_rejection(
374
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
375
  ):
376
  """Test rejection of tokens with invalid issuer."""
377
  token = rsa_key_pair.create_token(
@@ -384,7 +379,7 @@ class TestBearerToken:
384
  assert access_token is None
385
 
386
  async def test_invalid_audience_rejection(
387
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
388
  ):
389
  """Test rejection of tokens with invalid audience."""
390
  token = rsa_key_pair.create_token(
@@ -398,7 +393,7 @@ class TestBearerToken:
398
 
399
  async def test_no_issuer_validation_when_none(self, rsa_key_pair: RSAKeyPair):
400
  """Test that issuer validation is skipped when provider has no issuer configured."""
401
- provider = BearerAuthProvider(
402
  public_key=rsa_key_pair.public_key,
403
  issuer=None, # No issuer validation
404
  )
@@ -412,7 +407,7 @@ class TestBearerToken:
412
 
413
  async def test_no_audience_validation_when_none(self, rsa_key_pair: RSAKeyPair):
414
  """Test that audience validation is skipped when provider has no audience configured."""
415
- provider = BearerAuthProvider(
416
  public_key=rsa_key_pair.public_key,
417
  issuer="https://test.example.com",
418
  audience=None, # No audience validation
@@ -429,7 +424,7 @@ class TestBearerToken:
429
 
430
  async def test_multiple_audiences_validation(self, rsa_key_pair: RSAKeyPair):
431
  """Test validation with multiple audiences in token."""
432
- provider = BearerAuthProvider(
433
  public_key=rsa_key_pair.public_key,
434
  issuer="https://test.example.com",
435
  audience="https://api.example.com",
@@ -450,7 +445,7 @@ class TestBearerToken:
450
  self, rsa_key_pair: RSAKeyPair
451
  ):
452
  """Test provider configured with multiple expected audiences."""
453
- provider = BearerAuthProvider(
454
  public_key=rsa_key_pair.public_key,
455
  issuer="https://test.example.com",
456
  audience=["https://api.example.com", "https://other-api.example.com"],
@@ -486,7 +481,7 @@ class TestBearerToken:
486
  assert access_token3 is None
487
 
488
  async def test_scope_extraction_string(
489
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
490
  ):
491
  """Test scope extraction from space-separated string."""
492
  token = rsa_key_pair.create_token(
@@ -502,7 +497,7 @@ class TestBearerToken:
502
  assert set(access_token.scopes) == {"read", "write", "admin"}
503
 
504
  async def test_scope_extraction_list(
505
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
506
  ):
507
  """Test scope extraction from list format."""
508
  token = rsa_key_pair.create_token(
@@ -518,7 +513,7 @@ class TestBearerToken:
518
  assert set(access_token.scopes) == {"read", "write"}
519
 
520
  async def test_no_scopes(
521
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
522
  ):
523
  """Test token with no scopes."""
524
  token = rsa_key_pair.create_token(
@@ -534,7 +529,7 @@ class TestBearerToken:
534
  assert access_token.scopes == []
535
 
536
  async def test_scp_claim_extraction_string(
537
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
538
  ):
539
  """Test scope extraction from 'scp' claim with space-separated string."""
540
  token = rsa_key_pair.create_token(
@@ -550,7 +545,7 @@ class TestBearerToken:
550
  assert set(access_token.scopes) == {"read", "write", "admin"}
551
 
552
  async def test_scp_claim_extraction_list(
553
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
554
  ):
555
  """Test scope extraction from 'scp' claim with list format."""
556
  token = rsa_key_pair.create_token(
@@ -568,7 +563,7 @@ class TestBearerToken:
568
  assert set(access_token.scopes) == {"read", "write", "admin"}
569
 
570
  async def test_scope_precedence_over_scp(
571
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
572
  ):
573
  """Test that 'scope' claim takes precedence over 'scp' claim when both are present."""
574
  token = rsa_key_pair.create_token(
@@ -586,7 +581,7 @@ class TestBearerToken:
586
  assert access_token is not None
587
  assert set(access_token.scopes) == {"read", "write"} # Only 'scope' claim used
588
 
589
- async def test_malformed_token_rejection(self, bearer_provider: BearerAuthProvider):
590
  """Test rejection of malformed tokens."""
591
  malformed_tokens = [
592
  "not.a.jwt",
@@ -601,7 +596,7 @@ class TestBearerToken:
601
  assert access_token is None
602
 
603
  async def test_invalid_signature_rejection(
604
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
605
  ):
606
  """Test rejection of tokens with invalid signatures."""
607
  # Create a token with a different key pair
@@ -616,7 +611,7 @@ class TestBearerToken:
616
  assert access_token is None
617
 
618
  async def test_client_id_fallback(
619
- self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
620
  ):
621
  """Test client_id extraction with fallback logic."""
622
  # Test with explicit client_id claim
@@ -634,7 +629,7 @@ class TestBearerToken:
634
  async def test_string_issuer_validation(self, rsa_key_pair: RSAKeyPair):
635
  """Test that string (non-URL) issuers are supported per RFC 7519."""
636
  # Create provider with string issuer
637
- provider = BearerAuthProvider(
638
  public_key=rsa_key_pair.public_key,
639
  issuer="my-service", # String issuer, not a URL
640
  )
@@ -652,7 +647,7 @@ class TestBearerToken:
652
  async def test_string_issuer_mismatch_rejection(self, rsa_key_pair: RSAKeyPair):
653
  """Test that mismatched string issuers are rejected."""
654
  # Create provider with one string issuer
655
- provider = BearerAuthProvider(
656
  public_key=rsa_key_pair.public_key,
657
  issuer="my-service",
658
  )
@@ -669,7 +664,7 @@ class TestBearerToken:
669
  async def test_url_issuer_still_works(self, rsa_key_pair: RSAKeyPair):
670
  """Test that URL issuers still work after the fix."""
671
  # Create provider with URL issuer
672
- provider = BearerAuthProvider(
673
  public_key=rsa_key_pair.public_key,
674
  issuer="https://my-auth-server.com", # URL issuer
675
  )
@@ -688,9 +683,9 @@ class TestBearerToken:
688
  class TestFastMCPBearerAuth:
689
  def test_bearer_auth(self):
690
  mcp = FastMCP(
691
- auth=BearerAuthProvider(issuer="https://test.example.com", public_key="abc")
692
  )
693
- assert isinstance(mcp.auth, BearerAuthProvider)
694
 
695
  async def test_unauthorized_access(self, mcp_server_url: str):
696
  with pytest.raises(httpx.HTTPStatusError) as exc_info:
@@ -755,7 +750,10 @@ class TestFastMCPBearerAuth:
755
  with pytest.raises(httpx.HTTPStatusError) as exc_info:
756
  async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
757
  tools = await client.list_tools() # noqa: F841
758
- assert exc_info.value.response.status_code == 403
 
 
 
759
  assert "tools" not in locals()
760
 
761
  async def test_token_with_sufficient_scopes(
 
7
 
8
  from fastmcp import Client, FastMCP
9
  from fastmcp.client.auth.bearer import BearerAuth
10
+ from fastmcp.server.auth.verifiers import JWKData, JWKSData, JWTVerifier, RSAKeyPair
 
 
 
 
 
11
  from fastmcp.utilities.tests import run_server_in_process
12
 
13
 
 
26
 
27
 
28
  @pytest.fixture
29
+ def bearer_provider(rsa_key_pair: RSAKeyPair) -> JWTVerifier:
30
+ return JWTVerifier(
31
  public_key=rsa_key_pair.public_key,
32
  issuer="https://test.example.com",
33
  audience="https://api.example.com",
 
42
  run_kwargs: dict[str, Any] | None = None,
43
  ) -> None:
44
  mcp = FastMCP(
45
+ auth=JWTVerifier(
46
  public_key=public_key,
47
  **auth_kwargs or {},
48
  )
 
108
  """Tests for JWKS URI functionality."""
109
 
110
  @pytest.fixture
111
+ def jwks_provider(self, rsa_key_pair: RSAKeyPair) -> JWTVerifier:
112
  """Provider configured with JWKS URI."""
113
+ return JWTVerifier(
114
  jwks_uri="https://test.example.com/.well-known/jwks.json",
115
  issuer="https://test.example.com",
116
  audience="https://api.example.com",
 
132
  async def test_jwks_token_validation(
133
  self,
134
  rsa_key_pair: RSAKeyPair,
135
+ jwks_provider: JWTVerifier,
136
  mock_jwks_data: JWKSData,
137
  httpx_mock: HTTPXMock,
138
  ):
 
154
  async def test_jwks_token_validation_with_invalid_key(
155
  self,
156
  rsa_key_pair: RSAKeyPair,
157
+ jwks_provider: JWTVerifier,
158
  mock_jwks_data: JWKSData,
159
  httpx_mock: HTTPXMock,
160
  ):
 
174
  async def test_jwks_token_validation_with_kid(
175
  self,
176
  rsa_key_pair: RSAKeyPair,
177
+ jwks_provider: JWTVerifier,
178
  mock_jwks_data: JWKSData,
179
  httpx_mock: HTTPXMock,
180
  ):
 
197
  async def test_jwks_token_validation_with_kid_and_no_kid_in_token(
198
  self,
199
  rsa_key_pair: RSAKeyPair,
200
+ jwks_provider: JWTVerifier,
201
  mock_jwks_data: JWKSData,
202
  httpx_mock: HTTPXMock,
203
  ):
 
219
  async def test_jwks_token_validation_with_no_kid_and_kid_in_jwks(
220
  self,
221
  rsa_key_pair: RSAKeyPair,
222
+ jwks_provider: JWTVerifier,
223
  mock_jwks_data: JWKSData,
224
  httpx_mock: HTTPXMock,
225
  ):
 
241
  async def test_jwks_token_validation_with_kid_mismatch(
242
  self,
243
  rsa_key_pair: RSAKeyPair,
244
+ jwks_provider: JWTVerifier,
245
  mock_jwks_data: JWKSData,
246
  httpx_mock: HTTPXMock,
247
  ):
 
263
  async def test_jwks_token_validation_with_multiple_keys_and_no_kid_in_token(
264
  self,
265
  rsa_key_pair: RSAKeyPair,
266
+ jwks_provider: JWTVerifier,
267
  mock_jwks_data: JWKSData,
268
  httpx_mock: HTTPXMock,
269
  ):
 
295
  class TestBearerToken:
296
  def test_initialization_with_public_key(self, rsa_key_pair: RSAKeyPair):
297
  """Test provider initialization with public key."""
298
+ provider = JWTVerifier(
299
  public_key=rsa_key_pair.public_key, issuer="https://test.example.com"
300
  )
301
 
 
305
 
306
  def test_initialization_with_jwks_uri(self):
307
  """Test provider initialization with JWKS URI."""
308
+ provider = JWTVerifier(
309
  jwks_uri="https://test.example.com/.well-known/jwks.json",
310
  issuer="https://test.example.com",
311
  )
 
319
  with pytest.raises(
320
  ValueError, match="Either public_key or jwks_uri must be provided"
321
  ):
322
+ JWTVerifier(issuer="https://test.example.com")
323
 
324
  def test_initialization_rejects_both_key_and_uri(self, rsa_key_pair: RSAKeyPair):
325
  """Test that both public_key and jwks_uri cannot be provided."""
326
  with pytest.raises(
327
  ValueError, match="Provide either public_key or jwks_uri, not both"
328
  ):
329
+ JWTVerifier(
330
  public_key=rsa_key_pair.public_key,
331
  jwks_uri="https://test.example.com/.well-known/jwks.json",
332
  issuer="https://test.example.com",
333
  )
334
 
335
  async def test_valid_token_validation(
336
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
337
  ):
338
  """Test validation of a valid token."""
339
  token = rsa_key_pair.create_token(
 
352
  assert access_token.expires_at is not None
353
 
354
  async def test_expired_token_rejection(
355
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
356
  ):
357
  """Test rejection of expired tokens."""
358
  token = rsa_key_pair.create_token(
 
366
  assert access_token is None
367
 
368
  async def test_invalid_issuer_rejection(
369
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
370
  ):
371
  """Test rejection of tokens with invalid issuer."""
372
  token = rsa_key_pair.create_token(
 
379
  assert access_token is None
380
 
381
  async def test_invalid_audience_rejection(
382
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
383
  ):
384
  """Test rejection of tokens with invalid audience."""
385
  token = rsa_key_pair.create_token(
 
393
 
394
  async def test_no_issuer_validation_when_none(self, rsa_key_pair: RSAKeyPair):
395
  """Test that issuer validation is skipped when provider has no issuer configured."""
396
+ provider = JWTVerifier(
397
  public_key=rsa_key_pair.public_key,
398
  issuer=None, # No issuer validation
399
  )
 
407
 
408
  async def test_no_audience_validation_when_none(self, rsa_key_pair: RSAKeyPair):
409
  """Test that audience validation is skipped when provider has no audience configured."""
410
+ provider = JWTVerifier(
411
  public_key=rsa_key_pair.public_key,
412
  issuer="https://test.example.com",
413
  audience=None, # No audience validation
 
424
 
425
  async def test_multiple_audiences_validation(self, rsa_key_pair: RSAKeyPair):
426
  """Test validation with multiple audiences in token."""
427
+ provider = JWTVerifier(
428
  public_key=rsa_key_pair.public_key,
429
  issuer="https://test.example.com",
430
  audience="https://api.example.com",
 
445
  self, rsa_key_pair: RSAKeyPair
446
  ):
447
  """Test provider configured with multiple expected audiences."""
448
+ provider = JWTVerifier(
449
  public_key=rsa_key_pair.public_key,
450
  issuer="https://test.example.com",
451
  audience=["https://api.example.com", "https://other-api.example.com"],
 
481
  assert access_token3 is None
482
 
483
  async def test_scope_extraction_string(
484
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
485
  ):
486
  """Test scope extraction from space-separated string."""
487
  token = rsa_key_pair.create_token(
 
497
  assert set(access_token.scopes) == {"read", "write", "admin"}
498
 
499
  async def test_scope_extraction_list(
500
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
501
  ):
502
  """Test scope extraction from list format."""
503
  token = rsa_key_pair.create_token(
 
513
  assert set(access_token.scopes) == {"read", "write"}
514
 
515
  async def test_no_scopes(
516
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
517
  ):
518
  """Test token with no scopes."""
519
  token = rsa_key_pair.create_token(
 
529
  assert access_token.scopes == []
530
 
531
  async def test_scp_claim_extraction_string(
532
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
533
  ):
534
  """Test scope extraction from 'scp' claim with space-separated string."""
535
  token = rsa_key_pair.create_token(
 
545
  assert set(access_token.scopes) == {"read", "write", "admin"}
546
 
547
  async def test_scp_claim_extraction_list(
548
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
549
  ):
550
  """Test scope extraction from 'scp' claim with list format."""
551
  token = rsa_key_pair.create_token(
 
563
  assert set(access_token.scopes) == {"read", "write", "admin"}
564
 
565
  async def test_scope_precedence_over_scp(
566
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
567
  ):
568
  """Test that 'scope' claim takes precedence over 'scp' claim when both are present."""
569
  token = rsa_key_pair.create_token(
 
581
  assert access_token is not None
582
  assert set(access_token.scopes) == {"read", "write"} # Only 'scope' claim used
583
 
584
+ async def test_malformed_token_rejection(self, bearer_provider: JWTVerifier):
585
  """Test rejection of malformed tokens."""
586
  malformed_tokens = [
587
  "not.a.jwt",
 
596
  assert access_token is None
597
 
598
  async def test_invalid_signature_rejection(
599
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
600
  ):
601
  """Test rejection of tokens with invalid signatures."""
602
  # Create a token with a different key pair
 
611
  assert access_token is None
612
 
613
  async def test_client_id_fallback(
614
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier
615
  ):
616
  """Test client_id extraction with fallback logic."""
617
  # Test with explicit client_id claim
 
629
  async def test_string_issuer_validation(self, rsa_key_pair: RSAKeyPair):
630
  """Test that string (non-URL) issuers are supported per RFC 7519."""
631
  # Create provider with string issuer
632
+ provider = JWTVerifier(
633
  public_key=rsa_key_pair.public_key,
634
  issuer="my-service", # String issuer, not a URL
635
  )
 
647
  async def test_string_issuer_mismatch_rejection(self, rsa_key_pair: RSAKeyPair):
648
  """Test that mismatched string issuers are rejected."""
649
  # Create provider with one string issuer
650
+ provider = JWTVerifier(
651
  public_key=rsa_key_pair.public_key,
652
  issuer="my-service",
653
  )
 
664
  async def test_url_issuer_still_works(self, rsa_key_pair: RSAKeyPair):
665
  """Test that URL issuers still work after the fix."""
666
  # Create provider with URL issuer
667
+ provider = JWTVerifier(
668
  public_key=rsa_key_pair.public_key,
669
  issuer="https://my-auth-server.com", # URL issuer
670
  )
 
683
  class TestFastMCPBearerAuth:
684
  def test_bearer_auth(self):
685
  mcp = FastMCP(
686
+ auth=JWTVerifier(issuer="https://test.example.com", public_key="abc")
687
  )
688
+ assert isinstance(mcp.auth, JWTVerifier)
689
 
690
  async def test_unauthorized_access(self, mcp_server_url: str):
691
  with pytest.raises(httpx.HTTPStatusError) as exc_info:
 
750
  with pytest.raises(httpx.HTTPStatusError) as exc_info:
751
  async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
752
  tools = await client.list_tools() # noqa: F841
753
+ # JWTVerifier returns 401 when verify_token returns None (invalid token)
754
+ # This is correct behavior - when TokenVerifier.verify_token returns None,
755
+ # it indicates the token is invalid (not just insufficient permissions)
756
+ assert exc_info.value.response.status_code == 401
757
  assert "tools" not in locals()
758
 
759
  async def test_token_with_sufficient_scopes(
tests/contrib/test_component_manager.py CHANGED
@@ -4,7 +4,7 @@ from starlette.testclient import TestClient
4
 
5
  from fastmcp import FastMCP
6
  from fastmcp.contrib.component_manager import set_up_component_manager
7
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
8
 
9
 
10
  class TestComponentManagementRoutes:
@@ -340,7 +340,7 @@ class TestAuthComponentManagementRoutes:
340
  """Set up test fixtures."""
341
  # Generate a key pair and create an auth provider
342
  key_pair = RSAKeyPair.generate()
343
- self.auth = BearerAuthProvider(
344
  public_key=key_pair.public_key,
345
  issuer="https://dev.example.com",
346
  audience="my-dev-server",
@@ -425,7 +425,7 @@ class TestAuthComponentManagementRoutes:
425
  assert tool.enabled is False
426
 
427
  async def test_forbidden_enable_tool(self):
428
- """Test that unauthenticated requests to enable a resource are rejected."""
429
  tool = await self.mcp._tool_manager.get_tool("test_tool")
430
  tool.enabled = False
431
 
@@ -459,7 +459,7 @@ class TestAuthComponentManagementRoutes:
459
  assert resource.enabled is True
460
 
461
  async def test_forbidden_enable_resource(self):
462
- """Test that unauthenticated requests to enable a resource are rejected."""
463
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
464
  resource.enabled = False
465
 
@@ -515,7 +515,7 @@ class TestAuthComponentManagementRoutes:
515
  assert prompt.enabled is True
516
 
517
  async def test_forbidden_disable_prompt(self):
518
- """Test that unauthenticated requests to enable a resource are rejected."""
519
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
520
  prompt.enabled = True
521
 
@@ -606,11 +606,10 @@ class TestComponentManagerWithPathAuth:
606
  def setup_method(self):
607
  # Generate a key pair and create an auth provider
608
  key_pair = RSAKeyPair.generate()
609
- self.auth = BearerAuthProvider(
610
  public_key=key_pair.public_key,
611
  issuer="https://dev.example.com",
612
  audience="my-dev-server",
613
- required_scopes=["tool:write", "tool:read"],
614
  )
615
  self.mcp = FastMCP("TestServerWithPathAuth", auth=self.auth)
616
  set_up_component_manager(
 
4
 
5
  from fastmcp import FastMCP
6
  from fastmcp.contrib.component_manager import set_up_component_manager
7
+ from fastmcp.server.auth.verifiers import JWTVerifier, RSAKeyPair
8
 
9
 
10
  class TestComponentManagementRoutes:
 
340
  """Set up test fixtures."""
341
  # Generate a key pair and create an auth provider
342
  key_pair = RSAKeyPair.generate()
343
+ self.auth = JWTVerifier(
344
  public_key=key_pair.public_key,
345
  issuer="https://dev.example.com",
346
  audience="my-dev-server",
 
425
  assert tool.enabled is False
426
 
427
  async def test_forbidden_enable_tool(self):
428
+ """Test that requests with insufficient scopes are rejected."""
429
  tool = await self.mcp._tool_manager.get_tool("test_tool")
430
  tool.enabled = False
431
 
 
459
  assert resource.enabled is True
460
 
461
  async def test_forbidden_enable_resource(self):
462
+ """Test that requests with insufficient scopes are rejected."""
463
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
464
  resource.enabled = False
465
 
 
515
  assert prompt.enabled is True
516
 
517
  async def test_forbidden_disable_prompt(self):
518
+ """Test that requests with insufficient scopes are rejected."""
519
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
520
  prompt.enabled = True
521
 
 
606
  def setup_method(self):
607
  # Generate a key pair and create an auth provider
608
  key_pair = RSAKeyPair.generate()
609
+ self.auth = JWTVerifier(
610
  public_key=key_pair.public_key,
611
  issuer="https://dev.example.com",
612
  audience="my-dev-server",
 
613
  )
614
  self.mcp = FastMCP("TestServerWithPathAuth", auth=self.auth)
615
  set_up_component_manager(
tests/deprecated/test_bearer_auth_provider.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ # reset deprecation warnings for this module
4
+ pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
5
+
6
+
7
+ def test_bearer_auth_provider_deprecated():
8
+ """Test that BearerAuthProvider import shows deprecation warning."""
9
+ with pytest.warns(
10
+ DeprecationWarning,
11
+ match="The `fastmcp.server.auth.providers.bearer` module is deprecated and will be removed in a future version. Please use `fastmcp.server.auth.verifiers.JWTVerifier` instead of this module's BearerAuthProvider.",
12
+ ):
13
+ from fastmcp.server.auth import BearerAuthProvider # noqa: F401
tests/server/auth/test_token_verifier_integration.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for TokenVerifier integration with FastMCP."""
2
+
3
+ import httpx
4
+ import pytest
5
+ from mcp.server.auth.provider import AccessToken
6
+
7
+ from fastmcp.server import FastMCP
8
+ from fastmcp.server.auth.verifiers import StaticTokenVerifier
9
+
10
+
11
+ class TestTokenVerifierIntegration:
12
+ """Test TokenVerifier integration with FastMCP server."""
13
+
14
+ def test_static_token_verifier_creation(self):
15
+ """Test creating a FastMCP server with StaticTokenVerifier."""
16
+ verifier = StaticTokenVerifier(
17
+ {"test-token": {"client_id": "test-client", "scopes": ["read", "write"]}}
18
+ )
19
+
20
+ server = FastMCP("TestServer", auth=verifier)
21
+ assert server.auth is verifier
22
+
23
+ async def test_static_token_verifier_verify_token(self):
24
+ """Test StaticTokenVerifier token verification."""
25
+ verifier = StaticTokenVerifier(
26
+ {
27
+ "valid-token": {
28
+ "client_id": "test-client",
29
+ "scopes": ["read", "write"],
30
+ "expires_at": None,
31
+ },
32
+ "scoped-token": {"client_id": "limited-client", "scopes": ["read"]},
33
+ }
34
+ )
35
+
36
+ # Test valid token
37
+ result = await verifier.verify_token("valid-token")
38
+ assert isinstance(result, AccessToken)
39
+ assert result.client_id == "test-client"
40
+ assert result.scopes == ["read", "write"]
41
+ assert result.token == "valid-token"
42
+ assert result.expires_at is None
43
+
44
+ # Test token with different scopes
45
+ result = await verifier.verify_token("scoped-token")
46
+ assert isinstance(result, AccessToken)
47
+ assert result.client_id == "limited-client"
48
+ assert result.scopes == ["read"]
49
+
50
+ # Test invalid token
51
+ result = await verifier.verify_token("invalid-token")
52
+ assert result is None
53
+
54
+ async def test_server_with_token_verifier_http_app(self):
55
+ """Test that FastMCP server works with TokenVerifier for HTTP requests."""
56
+ verifier = StaticTokenVerifier(
57
+ {"test-token": {"client_id": "test-client", "scopes": ["read", "write"]}}
58
+ )
59
+
60
+ server = FastMCP("TestServer", auth=verifier)
61
+
62
+ @server.tool
63
+ def greet(name: str) -> str:
64
+ return f"Hello, {name}!"
65
+
66
+ # Create HTTP app
67
+ app = server.http_app(transport="http")
68
+
69
+ # Test unauthenticated request gets 401
70
+ async with httpx.AsyncClient(
71
+ transport=httpx.ASGITransport(app=app), base_url="http://test"
72
+ ) as client:
73
+ response = await client.post("/mcp/")
74
+ assert response.status_code == 401
75
+ assert "WWW-Authenticate" in response.headers
76
+
77
+ def test_server_rejects_both_oauth_and_token_verifier(self):
78
+ """Test that server raises error when both OAuth and TokenVerifier provided."""
79
+ from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
80
+
81
+ oauth_provider = InMemoryOAuthProvider("http://test.com")
82
+ token_verifier = StaticTokenVerifier({"token": {"client_id": "test"}})
83
+
84
+ # This should work - OAuth provider
85
+ server1 = FastMCP("Test1", auth=oauth_provider)
86
+ assert server1.auth is oauth_provider
87
+
88
+ # This should work - TokenVerifier
89
+ server2 = FastMCP("Test2", auth=token_verifier)
90
+ assert server2.auth is token_verifier
91
+
92
+
93
+ class TestJWTVerifierImport:
94
+ """Test JWT token verifier can be imported and created."""
95
+
96
+ def test_jwt_verifier_requires_pyjwt(self):
97
+ """Test that JWTVerifier raises helpful error without PyJWT."""
98
+ # Since PyJWT is likely installed in test environment, we'll just test construction
99
+ from fastmcp.server.auth.verifiers import JWTVerifier
100
+
101
+ # This should work if PyJWT is available
102
+ try:
103
+ verifier = JWTVerifier(public_key="dummy-key")
104
+ assert verifier.public_key == "dummy-key"
105
+ assert verifier.algorithm == "RS256"
106
+ except ImportError as e:
107
+ # If PyJWT not available, should get helpful error
108
+ assert "PyJWT is required" in str(e)
109
+
110
+
111
+ class TestIntrospectionTokenVerifierImport:
112
+ """Test introspection token verifier can be imported and created."""
113
+
114
+ def test_introspection_verifier_creation(self):
115
+ """Test IntrospectionTokenVerifier construction."""
116
+ from fastmcp.server.auth.verifiers import IntrospectionTokenVerifier
117
+
118
+ verifier = IntrospectionTokenVerifier(
119
+ "https://auth.example.com/introspect", "https://resource.example.com"
120
+ )
121
+
122
+ assert (
123
+ str(verifier.introspection_endpoint)
124
+ == "https://auth.example.com/introspect"
125
+ )
126
+ assert str(verifier.server_url) == "https://resource.example.com/"
127
+ assert verifier.validate_resource is False
128
+ assert verifier.required_scopes == []
129
+
130
+ def test_introspection_verifier_rejects_private_urls(self):
131
+ """Test that IntrospectionTokenVerifier rejects private URLs."""
132
+ from fastmcp.server.auth.verifiers import IntrospectionTokenVerifier
133
+
134
+ with pytest.raises(ValueError, match="private/localhost URL"):
135
+ IntrospectionTokenVerifier(
136
+ "http://localhost/introspect", "https://resource.example.com"
137
+ )
138
+
139
+ with pytest.raises(ValueError, match="private/localhost URL"):
140
+ IntrospectionTokenVerifier(
141
+ "http://127.0.0.1/introspect", "https://resource.example.com"
142
+ )
tests/server/http/test_auth_setup.py CHANGED
@@ -6,8 +6,8 @@ from mcp.server.auth.provider import AccessToken
6
  from starlette.middleware import Middleware
7
  from starlette.middleware.authentication import AuthenticationMiddleware
8
 
9
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
10
  from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
 
11
  from fastmcp.server.http import setup_auth_middleware_and_routes
12
 
13
 
@@ -15,10 +15,10 @@ class TestSetupAuthMiddlewareAndRoutes:
15
  """Test setup_auth_middleware_and_routes with TokenVerifier providers."""
16
 
17
  @pytest.fixture
18
- def bearer_provider(self) -> BearerAuthProvider:
19
- """Create BearerAuthProvider for testing."""
20
  key_pair = RSAKeyPair.generate()
21
- return BearerAuthProvider(
22
  public_key=key_pair.public_key,
23
  issuer="https://test.example.com",
24
  audience="https://api.example.com",
@@ -33,10 +33,10 @@ class TestSetupAuthMiddlewareAndRoutes:
33
  required_scopes=["user"],
34
  )
35
 
36
- def test_setup_with_bearer_provider(self, bearer_provider: BearerAuthProvider):
37
- """Test that setup works with BearerAuthProvider as TokenVerifier."""
38
  middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
39
- bearer_provider
40
  )
41
 
42
  # Should return middleware list
@@ -51,11 +51,11 @@ class TestSetupAuthMiddlewareAndRoutes:
51
 
52
  backend = auth_middleware.kwargs["backend"]
53
  assert isinstance(backend, BearerAuthBackend)
54
- assert backend.token_verifier is bearer_provider # type: ignore[attr-defined]
55
 
56
  # Should return auth routes
57
  assert isinstance(auth_routes, list)
58
- assert len(auth_routes) > 0 # Should have OAuth routes
59
 
60
  # Should return required scopes
61
  assert required_scopes == ["read", "write"]
@@ -81,25 +81,23 @@ class TestSetupAuthMiddlewareAndRoutes:
81
  # Should return required scopes
82
  assert required_scopes == ["user"]
83
 
84
- def test_setup_preserves_provider_functionality(
85
- self, bearer_provider: BearerAuthProvider
86
- ):
87
  """Test that setup doesn't break the provider's functionality."""
88
  # Setup should not modify the provider
89
- original_issuer = bearer_provider.issuer
90
- original_scopes = bearer_provider.required_scopes
91
 
92
  middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
93
- bearer_provider
94
  )
95
 
96
  # Provider should be unchanged
97
- assert bearer_provider.issuer == original_issuer
98
- assert bearer_provider.required_scopes == original_scopes
99
 
100
  # Provider should still work as TokenVerifier
101
- assert hasattr(bearer_provider, "verify_token")
102
- assert callable(bearer_provider.verify_token)
103
 
104
 
105
  class MockOAuthProvider:
 
6
  from starlette.middleware import Middleware
7
  from starlette.middleware.authentication import AuthenticationMiddleware
8
 
 
9
  from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
10
+ from fastmcp.server.auth.verifiers import JWTVerifier, RSAKeyPair
11
  from fastmcp.server.http import setup_auth_middleware_and_routes
12
 
13
 
 
15
  """Test setup_auth_middleware_and_routes with TokenVerifier providers."""
16
 
17
  @pytest.fixture
18
+ def jwt_verifier(self) -> JWTVerifier:
19
+ """Create JWTVerifier for testing."""
20
  key_pair = RSAKeyPair.generate()
21
+ return JWTVerifier(
22
  public_key=key_pair.public_key,
23
  issuer="https://test.example.com",
24
  audience="https://api.example.com",
 
33
  required_scopes=["user"],
34
  )
35
 
36
+ def test_setup_with_jwt_verifier(self, jwt_verifier: JWTVerifier):
37
+ """Test that setup works with JWTVerifier as TokenVerifier."""
38
  middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
39
+ jwt_verifier
40
  )
41
 
42
  # Should return middleware list
 
51
 
52
  backend = auth_middleware.kwargs["backend"]
53
  assert isinstance(backend, BearerAuthBackend)
54
+ assert backend.token_verifier is jwt_verifier # type: ignore[attr-defined]
55
 
56
  # Should return auth routes
57
  assert isinstance(auth_routes, list)
58
+ assert len(auth_routes) == 0 # TokenVerifier should not have OAuth routes
59
 
60
  # Should return required scopes
61
  assert required_scopes == ["read", "write"]
 
81
  # Should return required scopes
82
  assert required_scopes == ["user"]
83
 
84
+ def test_setup_preserves_provider_functionality(self, jwt_verifier: JWTVerifier):
 
 
85
  """Test that setup doesn't break the provider's functionality."""
86
  # Setup should not modify the provider
87
+ original_issuer = jwt_verifier.issuer
88
+ original_scopes = jwt_verifier.required_scopes
89
 
90
  middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
91
+ jwt_verifier
92
  )
93
 
94
  # Provider should be unchanged
95
+ assert jwt_verifier.issuer == original_issuer
96
+ assert jwt_verifier.required_scopes == original_scopes
97
 
98
  # Provider should still work as TokenVerifier
99
+ assert hasattr(jwt_verifier, "verify_token")
100
+ assert callable(jwt_verifier.verify_token)
101
 
102
 
103
  class MockOAuthProvider:
tests/server/http/test_bearer_auth_backend.py CHANGED
@@ -5,7 +5,7 @@ from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend
5
  from mcp.server.auth.provider import AccessToken
6
  from starlette.requests import HTTPConnection
7
 
8
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
9
 
10
 
11
  class TestBearerAuthBackendTokenVerifierIntegration:
@@ -17,9 +17,9 @@ class TestBearerAuthBackendTokenVerifierIntegration:
17
  return RSAKeyPair.generate()
18
 
19
  @pytest.fixture
20
- def bearer_provider(self, rsa_key_pair: RSAKeyPair) -> BearerAuthProvider:
21
- """Create BearerAuthProvider for testing."""
22
- return BearerAuthProvider(
23
  public_key=rsa_key_pair.public_key,
24
  issuer="https://test.example.com",
25
  audience="https://api.example.com",
@@ -36,18 +36,18 @@ class TestBearerAuthBackendTokenVerifierIntegration:
36
  )
37
 
38
  def test_bearer_auth_backend_constructor_accepts_token_verifier(
39
- self, bearer_provider: BearerAuthProvider
40
  ):
41
  """Test that BearerAuthBackend constructor accepts TokenVerifier."""
42
  # This should not raise an error
43
- backend = BearerAuthBackend(bearer_provider)
44
- assert backend.token_verifier is bearer_provider # type: ignore[attr-defined]
45
 
46
  async def test_bearer_auth_backend_authenticate_with_valid_token(
47
- self, bearer_provider: BearerAuthProvider, valid_token: str
48
  ):
49
  """Test BearerAuthBackend authentication with valid token."""
50
- backend = BearerAuthBackend(bearer_provider)
51
 
52
  # Create mock HTTPConnection with Authorization header
53
  scope = {
@@ -66,10 +66,10 @@ class TestBearerAuthBackendTokenVerifierIntegration:
66
  assert user.access_token.token == valid_token
67
 
68
  async def test_bearer_auth_backend_authenticate_with_invalid_token(
69
- self, bearer_provider: BearerAuthProvider
70
  ):
71
  """Test BearerAuthBackend authentication with invalid token."""
72
- backend = BearerAuthBackend(bearer_provider)
73
 
74
  # Create mock HTTPConnection with invalid Authorization header
75
  scope = {
@@ -82,10 +82,10 @@ class TestBearerAuthBackendTokenVerifierIntegration:
82
  assert result is None
83
 
84
  async def test_bearer_auth_backend_authenticate_with_no_header(
85
- self, bearer_provider: BearerAuthProvider
86
  ):
87
  """Test BearerAuthBackend authentication with no Authorization header."""
88
- backend = BearerAuthBackend(bearer_provider)
89
 
90
  # Create mock HTTPConnection without Authorization header
91
  scope = {
@@ -98,10 +98,10 @@ class TestBearerAuthBackendTokenVerifierIntegration:
98
  assert result is None
99
 
100
  async def test_bearer_auth_backend_authenticate_with_non_bearer_token(
101
- self, bearer_provider: BearerAuthProvider
102
  ):
103
  """Test BearerAuthBackend authentication with non-Bearer token."""
104
- backend = BearerAuthBackend(bearer_provider)
105
 
106
  # Create mock HTTPConnection with Basic auth header
107
  scope = {
 
5
  from mcp.server.auth.provider import AccessToken
6
  from starlette.requests import HTTPConnection
7
 
8
+ from fastmcp.server.auth.verifiers import JWTVerifier, RSAKeyPair
9
 
10
 
11
  class TestBearerAuthBackendTokenVerifierIntegration:
 
17
  return RSAKeyPair.generate()
18
 
19
  @pytest.fixture
20
+ def jwt_verifier(self, rsa_key_pair: RSAKeyPair) -> JWTVerifier:
21
+ """Create JWTVerifier for testing."""
22
+ return JWTVerifier(
23
  public_key=rsa_key_pair.public_key,
24
  issuer="https://test.example.com",
25
  audience="https://api.example.com",
 
36
  )
37
 
38
  def test_bearer_auth_backend_constructor_accepts_token_verifier(
39
+ self, jwt_verifier: JWTVerifier
40
  ):
41
  """Test that BearerAuthBackend constructor accepts TokenVerifier."""
42
  # This should not raise an error
43
+ backend = BearerAuthBackend(jwt_verifier)
44
+ assert backend.token_verifier is jwt_verifier # type: ignore[attr-defined]
45
 
46
  async def test_bearer_auth_backend_authenticate_with_valid_token(
47
+ self, jwt_verifier: JWTVerifier, valid_token: str
48
  ):
49
  """Test BearerAuthBackend authentication with valid token."""
50
+ backend = BearerAuthBackend(jwt_verifier)
51
 
52
  # Create mock HTTPConnection with Authorization header
53
  scope = {
 
66
  assert user.access_token.token == valid_token
67
 
68
  async def test_bearer_auth_backend_authenticate_with_invalid_token(
69
+ self, jwt_verifier: JWTVerifier
70
  ):
71
  """Test BearerAuthBackend authentication with invalid token."""
72
+ backend = BearerAuthBackend(jwt_verifier)
73
 
74
  # Create mock HTTPConnection with invalid Authorization header
75
  scope = {
 
82
  assert result is None
83
 
84
  async def test_bearer_auth_backend_authenticate_with_no_header(
85
+ self, jwt_verifier: JWTVerifier
86
  ):
87
  """Test BearerAuthBackend authentication with no Authorization header."""
88
+ backend = BearerAuthBackend(jwt_verifier)
89
 
90
  # Create mock HTTPConnection without Authorization header
91
  scope = {
 
98
  assert result is None
99
 
100
  async def test_bearer_auth_backend_authenticate_with_non_bearer_token(
101
+ self, jwt_verifier: JWTVerifier
102
  ):
103
  """Test BearerAuthBackend authentication with non-Bearer token."""
104
+ backend = BearerAuthBackend(jwt_verifier)
105
 
106
  # Create mock HTTPConnection with Basic auth header
107
  scope = {
tests/server/http/test_http_auth_middleware.py CHANGED
@@ -3,7 +3,7 @@ from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
3
  from starlette.routing import Mount
4
 
5
  from fastmcp.server import FastMCP
6
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
7
  from fastmcp.server.http import create_streamable_http_app
8
 
9
 
@@ -17,11 +17,11 @@ class TestStreamableHTTPAppResourceMetadataURL:
17
 
18
  @pytest.fixture
19
  def bearer_auth_provider(self, rsa_key_pair):
20
- provider = BearerAuthProvider(
21
  public_key=rsa_key_pair.public_key,
22
  issuer="https://issuer",
23
  audience="https://audience",
24
- resource_server="https://resource.example.com",
25
  )
26
  return provider
27
 
@@ -45,11 +45,11 @@ class TestStreamableHTTPAppResourceMetadataURL:
45
  )
46
 
47
  def test_trailing_slash_handling_in_resource_server_url(self, rsa_key_pair):
48
- provider = BearerAuthProvider(
49
  public_key=rsa_key_pair.public_key,
50
  issuer="https://issuer",
51
  audience="https://audience",
52
- resource_server="https://resource.example.com/",
53
  )
54
  server = FastMCP(name="TestServer")
55
  app = create_streamable_http_app(
 
3
  from starlette.routing import Mount
4
 
5
  from fastmcp.server import FastMCP
6
+ from fastmcp.server.auth.verifiers import JWTVerifier, RSAKeyPair
7
  from fastmcp.server.http import create_streamable_http_app
8
 
9
 
 
17
 
18
  @pytest.fixture
19
  def bearer_auth_provider(self, rsa_key_pair):
20
+ provider = JWTVerifier(
21
  public_key=rsa_key_pair.public_key,
22
  issuer="https://issuer",
23
  audience="https://audience",
24
+ resource_server_url="https://resource.example.com",
25
  )
26
  return provider
27
 
 
45
  )
46
 
47
  def test_trailing_slash_handling_in_resource_server_url(self, rsa_key_pair):
48
+ provider = JWTVerifier(
49
  public_key=rsa_key_pair.public_key,
50
  issuer="https://issuer",
51
  audience="https://audience",
52
+ resource_server_url="https://resource.example.com/",
53
  )
54
  server = FastMCP(name="TestServer")
55
  app = create_streamable_http_app(