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 normalgist.github.com/...page URL serves HTML and verification fails withparse 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:

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):

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