Python SDK
One package, two sides of the MCP auth protocol:
| You are building | You need | Start here |
|---|---|---|
| An MCP server that must be protected | Token validation + per-tool RBAC | Protect your MCP server |
| An agent/service that calls a protected server | A token — as itself (M2M) or on behalf of a user | M2M auth · ID-JAG delegation |
Install
pip install authsec-sdk
Latest release: 4.7.0 (PyPI) —
everything in this section (the three M2M auth methods, AgentIdentity,
ID-JAG delegation, the module layout) requires 4.7.0 or newer. Already
installed? Upgrade with pip install -U authsec-sdk.
Requires Python ≥ 3.10.11. Fully typed (py.typed ships with the package).
Package layout (v4.7+)
The package is organized by function; everything commonly needed is also re-exported at the top level:
authsec_sdk/
├── identity/ # AGENT SIDE — AgentIdentity (M2M + ID-JAG), browser_login,
│ # ClientSecretAuth, PrivateKeyJwtAuth, SpiffeSvidAuth,
│ # SpiffeWorkloadIdentity
├── runtime/ # SERVER SIDE — mount_mcp, Config, from_env, validation,
│ # scope matrix, manifest, RFC 9728 metadata
├── client/ # typed errors for parsing AuthSec 401/403 responses
├── integrations/ # framework glue (LangGraph)
├── spiffe/ # SPIFFE Workload API — X.509-SVIDs, mTLS material
└── _legacy/ # original decorator API (back-compat only)
Old flat module paths (authsec_sdk.agent_identity, .core, .ciba_sdk,
.delegation_sdk, .spire_sdk, .spiffe_workload_api) still work but emit
a DeprecationWarning — they're removed in v5.
Protect a server: drop in the wrapper
from fastapi import FastAPI
from authsec_sdk import from_env, mount_mcp
app = FastAPI()
cfg = from_env()
mount_mcp(app, "/mcp", your_existing_handler, cfg)
mount_mcp registers two routes:
/mcp— your handler, wrapped: every request has its bearer token validated andtools/callchecked against the scope policy you configured in AuthSec./.well-known/oauth-protected-resource/mcp— the RFC 9728 metadata that lets OAuth clients discover where to authenticate.
handler accepts a FastMCP instance (auto-detected), a plain async request
handler, or any ASGI app via wrap_asgi_handler.
Full walkthrough with dashboard screenshots:
Protect your MCP server — from pip install
to a launched, protected application.
Call a server: get a token
Machine-to-machine (no user) — three interchangeable credential types:
from authsec_sdk import AgentIdentity, ClientSecretAuth, PrivateKeyJwtAuth, SpiffeSvidAuth
agent = AgentIdentity(ISSUER, CLIENT_ID, auth=ClientSecretAuth("sec_...")) # shared secret
agent = AgentIdentity(ISSUER, CLIENT_ID, auth=PrivateKeyJwtAuth("key.pem", kid="key-1")) # RFC 7523
agent = AgentIdentity(ISSUER, CLIENT_ID, auth=SpiffeSvidAuth(svid)) # SPIFFE SVID
async with agent:
token = await agent.access_for(MCP_URL, requested_scopes=["myapp:read"])
On behalf of a signed-in user (XAA / ID-JAG):
from authsec_sdk import AgentIdentity, browser_login
id_token = await browser_login(issuer=ISSUER, client_id=CLIENT_ID, resource=MCP_URL)
agent = AgentIdentity(ISSUER, CLIENT_ID, client_secret=SECRET, idp_issuer=ISSUER)
async with agent:
token = await agent.access_for(
MCP_URL,
user_session={"subject_token": id_token},
requested_scopes=["myapp:read"],
)
# token: sub = the user, act.client_id = this agent — auditable delegation
Guides: M2M auth (all three methods) · ID-JAG delegation
Configuration (server side)
All fields are documented inline in authsec_sdk.runtime.config.Config. The required ones for a production deploy:
| Field | Source | Notes |
|---|---|---|
issuer | AuthSec base URL | e.g. https://api.authsec.dev |
authorization_server | AuthSec API origin | Defaults to issuer when omitted |
jwks_url | JWKS endpoint | Required for JWT validation modes |
introspection_url | RFC 7662 endpoint | Required for introspection validation modes |
resource_server_id | Application detail panel, field id | UUID |
introspection_client_id | Same value as resource_server_id | Username for Basic auth |
introspection_client_secret | One-time secret from registration | Store in a secret manager |
resource_uri | Application detail panel, field resource_url | Must match the aud claim of issued tokens |
publish_manifest | True recommended | Pushes tool inventory at startup |
Optional but useful:
| Field | Purpose |
|---|---|
policy_mode | REMOTE_REQUIRED (default), REMOTE_WITH_LOCAL_FALLBACK, LOCAL_ONLY, OPEN. |
validation_mode | JWT_AND_INTROSPECT (default), JWT_ONLY, INTROSPECTION_ONLY, JWT_OR_INTROSPECT. |
tool_scopes | Local fallback policy. Required if policy_mode=REMOTE_WITH_LOCAL_FALLBACK. |
tool_scope_suggestions | Per-tool scope hints sent in the manifest payload. |
scope_matrix_ttl | How long fetched policy is cached. Default 30 s — dashboard changes reach a running server within that window. |
Environment-variable config
For container deployments, use from_env():
from authsec_sdk import from_env, mount_mcp
cfg = from_env() # reads AUTHSEC_* env vars
cfg.validate() # raises ValueError on misconfiguration
mount_mcp(app, "/mcp", your_handler, cfg)
Supported env vars (all prefixed AUTHSEC_):
AUTHSEC_ISSUER
AUTHSEC_AUTHORIZATION_SERVER
AUTHSEC_JWKS_URL
AUTHSEC_INTROSPECTION_URL
AUTHSEC_INTROSPECTION_CLIENT_ID
AUTHSEC_INTROSPECTION_CLIENT_SECRET
AUTHSEC_RESOURCE_URI
AUTHSEC_RESOURCE_NAME
AUTHSEC_RESOURCE_SERVER_ID
AUTHSEC_SUPPORTED_SCOPES # space-separated
AUTHSEC_POLICY_MODE # remote_required | remote_with_local_fallback | local_only | open
AUTHSEC_VALIDATION_MODE # jwt_and_introspect | jwt_only | introspection_only | jwt_or_introspect
AUTHSEC_PUBLISH_MANIFEST # true | false
Older aliases such as AUTHSEC_RESOURCE, AUTHSEC_JWKS_URI, AUTHSEC_INTROSPECTION_ENDPOINT, AUTHSEC_INTROSPECTION_ID, and AUTHSEC_INTROSPECTION_SECRET are accepted for compatibility, but new deploys should use the canonical names above.
Common scenarios
Run in observe-only mode
You want the SDK to validate tokens but not block anything while you map scopes. Use PolicyMode.OPEN:
from authsec_sdk import Config, PolicyMode
cfg = Config(
# ... regular fields ...
policy_mode=PolicyMode.OPEN,
)
Open mode logs every request, attaches the validated Principal to the request, but allows all tool calls regardless of scope. Switch back to REMOTE_REQUIRED once every tool is mapped.
Keep serving when AuthSec is briefly unreachable
Use REMOTE_WITH_LOCAL_FALLBACK and ship a baseline tool_scopes map:
from authsec_sdk import Config, PolicyMode
cfg = Config(
# ... regular fields ...
policy_mode=PolicyMode.REMOTE_WITH_LOCAL_FALLBACK,
tool_scopes={
"list_repos": ["github.repo:read"],
"create_issue": ["github.issue:write"],
"health_check": [], # empty list = public
},
)
The SDK prefers remote policy; the local map is used only when the AuthSec scope-matrix fetch fails.
Read the authenticated principal in a downstream handler
# Via request state (FastAPI / Starlette route)
async def my_tool(request: Request):
principal = request.state.authsec_principal
log.info("subject=%s scopes=%s", principal.subject, principal.scopes)
# Or via contextvar in deeply-nested async code
from authsec_sdk import principal_from_context
async def deep_helper():
p = principal_from_context()
if p:
...
The Principal carries subject, issuer, audience (list), scopes (list), claims (raw dict), active (bool).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ValueError: introspection client credentials are required | introspection_url set with no client ID/secret | Set both. They come from the application registration response. |
App refuses to start with REMOTE_REQUIRED initial scope matrix fetch failed | The SDK can't reach the policy endpoint and manifest publishing is not enabled | Check the AuthSec URLs and credentials. For first launch, set AUTHSEC_PUBLISH_MANIFEST=true so the SDK can start, publish tools, and deny tool calls until policy is complete. |
Every request returns 401 invalid_token with audience mismatch | Token's aud claim doesn't match cfg.resource_uri | The OAuth client must request resource=<your resource_uri>. MCP clients with proper discovery do this automatically. |
403 insufficient_scope on a tool you mapped | Scope matrix cached pre-mapping; or the tool name in tools/call doesn't match the manifest | Wait for scope_matrix_ttl to expire (default 30 s) or restart the app. |
| Manifest publish silently fails | Best-effort; failures are logged at WARNING | Check logs for authsec.manifest and confirm credentials. |
Agent-side errors (PendingApprovalError, CredentialInvalidError, …) are
covered in the M2M and
delegation guides.
Reference
- Package on PyPI:
authsec-sdk - Source on GitHub:
sdk-authsec/packages/python-sdk - Working example:
examples/protect_existing_mcp_server.py
The legacy decorator API (protected_by_AuthSec, run_mcp_server_with_oauth) now lives in authsec_sdk._legacy and remains importable from the top level for backward compatibility. New applications should use mount_mcp.