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.0.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.0-py3-none-any.whl (13.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: fastapi_mcp_azure_oauth-1.0.0.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.0.tar.gz
Algorithm Hash digest
SHA256 7002270766088df4807e0fc5f87607c6db47f20f14dda1245779fd481b9b6985
MD5 96e8a3598361ece0f2dd800f1ff8e0c4
BLAKE2b-256 08ae5dc8e21c08d85c55417b5c624e998517c0bfea237062cf2291202ca7cefa

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for fastapi_mcp_azure_oauth-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0f45794fecf90613dae11057b9312fe1e9f5a4ba01457762eba40c3866efec13
MD5 75e864f1036bb37e0dd35e67ec0d90a0
BLAKE2b-256 04256ba324a47a0247bacf3fd0f4a5b6bf1f0c26e3d5f79109205f90ffd48f17

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