Skip to main content

RFC-compliant Azure AD OAuth 2.0 router for FastAPI MCP servers with Copilot Studio support

Project description

fastapi-mcp-azure-oauth

CI PyPI Python License: MIT Coverage

RFC-compliant Azure AD OAuth 2.0 router for FastAPI MCP servers, with first-class support for Copilot Studio and other Azure AD clients.


What it does

Drop a single call into any FastAPI application to get:

Standard Endpoint Purpose
RFC 8414 GET /.well-known/oauth-authorization-server Delegates clients to Azure AD's real auth endpoints
RFC 7591 GET /register Returns app credentials (Copilot Studio GET-variant)
RFC 7591 POST /register Dynamic Client Registration + auto Azure AD URI enrolment
RFC 9728 GET /.well-known/oauth-protected-resource/{slug} Protected resource metadata for MCP autodiscovery
GET /oauth/callback Minimal callback (echoes code + state for client-side exchange)
GET /oauth/config MSAL-compatible configuration for browser clients

Plus a TokenValidator that:

  • Verifies Azure AD JWT signatures via JWKS with per-tenant key caching
  • Supports both single-tenant and multi-tenant (/organizations) deployments
  • Enforces explicit issuer binding after signature verification (closes PyJWT verify_iss no-op gap)
  • Rejects api://{app_id}/.default as an audience (it's a scope suffix, not a valid token audience)
  • Caps the JWKS client cache at 50 tenants with FIFO eviction

Installation

pip install fastapi-mcp-azure-oauth

Requires Python 3.10+ and FastAPI 0.115+.


Quick start

from fastapi import FastAPI, Depends
from fastapi_mcp_azure_oauth import build_oauth_router, TokenValidator

app = FastAPI()

# 1 — Mount the OAuth router (all RFC-required endpoints)
app.include_router(
    build_oauth_router(
        app_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",   # Azure AD App (client) ID
        tenant_id="yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", # Home tenant ID
        client_secret="your-client-secret",
        api_scope="access_as_user",                       # exposed under api://{app_id}/
        resource_path="/mcp",                             # your protected resource path
    )
)

# 2 — Validate incoming Bearer tokens on protected endpoints
validator = TokenValidator(app_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")

@app.post("/mcp")
async def mcp_handler(claims: dict = Depends(validator.as_dependency)):
    user_id = validator.get_user_id(claims)
    return {"user": user_id}

Configuration reference

build_oauth_router()

Parameter Type Default Description
app_id str required Azure AD Application (client) ID
tenant_id str required Home tenant ID — used for Graph API calls and single-tenant discovery. Pass the home tenant even in multi-tenant deployments.
client_secret str required Azure AD client secret — used for Graph API calls and returned in DCR responses
api_scope str "access_as_user" Scope name under api://{app_id}/
resource_path str "/mcp" Path to your protected resource — drives /.well-known slugs and the resource field
allowed_tenant_ids list[str] | None None Restrict discovery to specific tenants. None advertises /organizations.
config_redirect_uri_path str "/oauth/callback" Server-relative path returned as redirect_uri in GET /oauth/config

TokenValidator()

Parameter Type Default Description
app_id str required Azure AD Application (client) ID
allowed_tenant_ids list[str] | None None Restrict token acceptance. None accepts all Azure AD tenants.

How it works

Client                    This server               Azure AD / Graph
  │                           │                           │
  │  GET /.well-known/...     │                           │
  │──────────────────────────>│                           │
  │<── auth/token endpoints ──│  (points at Azure AD)     │
  │                           │                           │
  │  POST /register           │                           │
  │──────────────────────────>│  POST /oauth2/token ─────>│
  │                           │<── access_token ──────────│
  │                           │  PATCH /applications ─────>│
  │<── client_id + secret ────│<── 204 ───────────────────│
  │                           │                           │
  │  GET /authorize (→ AAD)   │                           │
  │──────────────────────────────────────────────────────>│
  │<────────────────────── code ──────────────────────────│
  │  POST /token (→ AAD)      │                           │
  │──────────────────────────────────────────────────────>│
  │<──────────────── access_token ────────────────────────│
  │                           │                           │
  │  POST /mcp                │                           │
  │  Authorization: Bearer .. │                           │
  │──────────────────────────>│  GET /discovery/v2.0/keys>│
  │                           │<── JWKS ──────────────────│
  │                           │  Verify signature         │
  │                           │  Check iss binding        │
  │                           │  Check aud                │
  │<─── MCP response ─────────│                           │

Step 3 (auth code flow) and step 4 (token exchange) happen entirely on Microsoft's side — this server is not involved.


Azure AD app registration requirements

  1. Register an app in Azure AD / Entra ID.
  2. Create a Client secret and note it.
  3. Under Expose an API, add a scope (e.g. access_as_user).
  4. Grant the app Application.ReadWrite.OwnedBy Microsoft Graph permission (for automatic redirect URI enrolment via POST /register). Use Application.ReadWrite.All if the app doesn't own itself in your tenant.
  5. Under Authentication, add the following as SPA redirect URIs:
    • https://your-server/oauth/callback
    • Any other redirect URIs your clients use

Multi-tenant deployments

Pass allowed_tenant_ids=None (the default) and use tenant_id as your app's home tenant:

build_oauth_router(
    app_id="...",
    tenant_id="your-home-tenant-id",   # used for Graph API only
    client_secret="...",
    allowed_tenant_ids=None,            # accept tokens from any AAD tenant
)

validator = TokenValidator(
    app_id="...",
    allowed_tenant_ids=None,            # accept tokens from any AAD tenant
)

To restrict to a specific set of tenants:

validator = TokenValidator(
    app_id="...",
    allowed_tenant_ids=["tenant-a", "tenant-b"],
)

Contributing

See CONTRIBUTING.md. All contributions are welcome.


Security

Please report security vulnerabilities privately. See SECURITY.md.


License

MIT © 2026 Lee Pasifull

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_mcp_azure_oauth-1.0.1.tar.gz (18.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_mcp_azure_oauth-1.0.1-py3-none-any.whl (13.1 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_mcp_azure_oauth-1.0.1.tar.gz.

File metadata

  • Download URL: fastapi_mcp_azure_oauth-1.0.1.tar.gz
  • Upload date:
  • Size: 18.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for fastapi_mcp_azure_oauth-1.0.1.tar.gz
Algorithm Hash digest
SHA256 d1c54f612d59a58bac169b87537bf8ab1ea878d9f20682a460df329efbaa0d3a
MD5 c5e78bde944518264d47994e4b4ae47f
BLAKE2b-256 a984cce76ec37c57031d51bb4721e1c8f566bfbe6cdaacf418a93c20ee9db13c

See more details on using hashes here.

File details

Details for the file fastapi_mcp_azure_oauth-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_mcp_azure_oauth-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c98a7e4c78bbfb2e17b5a4aa246faee818ed0d4848be680e5469333e7c430ac4
MD5 3999e145575eb6f0df667562d0fbdef9
BLAKE2b-256 d40878263fbb783bd5f9a2dec12b03672cd95d780736bf7ec3df037168f4cce7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page