auth.trusted-keys — register public keys for offline JWT signing
cyoda-go version 0.8.2
auth.trusted-keys
Section titled “auth.trusted-keys”auth.trusted-keys — register a public key with cyoda so JWTs you sign offline with the matching private key are accepted, without an IdP or /oauth/token round-trip.
You have a workload that can sign JWTs (a CI job, a controller, an air-gapped service) and you don’t want to depend on cyoda being reachable for token minting or an external IdP for discovery. Register the public key once; thereafter your workload signs JWTs locally and cyoda validates them by kid lookup against the registry.
Use this path when token minting must work even when cyoda’s /oauth/token is unreachable, or when you want zero IdP infrastructure.
Feature flag. The 5 trusted-key endpoints under /oauth/keys/trusted/* are off by default. The operator must set CYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED=true to enable them; otherwise every endpoint returns 404 FEATURE_DISABLED. This is intentional — trusted keys move the trust boundary, and that posture should be explicit.
PREREQUISITES
Section titled “PREREQUISITES”Admin (cyoda operator) sets up:
CYODA_IAM_MODE=jwt.CYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED=true(gate; see callout above).CYODA_IAM_TRUSTED_KEY_MAX_PER_TENANT(default10) — per-tenant cap on currently-valid trusted keys. Counts active + within-validity entries only.CYODA_IAM_TRUSTED_KEY_MAX_VALIDITY_DAYS(default365) — default validity for trusted keys when not specified at registration.
Client (you) needs:
- A keypair you generated yourself. cyoda-go v0.8.0 supports
kty: "RSA"only. Cloud also supportskty: "EC"andkty: "OKP"; cyoda-go parity is tracked for a future release. - A
ROLE_ADMINcyoda token to register / delete / lifecycle the entry.
REQUEST FLOW
Section titled “REQUEST FLOW”Register a public key
Section titled “Register a public key”# Generate a keypair locallyopenssl genrsa -out signing.pem 2048openssl rsa -in signing.pem -pubout -out signing.pub# Convert the public key to JWK shape — your tooling of choice;# the API expects the JWK members at the top level.
curl -X POST https://cyoda.example.com/api/oauth/keys/trusted \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "keyId": "my-signing-key-2026-06", "kty": "RSA", "n": "<base64url-modulus>", "e": "AQAB" }'Response (200 OK) echoes the registered key shape plus lifecycle metadata.
Pick a stable, descriptive keyId. It becomes the JWT kid header you must set when signing.
List trusted keys
Section titled “List trusted keys”curl -X GET https://cyoda.example.com/api/oauth/keys/trusted \ -H "Authorization: Bearer ${TOKEN}"Returns the tenant’s keys with status (active / invalidated) and validity window.
Invalidate / reactivate
Section titled “Invalidate / reactivate”# Stop accepting tokens signed with this key, without removing the entry.curl -X POST https://cyoda.example.com/api/oauth/keys/trusted/${KEY_ID}/invalidate \ -H "Authorization: Bearer ${ADMIN_TOKEN}"
# Re-enable.curl -X POST https://cyoda.example.com/api/oauth/keys/trusted/${KEY_ID}/reactivate \ -H "Authorization: Bearer ${ADMIN_TOKEN}"Delete
Section titled “Delete”curl -X DELETE https://cyoda.example.com/api/oauth/keys/trusted/${KEY_ID} \ -H "Authorization: Bearer ${ADMIN_TOKEN}"Sign and present a token
Section titled “Sign and present a token”Your workload signs a JWT with the matching private key, setting kid to the registered keyId:
Header: { "alg": "RS256", "typ": "JWT", "kid": "my-signing-key-2026-06" }Payload: { universal cyoda claim contract — see auth.tokens }Present it like any other cyoda token:
curl -X GET https://cyoda.example.com/api/clients \ -H "Authorization: Bearer ${SIGNED_JWT}"cyoda looks up kid in the trusted-key registry, validates the RS256 signature against the registered public key, then enforces the universal claim checks (iss, aud, exp, …).
JWTs you sign with a trusted-key private key must conform to the universal cyoda claim contract documented in auth.tokens. In particular:
issmust matchCYODA_JWT_ISSUER(typically the value you set on the cyoda deployment) — trusted-key tokens are not issuer-bound to your IdP because there is no IdP.audis checked againstCYODA_JWT_AUDIENCEif set.caas_org_idmust match the tenant that registered the key.
Cyoda does not mint trusted-key JWTs — you sign them. This page covers the registration + lifecycle of the public key; the claim contract is in auth.tokens.
ERRORS
Section titled “ERRORS”errors.FEATURE_DISABLED(404) — trusted-key endpoints called withCYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED=false.errors.TRUSTED_KEY_NOT_FOUND(404) — referencedkeyIdnot in the registry (also returned for cross-tenant access — the existence of another tenant’s key is never confirmed).errors.TRUSTED_KEY_CAP_REACHED(400) — per-tenant cap reached; delete or invalidate an old key first.errors.KEY_OWNED_BY_DIFFERENT_TENANT(409) — registration request specifies akeyIdthat already belongs to another tenant. Pick a freshkeyId.errors.UNSUPPORTED_KEY_TYPE(400) —ktyis not"RSA"(the only type cyoda-go v0.8.0 accepts).errors.UNAUTHORIZED(401) — caller lacks a valid bearer for the management call; or, at validation time, the signing key is unknown / invalidated / expired.
SEE ALSO
Section titled “SEE ALSO”auth.tokens— universal JWT claim contractconfig.auth—CYODA_IAM_TRUSTED_KEY_*env varsopenapi—cyoda help openapi tagsand look for theIAMtag’s/oauth/keys/trusted/*operations
See also
Section titled “See also”cyoda help auth— auth — authenticate client applications against cyoda.cyoda help auth tokens— auth.tokens — exchange credentials for a JWT atPOST /api/oauth/token. Covers every supported grant and the canonical JWT claim contract that all cyoda tokens (M2M, OBO, federated OIDC, trusted-key) conform to.cyoda help config auth— config.auth — IAM mode, JWT issuer, HMAC secret, and admin bootstrap controls.cyoda help errors TRUSTED_KEY_NOT_FOUND— Returned by trusted-key admin endpoints when the supplied KID does not match any registered key:cyoda help errors TRUSTED_KEY_CAP_REACHED—POST /oauth/keys/trustedenforces a per-tenant cap (default 10, configurable viaCYODA_IAM_TRUSTED_KEY_MAX_PER_TENANT). The cap counts only currently-valid keys (Active and not pastvalidTo). Delete or invalidate older keys, or raise the cap.cyoda help errors KEY_OWNED_BY_DIFFERENT_TENANT— Trusted keys are tenant-scoped. WhenPOST /oauth/keys/trustedis called with akeyIdthat already belongs to a different tenant, the request is rejected with409. Pick a freshkeyId(the caller cannot see or affect the other tenant’s keys).cyoda help errors UNSUPPORTED_KEY_TYPE—POST /oauth/keys/trustedaccepts onlykty: "RSA"in v0.8.0. Cloud also supportskty: "EC"andkty: "OKP"; cyoda-go parity is tracked in a v0.8.1 follow-up.cyoda help errors FEATURE_DISABLED— Returned by trusted-key endpoints whenCYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED=false(the default):
Raw formats
Section titled “Raw formats”/help/auth/trusted-keys.json— full descriptor (matchesGET /help/{topic}envelope)/help/auth/trusted-keys.md— body only