Skip to content
Settings

auth.trusted-keys — register public keys for offline JWT signing

cyoda-go version 0.8.2

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.

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 (default 10) — per-tenant cap on currently-valid trusted keys. Counts active + within-validity entries only.
  • CYODA_IAM_TRUSTED_KEY_MAX_VALIDITY_DAYS (default 365) — 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 supports kty: "EC" and kty: "OKP"; cyoda-go parity is tracked for a future release.
  • A ROLE_ADMIN cyoda token to register / delete / lifecycle the entry.
Terminal window
# Generate a keypair locally
openssl genrsa -out signing.pem 2048
openssl 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.

Terminal window
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.

Terminal window
# 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}"
Terminal window
curl -X DELETE https://cyoda.example.com/api/oauth/keys/trusted/${KEY_ID} \
-H "Authorization: Bearer ${ADMIN_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:

Terminal window
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:

  • iss must match CYODA_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.
  • aud is checked against CYODA_JWT_AUDIENCE if set.
  • caas_org_id must 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.FEATURE_DISABLED (404) — trusted-key endpoints called with CYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED=false.
  • errors.TRUSTED_KEY_NOT_FOUND (404) — referenced keyId not 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 a keyId that already belongs to another tenant. Pick a fresh keyId.
  • errors.UNSUPPORTED_KEY_TYPE (400) — kty is 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.
  • auth.tokens — universal JWT claim contract
  • config.authCYODA_IAM_TRUSTED_KEY_* env vars
  • openapicyoda help openapi tags and look for the IAM tag’s /oauth/keys/trusted/* operations
  • cyoda help auth — auth — authenticate client applications against cyoda.
  • cyoda help auth tokens — auth.tokens — exchange credentials for a JWT at POST /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_REACHEDPOST /oauth/keys/trusted enforces a per-tenant cap (default 10, configurable via CYODA_IAM_TRUSTED_KEY_MAX_PER_TENANT). The cap counts only currently-valid keys (Active and not past validTo). Delete or invalidate older keys, or raise the cap.
  • cyoda help errors KEY_OWNED_BY_DIFFERENT_TENANT — Trusted keys are tenant-scoped. When POST /oauth/keys/trusted is called with a keyId that already belongs to a different tenant, the request is rejected with 409. Pick a fresh keyId (the caller cannot see or affect the other tenant’s keys).
  • cyoda help errors UNSUPPORTED_KEY_TYPEPOST /oauth/keys/trusted accepts only kty: "RSA" in v0.8.0. Cloud also supports kty: "EC" and kty: "OKP"; cyoda-go parity is tracked in a v0.8.1 follow-up.
  • cyoda help errors FEATURE_DISABLED — Returned by trusted-key endpoints when CYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED=false (the default):