Skip to main content

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 the client ID (Copilot Studio GET-variant)
RFC 7591 POST /register Dynamic Client Registration + Azure AD enrolment of allowlisted redirect URIs
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)
  • Accepts only access tokens: delegated tokens need an scp claim (optionally a specific scope); ID tokens and app-only tokens are rejected unless you opt in via required_roles
  • Rejects non-GUID tenant IDs before any JWKS fetch, and caps the JWKS client cache at 50 tenants with FIFO eviction
  • Offers validate_token_async() so JWKS fetches never block the event loop

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
        base_url="https://mcp.example.com",               # public URL of this server
        allowed_tenant_ids=["yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"],
    )
)

# 2 — Validate incoming Bearer tokens on protected endpoints
validator = TokenValidator(
    app_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    allowed_tenant_ids=["yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"],
    required_scopes=["access_as_user"],
)

@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 only for Graph API calls; never returned by any endpoint
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
base_url str | None None Public base URL of this server. Recommended. Without it, URLs in responses come from the request's Host header and the server's own callback is never enrolled in Azure AD.
allowed_redirect_uris list[str] | None None Exact https:// redirect URIs that POST /register may add to the Azure AD app registration. None enrols no client-supplied URIs.

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.
required_scopes list[str] | None None Delegated tokens must carry at least one of these scopes in scp. None accepts any non-empty scp.
required_roles list[str] | None None Accept app-only (client credentials) tokens carrying at least one of these app roles. None rejects all app-only tokens.

Use await validator.validate_token_async(token) from async code (middleware etc.). as_dependency already does this.


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 ─────────────│<── 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. Configure it directly in your MCP client (e.g. Copilot Studio's connector settings) — this server never hands it out.
  3. Under Expose an API, add a scope (e.g. access_as_user).
  4. Under Authentication, add the following as SPA redirect URIs:
    • https://your-server/oauth/callback
    • Any other redirect URIs your clients use
  5. Optional: to let POST /register enrol redirect URIs automatically, grant the app the Application.ReadWrite.OwnedBy Microsoft Graph permission and list the permitted URIs in allowed_redirect_uris. If you register redirect URIs manually (step 4), skip this — the app then needs no Graph permissions at all.

Upgrading from 1.x

2.0.0 is a security release with breaking changes:

  • GET /register and POST /register no longer return client_secret. Configure the secret in your client directly.
  • POST /register only enrols redirect URIs listed in allowed_redirect_uris, and only enrols the server's own callback when base_url is set.
  • TokenValidator rejects ID tokens, tokens without exp, app-only tokens (unless required_roles is set) and non-GUID tenant IDs.

If you ran 1.x on a reachable server, rotate the client secret and review the app registration's redirect URIs and credentials for anything you did not add.


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
    required_scopes=["access_as_user"],
)

Accepting every tenant means any Microsoft work or school account can obtain a token for your API. Do your own authorisation on the returned claims, or restrict tenants as below.

To restrict to a specific set of tenants:

validator = TokenValidator(
    app_id="...",
    allowed_tenant_ids=[
        "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
        "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
    ],
)

Contributing

See CONTRIBUTING.md. All contributions are welcome.


Security

Please report security vulnerabilities privately. See SECURITY.md.


License

MIT © 2026 Lee Pasifull

Metadata

Release files for fastapi-mcp-azure-oauth 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fastapi-mcp-azure-oauth 2.0.0
File Size Uploaded
fastapi_mcp_azure_oauth-2.0.0.tar.gz 26.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fastapi-mcp-azure-oauth 2.0.0
File Interpreter ABI Platform
fastapi_mcp_azure_oauth-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.7 kB

Release files / fastapi_mcp_azure_oauth-2.0.0.tar.gz

Download URL fastapi_mcp_azure_oauth-2.0.0.tar.gz
Size 26.2 kB
Tags Source
SHA-256 checksum
How to use checksums
1994630972bd2410858177df43a387a8cd0aca642daf6d020d8a3d2a6c86fe3e
BLAKE2b-256 checksum
How to use checksums
6ae4c5058800b52e11d35140ae8440fe119ffa7f0008d0b2a7964f5706930942
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / fastapi_mcp_azure_oauth-2.0.0-py3-none-any.whl

Download URL fastapi_mcp_azure_oauth-2.0.0-py3-none-any.whl
Size 15.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f94a0450b332e1f5dd3cdb033c2bd4b7759ed62a6da28ac3c154115d4241436a
BLAKE2b-256 checksum
How to use checksums
f23179df7b0f0d155c7becce35c3e99bc06ce48cd20166c5aeef4186d9fef631
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page