{"record":{"id":"776479cb53288242","repo":"PrefectHQ/fastmcp","slug":"azureprovider-requires-at-least-one-non-oidc-scope","errorCode":null,"errorMessage":"AzureProvider requires at least one non-OIDC scope in required_scopes (e.g., 'read', 'write'). OIDC scopes like 'openid', 'profile', 'email', and 'offline_access' are not included in Azure access token claims and cannot be used for scope enforcement.","messagePattern":"AzureProvider requires at least one non-OIDC scope in required_scopes \\(e\\.g\\., 'read', 'write'\\)\\. OIDC scopes like 'openid', 'profile', 'email', and 'offline_access' are not included in Azure access token claims and cannot be used for scope enforcement\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/providers/azure.py","lineNumber":236,"sourceCode":"        self._obo_supported = True\n\n        # Apply defaults\n        self.identifier_uri = identifier_uri or f\"api://{client_id}\"\n        self.additional_authorize_scopes: list[str] = parsed_additional_scopes\n\n        # Always validate tokens against the app's API client ID using JWT\n        issuer = token_issuer or f\"https://{base_authority}/{tenant_id}/v2.0\"\n        jwks_uri = f\"https://{base_authority}/{tenant_id}/discovery/v2.0/keys\"\n\n        # Azure access tokens only include custom API scopes in the `scp` claim,\n        # NOT standard OIDC scopes (openid, profile, email, offline_access).\n        # Filter out OIDC scopes from validation - they'll still be sent to Azure\n        # during authorization (handled by _prefix_scopes_for_azure).\n        validation_scopes = [\n            s for s in (parsed_required_scopes or []) if s not in OIDC_SCOPES\n        ]\n        if not validation_scopes:\n            raise ValueError(\n                \"AzureProvider requires at least one non-OIDC scope in \"\n                \"required_scopes (e.g., 'read', 'write'). OIDC scopes like \"\n                \"'openid', 'profile', 'email', and 'offline_access' are not \"\n                \"included in Azure access token claims and cannot be used for \"\n                \"scope enforcement.\"\n            )\n\n        token_verifier = JWTVerifier(\n            jwks_uri=jwks_uri,\n            issuer=issuer,\n            audience=[client_id, self.identifier_uri],\n            algorithm=\"RS256\",\n            required_scopes=validation_scopes,  # Only validate non-OIDC scopes\n            http_client=http_client,\n        )\n\n        # Build Azure OAuth endpoints with tenant\n        authorization_endpoint = (","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/providers/azure.py#L218-L254","documentation":"AzureProvider enforces scopes against claims embedded in the Azure access token. OIDC scopes (openid, profile, email, offline_access) are never included in those token claims, so if required_scopes contains only OIDC scopes there is nothing enforceable — every request would fail scope checks. The constructor therefore rejects such configurations with ValueError.","triggerScenarios":"AzureProvider(client_id=..., client_secret=..., required_scopes=[\"openid\", \"profile\"]) — i.e. parsed required_scopes minus OIDC_SCOPES yields an empty list.","commonSituations":"Reusing a required_scopes list meant for the authorization request (openid/profile) as the enforcement list; copying Google/GenericOAuth examples where OIDC scopes are legitimate requirements; assuming Azure validates OIDC scopes in tokens.","solutions":["Add at least one application-defined scope (e.g. 'read', 'write', or an api://... scope exposed by your app) to required_scopes.","If you only want OIDC scopes sent during login, remove required_scopes entirely — OIDC scopes are still sent to Azure via _prefix_scopes_for_azure without being enforced.","Enforce OIDC scope presence with a custom token verifier if truly needed, not via required_scopes."],"exampleFix":"// before\nAzureProvider(client_id=cid, client_secret=sec, required_scopes=[\"openid\", \"profile\"])\n// after\nAzureProvider(client_id=cid, client_secret=sec, required_scopes=[\"read\", \"write\"])","handlingStrategy":"validation","validationCode":"OIDC_SCOPES = {\"openid\", \"profile\", \"email\", \"offline_access\"}\nif required_scopes and not (set(required_scopes) - OIDC_SCOPES):\n    raise ValueError(\"AzureProvider required_scopes needs at least one non-OIDC scope\")","typeGuard":"def has_enforceable_scopes(scopes: list[str] | None) -> bool:\n    oidc = {\"openid\", \"profile\", \"email\", \"offline_access\"}\n    return bool(scopes) and bool(set(scopes) - oidc)","tryCatchPattern":"try:\n    provider = AzureProvider(client_id=cid, client_secret=sec, required_scopes=scopes)\nexcept ValueError as e:\n    if \"non-OIDC scope\" in str(e):\n        provider = AzureProvider(client_id=cid, client_secret=sec, required_scopes=[*scopes, \"read\"])\n    else:\n        raise","preventionTips":["Keep authorization-request scopes separate from enforcement scopes in config","Document app-defined scopes (api://... / read / write) as the source for required_scopes"],"tags":["python","azure","oauth","scopes","configuration"],"backgroundTag":"unenforceable-token-scope","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}