2.13.0 RC2: EntraID auth tokens are failing verification - no RS256 algorithm found in JsonWebSignature used
Description
Steps to reproduce:
- Create an MCP object with Entra ID authentication via the AzureProvider
- Get a bearer token by whatever means by logging into Azure
- Use it in an MCP tool call
- Debug into BearerAuthBackend's authenticate method
- Call stack goes through: auth.py. Set a breakpoint at https://github.com/authlib/authlib/blob/main/authlib/jose/rfc7519/jwt.py#L104
- On my debugger, the JsonWebSignature instance that is processing the token does not include the RS256 algorithm, which is used by Entra - only the HS256 algorithm is present.
Example Code
Version Information
2.13.0rc2
I think there might be a few things getting confused here.
- the Azure provider's JWT Verifier is hardcoded to be "RS256" https://github.com/jlowin/fastmcp/blob/ac62a06158fbd8211a0c1bcc4f6b7e31e42b05be/src/fastmcp/server/auth/providers/azure.py#L194
- All OAuth providers now mint their own tokens which are passed to clients using "HS256" https://github.com/jlowin/fastmcp/blob/ac62a06158fbd8211a0c1bcc4f6b7e31e42b05be/src/fastmcp/server/auth/jwt_issuer.py#L114
So I think there's an X/Y problem where your debugging trace is actually an expected route unrelated to the error. Can you say more about how you're configuring your auth provider or what errors you observe?
For the record: the code on my end worked on 2.12.5, but fails in 2.13.0rc2 - which is why I wrote the issue.
mcp: FastMCP = FastMCP("my-name-here" auth=AUTH_PROVIDER)
Where
AUTH_PROVIDER = PatchedAzureProvider(
client_id=os.getenv("AZURE_CLIENT_ID"),
client_secret=os.getenv("AZURE_CLIENT_SECRET"),
tenant_id=os.getenv("AZURE_TENANT_ID"),
base_url=os.getenv("FASTMCP_BASE_URL", "http://localhost:8000"),
required_scopes=[
"openid",
"profile",
"email",
"offline_access",
"User.Read",
],
allowed_client_redirect_uris=allowed_redirect_uris,
client_storage=kv_storage,
)
The patch involved is something harmless, probably -- we discovered that to get Azure auth to work for us, we needed to do these two things:
class PatchedAzureProvider(AzureProvider):
def _get_resource_url(
self, path
):
"""Disable resource parameter for Azure v2.0 compatibility."""
return None
async def authorize(self, *args, **kwargs):
"""Strip resource parameter before calling parent authorize."""
kwargs.pop("resource", None)
return await super().authorize(*args, **kwargs)
I'll try once more to describe the flow. The AzureProvider uses the OAuthProxy's load_access_token() method.
Which goes through this line: https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1449 Which starts to process the bearer token received against the inner JWT issuer, as you said.
The trouble is, it throws an exception, and goes here next: https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1486
So the actual token validator never gets a chance to run: https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1474
Are you saying that I'm missing a step of some sort here? I used to call Azure to get a bearer token, and pass that in on my API call directly. Do I now need to first exchange that bearer token for one that the MCP issues?
For the record: the code on my end worked on 2.12.5, but fails in 2.13.0rc2 - which is why I wrote the issue.
I understand this, but both the Azure Provider AND the Oauth Proxy have completely different internals between those two versions, including the fact that there are now two tokens involved, so it's important to nail down where exactly the issue is taking place. By most accounts, the Azure Provider really didn't work in most cases < 2.13.
Based on your report, it sounds like your "second" JWT is not being validated by the server, which is throwing the exception (can you see what the exception is?). Did your server restart since the JWT was issued? If you haven't configured persistent storage, jwt keys, and encryption keys, then JWTs issued before a restart may not be recognized once the server comes back up.
Are you saying that I'm missing a step of some sort here? I used to call Azure to get a bearer token, and pass that in on my API call directly. Do I now need to first exchange that bearer token for one that the MCP issues?
Yes, 2.13 uses a two-step process because passing the upstream token to the downstream client crosses a security boundary that can't be violated; the token was issued to the proxy server, not the client.
Okay - can you point me to a test suite that can show me what the correct flow should be? (We've been struggling to keep the auth flow stable on 2.12.5, so I accept that we need to change something - just looking for something concrete to model.)
We don't have integration tests for Azure, but there's a lot of relevant discussion in https://github.com/jlowin/fastmcp/pull/1891 and perhaps @nbaju1 and @JonasKs could chime in with thoughts
#1891 is the best documentation. As for Azure setup, you can look at https://intility.github.io/fastapi-azure-auth/single-tenant/fastapi_configuration. Remember to set v2 in the token scheme.
Please decode your token at e.g. https://jwt.io and obfuscate anything you don't want public (such as your name) and post it here.
I suspect something else than the algorithm actually failing here.
I'm okay with closing this. We've moved to your Azure w/ 2.0 tokens, and life is good.