fastmcp icon indicating copy to clipboard operation
fastmcp copied to clipboard

2.13.0 RC2: EntraID auth tokens are failing verification - no RS256 algorithm found in JsonWebSignature used

Open wattdave opened this issue 1 month ago • 8 comments

Description

Steps to reproduce:

  1. Create an MCP object with Entra ID authentication via the AzureProvider
  2. Get a bearer token by whatever means by logging into Azure
  3. Use it in an MCP tool call
  4. Debug into BearerAuthBackend's authenticate method
  5. Call stack goes through: auth.py. Set a breakpoint at https://github.com/authlib/authlib/blob/main/authlib/jose/rfc7519/jwt.py#L104
  6. 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

wattdave avatar Oct 21 '25 18:10 wattdave

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?

jlowin avatar Oct 21 '25 22:10 jlowin

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)

wattdave avatar Oct 21 '25 23:10 wattdave

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?

wattdave avatar Oct 21 '25 23:10 wattdave

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.

jlowin avatar Oct 22 '25 00:10 jlowin

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.)

wattdave avatar Oct 22 '25 14:10 wattdave

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

jlowin avatar Oct 23 '25 01:10 jlowin

#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.

JonasKs avatar Oct 23 '25 03:10 JonasKs

I'm okay with closing this. We've moved to your Azure w/ 2.0 tokens, and life is good.

wattdave avatar Oct 30 '25 05:10 wattdave