Skip to main content

Machine-to-machine auth (Python SDK)

A pipeline, a cron job, a backend service, a Kubernetes workload — any program that calls a protected MCP server as itself, with its own standing permissions, no user session.

Dashboard walkthrough

For the step-by-step dashboard flow with screenshots (create the service account, grant a role, verify the connection), see Connect agents and services.

The three credential classes

All three methods end at the same place — POST /oauth/token (client_credentials) → scoped access token. They differ only in how the machine proves its identity:

MethodProofSecret on the wire?Best for
A. Client secretID + shared secret (HTTP Basic)⚠️ every requestquick starts, simple deployments
B. Private-key JWTRS256-signed assertion (RFC 7523)never — key stays localenterprise security postures
C. SPIFFE SVIDplatform-attested workload identityno stored credential at allKubernetes

Security ladder: A → B → C goes from "shared password" to "asymmetric keys" to "the infrastructure itself vouches for the workload".

Unified API — AgentIdentity

In the SDK all three are interchangeable credential classes. The caller code is identical; only the auth= parameter changes:

from authsec_sdk import AgentIdentity, ClientSecretAuth, PrivateKeyJwtAuth, SpiffeSvidAuth

# Method A — shared secret
agent = AgentIdentity(ISSUER, CLIENT_ID, auth=ClientSecretAuth("sec_..."))

# Method B — asymmetric key (key never leaves your machine)
agent = AgentIdentity(ISSUER, CLIENT_ID, auth=PrivateKeyJwtAuth("key.pem", kid="key-1"))

# Method C — platform attestation (zero stored credentials)
agent = AgentIdentity(ISSUER, CLIENT_ID, auth=SpiffeSvidAuth(svid))

# All three use the same token-acquisition call:
async with agent:
token = await agent.access_for(MCP_URL, requested_scopes=["my_mcp:read"])

Per-method guides

MethodSDK guideDashboard guide
A. Client secretPython codeDashboard steps
B. Private-key JWTPython codeDashboard steps
C. SPIFFE SVIDPython codeDashboard steps

SDK class reference

AgentIdentity

The core class for all M2M and ID-JAG flows. Manages token acquisition, caching, and refresh.

from authsec_sdk import AgentIdentity

agent = AgentIdentity(
issuer, # str — the OAuth issuer URL
client_id, # str — the service account or agent ID
auth=..., # ClientSecretAuth | PrivateKeyJwtAuth | SpiffeSvidAuth
client_secret=..., # str (optional) — used for ID-JAG agents instead of auth=
idp_issuer=..., # str (optional) — the IdP issuer for ID-JAG token exchange
)
MethodWhat it does
await agent.access_for(mcp_url, requested_scopes=[...])Acquire a scoped access token for the target MCP server. Returns the token string. Caches until near expiry.
await agent.access_for(mcp_url, user_session={...}, requested_scopes=[...])ID-JAG variant -- exchanges a user's id_token for a delegated token.
agent.clear_cache(mcp_url)Drop the cached token for this server. Use after a ConnectionRevokedError.
async with agent:Context manager -- opens and closes the internal HTTP session.

ClientSecretAuth

from authsec_sdk import ClientSecretAuth

auth = ClientSecretAuth(client_secret) # str — the 64-char hex secret

Sends the client ID and secret as HTTP Basic credentials on each client_credentials grant. Simplest method; secret crosses the wire on every request.

PrivateKeyJwtAuth

from authsec_sdk import PrivateKeyJwtAuth

auth = PrivateKeyJwtAuth(
private_key, # str — path to PEM file, or the PEM string itself
kid="key-1", # str — must match the kid in your hosted JWKS
)

Signs a fresh JWT assertion per token request (5-min lifetime, unique jti, audience = token endpoint). No secret crosses the wire -- AuthSec verifies the signature against your public JWKS.

SpiffeSvidAuth

from authsec_sdk import SpiffeSvidAuth

auth = SpiffeSvidAuth(svid) # str — a JWT-SVID from SPIRE

Low-level class for manual SVID usage. Does not auto-renew -- use SpiffeWorkloadIdentity in production (see below).

SpiffeWorkloadIdentity

from authsec_sdk import SpiffeWorkloadIdentity, SpiffeConfig

spiffe = SpiffeWorkloadIdentity(SpiffeConfig(
mcp_server_url="https://...",
client_id="...",
spiffe_id="spiffe://...",
scopes=["my_mcp:read"],
agent_socket_path="/run/spire/sockets/agent.sock", # default
))

High-level class that fetches SVIDs from the SPIRE agent socket and renews them automatically. Use this inside a Kubernetes pod.

MethodWhat it does
await spiffe.access_for()Mint (or reuse) an SVID, exchange it for an access token.
async with spiffe:Context manager for the socket connection.

ManifestTool

Used in tool_inventory_provider to declare tools for the manifest:

from authsec_sdk import ManifestTool

ManifestTool(
name="add_no", # str — the tool name (must match @mcp.tool)
description="Add numbers", # str — shown in the dashboard
input_schema={...}, # dict — JSON Schema for the tool's parameters
annotations=None, # dict (optional) — MCP annotations (readOnlyHint, etc.)
suggested_scopes=None, # list[str] (optional) — scope hints for the dashboard
)

ID-JAG helper functions

from authsec_sdk import browser_login, poll_until_approved
FunctionWhat it does
await browser_login(issuer, client_id, resource)Opens the browser for user login + consent. Returns an id_token. Runs a local HTTP server on localhost:8126/callback for the redirect.
await poll_until_approved(agent, mcp_url, status_url, user_session, requested_scopes)Polls AuthSec until the admin approves the agent's first connection. Returns the access token.

ID-JAG exceptions

ExceptionWhen it's raised
PendingApprovalErrorAgent's first connection -- admin approval pending. Has a .status_url for polling.
ApprovalDeniedErrorAdmin declined the connection request.
ConnectionRevokedErrorA previously approved connection was revoked.
TrustedIssuerMissingErrorThe IdP isn't configured as a trusted issuer in AuthSec.
CredentialInvalidErrorAgent's client_id or secret is wrong.

Grant access — required for every method

Creating a service account gives it an identity, not permissions. Until you grant it a role on the target application, every token request fails with:

access_denied: client not authorized for this resource server

Grant a role from the application's Access tab → Add accessMachine credential (secret/key). Full walkthrough: Connect — client secret.

Verify

Run the code from any method, then prove the token works:

import httpx

async def tools_list(token: str):
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(MCP_URL,
json={"jsonrpc": "2.0", "method": "tools/list", "id": 1},
headers={"Authorization": f"Bearer {token}",
"Accept": "application/json, text/event-stream"})
return r

You should see only the tools your granted scopes allow.

Troubleshooting

ErrorCauseFix
invalid_client: invalid client secretTypo'd/rotated secret (64 hex chars)Copy-paste from the dashboard, never retype
access_denied: client not authorized for this resource serverCredential valid, but no role grantedGrant a role on the target application
JWKS resolution failed: parse JWKS: invalid character '<'JWKS URI returns HTML (gist page URL, 404)Use the raw JSON URL
invalid_client: token aud must include this token endpointSPIFFE SVID minted with wrong audienceMint with audience = <issuer>/oauth/token
invalid_client: … token is expiredJWT-SVIDs live ~5 minMint right before use, or use SpiffeWorkloadIdentity
Signature verification fails (private-key JWT)kid mismatch between code and JWKSMake PrivateKeyJwtAuth(kid=...) match the JWKS kid