99from pydantic import AnyUrl
1010
1111from fastmcp .server .auth .providers .azure import AzureProvider
12- from fastmcp .server .auth .providers .jwt import JWTVerifier
12+ from fastmcp .server .auth .providers .jwt import JWTVerifier , RSAKeyPair
1313
1414
1515@pytest .fixture
@@ -190,8 +190,6 @@ def test_init_with_custom_audience_uses_jwt_verifier(
190190 self , memory_storage : MemoryStore
191191 ):
192192 """When audience is provided, JWTVerifier is configured with JWKS and issuer."""
193- from fastmcp .server .auth .providers .jwt import JWTVerifier
194-
195193 provider = AzureProvider (
196194 client_id = "test_client" ,
197195 client_secret = "test_secret" ,
@@ -211,11 +209,100 @@ def test_init_with_custom_audience_uses_jwt_verifier(
211209 "https://login.microsoftonline.com/my-tenant/discovery/v2.0/keys"
212210 )
213211 assert verifier .issuer == "https://login.microsoftonline.com/my-tenant/v2.0"
214- assert verifier .audience == " api://my-api"
212+ assert verifier .audience == [ "test_client" , " api://my-api"]
215213 # Scopes are stored unprefixed for token validation
216214 # (Azure returns unprefixed scopes like ".default" in JWT tokens)
217215 assert verifier .required_scopes == [".default" ]
218216
217+ async def test_token_accepted_with_client_id_audience (
218+ self , memory_storage : MemoryStore
219+ ):
220+ """Azure AD v2 tokens use the bare client_id as aud — must be accepted."""
221+ key_pair = RSAKeyPair .generate ()
222+ provider = AzureProvider (
223+ client_id = "test_client" ,
224+ client_secret = "test_secret" ,
225+ tenant_id = "my-tenant" ,
226+ base_url = "https://myserver.com" ,
227+ identifier_uri = "api://my-api" ,
228+ required_scopes = ["read" ],
229+ jwt_signing_key = "test-secret" ,
230+ client_storage = memory_storage ,
231+ )
232+
233+ assert isinstance (provider ._token_validator , JWTVerifier )
234+ verifier = provider ._token_validator
235+ verifier .public_key = key_pair .public_key
236+ verifier .jwks_uri = None
237+
238+ token = key_pair .create_token (
239+ subject = "test-user" ,
240+ issuer = "https://login.microsoftonline.com/my-tenant/v2.0" ,
241+ audience = "test_client" ,
242+ additional_claims = {"scp" : "read" },
243+ )
244+ result = await verifier .load_access_token (token )
245+ assert result is not None
246+
247+ async def test_token_accepted_with_identifier_uri_audience (
248+ self , memory_storage : MemoryStore
249+ ):
250+ """Azure AD v1 tokens use the identifier_uri as aud — must be accepted."""
251+ key_pair = RSAKeyPair .generate ()
252+ provider = AzureProvider (
253+ client_id = "test_client" ,
254+ client_secret = "test_secret" ,
255+ tenant_id = "my-tenant" ,
256+ base_url = "https://myserver.com" ,
257+ identifier_uri = "api://my-api" ,
258+ required_scopes = ["read" ],
259+ jwt_signing_key = "test-secret" ,
260+ client_storage = memory_storage ,
261+ )
262+
263+ assert isinstance (provider ._token_validator , JWTVerifier )
264+ verifier = provider ._token_validator
265+ verifier .public_key = key_pair .public_key
266+ verifier .jwks_uri = None
267+
268+ token = key_pair .create_token (
269+ subject = "test-user" ,
270+ issuer = "https://login.microsoftonline.com/my-tenant/v2.0" ,
271+ audience = "api://my-api" ,
272+ additional_claims = {"scp" : "read" },
273+ )
274+ result = await verifier .load_access_token (token )
275+ assert result is not None
276+
277+ async def test_token_rejected_with_wrong_audience (
278+ self , memory_storage : MemoryStore
279+ ):
280+ """Tokens for a different application must be rejected."""
281+ key_pair = RSAKeyPair .generate ()
282+ provider = AzureProvider (
283+ client_id = "test_client" ,
284+ client_secret = "test_secret" ,
285+ tenant_id = "my-tenant" ,
286+ base_url = "https://myserver.com" ,
287+ required_scopes = ["read" ],
288+ jwt_signing_key = "test-secret" ,
289+ client_storage = memory_storage ,
290+ )
291+
292+ assert isinstance (provider ._token_validator , JWTVerifier )
293+ verifier = provider ._token_validator
294+ verifier .public_key = key_pair .public_key
295+ verifier .jwks_uri = None
296+
297+ token = key_pair .create_token (
298+ subject = "test-user" ,
299+ issuer = "https://login.microsoftonline.com/my-tenant/v2.0" ,
300+ audience = "wrong-app-id" ,
301+ additional_claims = {"scp" : "read" },
302+ )
303+ result = await verifier .load_access_token (token )
304+ assert result is None
305+
219306 async def test_authorize_filters_resource_and_stores_unprefixed_scopes (
220307 self , memory_storage : MemoryStore
221308 ):
0 commit comments