Skip to main content

namoid

PyPI version Python versions License: MIT

Python SDK for NamoID — enterprise identity for India (OAuth 2.1 / OpenID Connect): SSO, MFA, passkeys, and India verification (DigiLocker, Aadhaar, WhatsApp OTP, Truecaller) with a DPDP-ready audit trail.

Install

pip install namoid

Hosted Auth needs only the base install. Protecting an MCP server adds an extra:

pip install "namoid[fastmcp]"   # protect a FastMCP server (Python 3.11+)
pip install "namoid[mcp]"       # the same core, without FastMCP

Python 3.10 or newer. The FastMCP extra requires 3.11.

Pick what you need

The two surfaces are independent. Take either, both, or neither — you pay only for what you name.

You want Install Import Pulled in
Hosted Auth namoid namoid httpx
Protect an MCP server namoid[mcp] namoid.mcp httpx, joserfc
…on FastMCP namoid[fastmcp] namoid.mcp.fastmcp the above, fastmcp

import namoid loads neither surface. The top-level names resolve on first use (PEP 562), so:

  • A Hosted Auth application never imports the MCP code and never needs its extra.
  • An MCP server never imports the Hosted Auth client.
  • The MCP core (namoid.mcp) imports no MCP framework at all — fastmcp appears only inside namoid.mcp.fastmcp, so the core also backs the official MCP Python SDK, a bare Starlette app, or a test.

dir(namoid) and namoid.__all__ still list the full surface, and type checkers resolve every export, so laziness costs nothing in editor support. These boundaries are enforced by tests in tests/test_modularity.py, each run in a fresh interpreter, rather than left as an intention.

Everything raises NamoIDError, including the MCP errors, so one except catches both surfaces without importing the one you do not use.

Hosted Auth

Hosted Auth uses standard OpenID Connect Authorization Code flow with S256 PKCE. The Client ID resolves the application and issuer; discovery supplies the authorization, token, UserInfo, revocation, JWKS, and logout endpoints.

from namoid import NamoIDClient

namoid = NamoIDClient(
    client_id=os.environ["NAMOID_CLIENT_ID"],
    client_secret=os.environ["NAMOID_CLIENT_SECRET"],   # server-side only
)

# 1. Start a state-, nonce-, and PKCE-bound transaction. Keep it server-side.
transaction = namoid.create_oidc_transaction("https://app.example/auth/callback")
session["namoid_state"] = transaction.state
session["namoid_nonce"] = transaction.nonce
session["namoid_verifier"] = transaction.code_verifier

# 2. Send the browser to the discovered authorization endpoint.
url = namoid.authorization_url(transaction)

# 3. On the callback, compare state, then exchange using the same redirect URI.
tokens = namoid.exchange_code(
    code=request.args["code"],
    code_verifier=session.pop("namoid_verifier"),
    redirect_uri="https://app.example/auth/callback",
)

# 4. Verify the ID token signature and callback-bound nonce, then fetch UserInfo.
claims = namoid.validate_id_token(tokens.raw["id_token"],
                                  nonce=session.pop("namoid_nonce"))
user = namoid.user_info(tokens.access_token)
assert claims["sub"] == user["sub"]

# 5. On sign-out, revoke the refresh token and redirect through provider logout.
namoid.revoke_token(tokens.refresh_token, token_type_hint="refresh_token")
url = namoid.logout_url(id_token_hint=tokens.raw["id_token"],
                        post_logout_redirect_uri="https://app.example/signed-out")

AsyncNamoIDClient has exactly the same methods with await, for FastAPI, Starlette, or any async framework:

from namoid import AsyncNamoIDClient

async with AsyncNamoIDClient(client_id=..., client_secret=...) as namoid:
    tokens = await namoid.exchange_code(
        code=code, code_verifier=verifier,
        redirect_uri="https://app.example/auth/callback",
    )

Both accept an http_client if you want to supply your own configured httpx.Client / httpx.AsyncClient, and cache the auth config after the first fetch.

For a public client, omit client_secret; PKCE protects the code exchange. For a confidential web application, the SDK sends the secret using HTTP Basic authentication at the discovered token endpoint. Never put a Client Secret anywhere a browser can reach.

Method Endpoint
get_auth_config() GET /v1/auth/config
get_oidc_discovery() issuer /.well-known/openid-configuration
authorization_url(...) discovered authorization endpoint
exchange_code(...) discovered token endpoint
refresh(...) discovered token endpoint
user_info(...) discovered UserInfo endpoint
validate_id_token(...) discovered JWKS endpoint; local verification
revoke_token(...) discovered revocation endpoint
logout_url(...) discovered end-session endpoint
validate_access_token(...) POST /v1/auth/tokens/validate

The older hosted_auth_url(...) and revoke_session(...) helpers remain for applications using NamoID's legacy Hosted Auth contract.

Every failure raises NamoIDError, carrying status, code (the API's own error code when present), and the parsed detail.

namoid.hosted_auth exposes the pure pieces — create_hosted_auth_transaction, build_hosted_auth_url, build_configured_hosted_auth_url, pkce_challenge, random_base64url — if you would rather drive the flow yourself.

Protect an MCP server

NamoID is the authorization server. Your MCP server is the protected resource. An MCP host — Claude, ChatGPT, Cursor, VS Code — is the OAuth client, and the signed-in human is the resource owner. NamoID authenticates that human, records consent, and issues a short-lived token limited to your server and to the actions approved; this package validates and enforces it.

No NamoID credentials are needed: a resource server only consumes public discovery metadata and JWKS.

from fastmcp import FastMCP
from namoid.mcp.fastmcp import create_namoid_auth, current_caller, require_namoid_scopes

# Console -> Environment -> MCP Authorization -> Integration details.
# Discovery runs here, so a wrong issuer or resource fails at startup rather
# than as an opaque 401 on the first tool call.
auth = create_namoid_auth(
    issuer="https://acme-test.id.namoid.in",
    resource="https://mcp.acme.example/mcp",   # exactly as registered as the audience
    resource_name="Acme Finance MCP",
    scopes_supported=["customers:read", "invoices:read"],
)

mcp = FastMCP(name="acme-finance-mcp", auth=auth.provider)


@mcp.tool
@require_namoid_scopes(auth, "refunds:create")
def issue_refund(invoice_id: str, amount_minor: int) -> dict:
    caller = current_caller(auth)          # the NamoID user who consented
    assert_refund_allowed(caller.subject, invoice_id, amount_minor)
    return refund(invoice_id, amount_minor)


if __name__ == "__main__":
    # The path comes from the resource URL, so the endpoint matches the audience.
    mcp.run(transport="http", path=auth.mcp_path)

A scope is permission to attempt an action. refunds:create does not mean this user may refund another organization's invoice or exceed your refund policy. Ownership, limits, and every other business rule stay in the handler.

When a scope is missing, the tool answers with an insufficient_scope result naming the missing scopes, the resource, and the metadata URL — what a host needs to start incremental authorization. The tool stays visible in tools/list, because hiding it would leave the host unable to ask for access. Use FastMCP's own require_scopes when hiding a capability is the goal.

What it validates

Every token must satisfy all of:

Check Why
RS256 from the environment's JWKS The only algorithm NamoID issues
Exact iss A token from another issuer is not yours
Exact aud A token minted for MCP server A must fail on server B
exp / nbf, 30s tolerance Configurable via clock_tolerance_seconds
token_use == "access" An ID token must never be an API token
sub and client_id present Without sub, every caller is one identity

Discovery also checks that the issuer's metadata declares the issuer you asked for — RFC 9700 mix-up defence — and reads jwks_uri from it rather than hard-coding a key location. The key set is cached, and refetched when a token arrives with an unrecognised kid so key rotation is picked up without a restart.

Without FastMCP

namoid.mcp imports no MCP framework, so it can back the official MCP Python SDK, a bare Starlette app, or a test:

from namoid.mcp import create_namoid_mcp_auth, NamoIDMcpTokenError

auth = await create_namoid_mcp_auth(
    issuer="https://acme-test.id.namoid.in",
    resource="https://mcp.acme.example/mcp",
    scopes_supported=["invoices:read"],
)

# Publish auth.protected_resource_metadata at auth.metadata_path (RFC 9728),
# and answer an unauthenticated call with a WWW-Authenticate challenge
# pointing at auth.metadata_url.
try:
    caller = await auth.verify_access_token(bearer_token)
except NamoIDMcpTokenError:
    ...  # 401 + challenge

create_namoid_mcp_auth_sync is the blocking form, for servers built at module import where there is no event loop to await on.

Client onboarding

NamoID resolves MCP clients by pre-registration or Client ID Metadata Document (CIMD). Dynamic Client Registration is not available for customer-owned MCP resources, so a host that can only do DCR cannot connect yet.

Examples

Complete runnable servers, including a TypeScript equivalent: namoid-examples/mcp-authorization.

For the rest of the SDK, see the integration guides and API reference at docs.namoid.in.

Develop

python -m venv .venv && .venv/bin/pip install -e ".[fastmcp]" pytest pytest-asyncio
.venv/bin/python -m pytest tests/ -q

Links

License

MIT © PolyMindsLabs Pvt. Ltd.

Metadata

Release files for namoid 0.2.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 namoid 0.2.0
File Size Uploaded
namoid-0.2.0.tar.gz 40.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for namoid 0.2.0
File Interpreter ABI Platform
namoid-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 71.1 kB

Release files / namoid-0.2.0.tar.gz

Download URL namoid-0.2.0.tar.gz
Size 40.6 kB
Tags Source
SHA-256 checksum
How to use checksums
64054be96ace79408ac7b38479dfac720cf7889f7e8a5d4707c99080e746625a
BLAKE2b-256 checksum
How to use checksums
881ba24e52ff973914398baddde23850fd4ab68ee2139e0dd7fc23fd9b66e37b
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 Sep 1, 2026.

Transparency log

Release files / namoid-0.2.0-py3-none-any.whl

Download URL namoid-0.2.0-py3-none-any.whl
Size 30.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
07a52ff344f1934b300df48776e89522ad7d621f03f1f3722abfc35fbb9673ff
BLAKE2b-256 checksum
How to use checksums
b200b6daa6908bc7b489d19a9a5d33dee281cdc6aa75f364b6ef14c65f1e45a7
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 Sep 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.0.2

2 release files

0.0.1

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