Skip to main content

Method B — Private-key JWT

No shared secret ever crosses the wire: your service signs a short-lived JWT assertion with its private key; AuthSec verifies it with the public key you publish. Right for enterprise security postures — see the comparison if you're unsure which method fits.

No shared secret ever crosses the wire. Your service signs a short-lived JWT assertion with its private key; AuthSec verifies the signature with the public key you publish. Three preparation steps:

Generate a keypair

openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem

private_key.pem stays on the machine that runs your service. Never commit it, never upload it anywhere.

Build and host the JWKS (the public key as JSON)

AuthSec fetches your public key from a URL, in JWKS format:

{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "key-1",
"n": "noFETcNJsPepUVEweAxoV1eb... ← from your public key",
"e": "AQAB"
}
]
}

Generate it from public_key.pem with this one-time snippet:

from cryptography.hazmat.primitives import serialization
import base64, json

pub = serialization.load_pem_public_key(open("public_key.pem", "rb").read())
n, e = pub.public_numbers().n, pub.public_numbers().e
b64u = lambda i, l: base64.urlsafe_b64encode(i.to_bytes(l, "big")).rstrip(b"=").decode()
print(json.dumps({"keys": [{
"kty": "RSA", "use": "sig", "alg": "RS256", "kid": "key-1",
"n": b64u(n, (n.bit_length() + 7) // 8), "e": b64u(e, 3),
}]}, indent=2))

Host the JSON anywhere public — your own domain (https://example.com/.well-known/jwks.json), an S3 bucket, or a GitHub gist for testing.

⚠️ The JWKS URI must return raw JSON, not an HTML page. With a gist, use the raw URL (gist.githubusercontent.com/.../raw/.../jwks.json) — the normal gist.github.com/... page URL serves HTML and verification fails with parse JWKS: invalid character '<'.

Create the service account with the JWKS URI

In the create dialog pick Private-key JWT — a JWKS URI field appears; paste your URL:

Create service account — private-key JWT + JWKS URI

This time there's no secret to save — just the CLIENT_ID (there's nothing secret between you and AuthSec; your private key is the credential):

Private-key JWT credentials — client id only

Grant access (below), then code:

from authsec_sdk import AgentIdentity, PrivateKeyJwtAuth

agent = AgentIdentity(
ISSUER, PK_CLIENT_ID,
auth=PrivateKeyJwtAuth("private_key.pem", kid="key-1"), # PEM path or PEM string
)
async with agent:
token = await agent.access_for(MCP_URL, requested_scopes=["test_mcp:read"])

The kid must match the kid in your hosted JWKS. Each request signs a fresh assertion — 5-minute lifetime, single-use jti, audience-bound to the token endpoint — so an intercepted assertion is useless.

Key rotation: generate a new pair, add the new key to the JWKS under kid: "key-2", deploy the new private key with kid="key-2", then remove the old entry. No dashboard changes, no downtime.


Shared steps: grant access · verify · troubleshooting