auth.oidc — federated OIDC providers
cyoda-go version 0.8.2
auth.oidc
Section titled “auth.oidc”auth.oidc — register an external OIDC identity provider so JWTs it issues are accepted by cyoda directly, without re-minting at /oauth/token.
You already have an OIDC IdP — Cognito, Keycloak, Auth0, Okta, your own — and you want clients to present that IdP’s JWTs to cyoda. Register the provider once; cyoda fetches its JWKS, validates inbound tokens against the keys, maps roles from the configured claim, and binds the resulting identity to a tenant.
Use this path when the IdP is the source of truth for user accounts. For pure M2M, auth.clients + auth.tokens is simpler.
PREREQUISITES
Section titled “PREREQUISITES”Admin (cyoda operator) sets up:
CYODA_IAM_MODE=jwt(federated OIDC is unavailable in mock mode).CYODA_OIDC_REQUIRE_HTTPS=truefor production. Set tofalseonly for dev IdPs over plain HTTP.CYODA_OIDC_ALLOW_PRIVATE_NETWORKS=falsefor production. Setting totruedisables the SSRF blocklist that preventswellKnownConfigUriresolving to RFC 1918 / loopback / link-local addresses.CYODA_OIDC_ROLES_CLAIM(defaultroles) — global default JWT claim from which roles are read. Per-provider override available at registration.CYODA_OIDC_CONNECT_TIMEOUT_MS,CYODA_OIDC_SOCKET_TIMEOUT_MS,CYODA_OIDC_CONNECTION_REQUEST_TIMEOUT_MS(each default5000) — HTTP timeouts for discovery + JWKS fetches.
Client (you) needs:
- A working
.well-known/openid-configurationURL on the IdP. - A
ROLE_ADMINcyoda token to register the provider. - A UUID-shaped tenant ID — the bootstrap convenience literal
default-tenantis rejected by registration (returnsOIDC_INVALID_TENANT) because non-UUID tenant identifiers collide in storage.
REQUEST FLOW
Section titled “REQUEST FLOW”Register a provider
Section titled “Register a provider”curl -X POST https://cyoda.example.com/api/oauth/oidc/providers \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "wellKnownConfigUri": "https://idp.example.com/.well-known/openid-configuration", "issuers": ["https://idp.example.com/"], "expectedAudiences": ["cyoda-prod"], "rolesClaim": "cognito:groups" }'Response (200 OK) carries the full provider DTO:
{ "id": "f47ac10b-58cc-…", "wellKnownConfigUri": "https://idp.example.com/.well-known/openid-configuration", "active": true, "createdAt": "2026-06-17T12:00:00Z", "issuers": ["https://idp.example.com/"], "expectedAudiences": ["cyoda-prod"], "rolesClaim": "cognito:groups"}Registration succeeds even when the IdP’s discovery / JWKS endpoints are unreachable — cyoda warms them asynchronously after the response. Tokens issued by an un-warmed provider fail with 401 UNAUTHORIZED until the next warmup cycle (or an explicit /reload). See DIAGNOSTICS below for the operator path.
Behaviour on expectedAudiences:
- Omitted,
null, or empty array → noaudenforcement (issuer-binding is the trust anchor). - Non-empty → inbound JWT
audclaim must match at least one entry byte-wise.
Behaviour on issuers:
- Omitted,
null, or empty → cyoda enforces theissclaim matches the discovery document’sissuerfield byte-wise (OIDC Core 1.0 §2). - Non-empty → inbound JWT
issmust match one of the listed values byte-wise.
List providers
Section titled “List providers”curl -X GET "https://cyoda.example.com/api/oauth/oidc/providers?activeOnly=true" \ -H "Authorization: Bearer ${TOKEN}"activeOnly=true filters out invalidated providers. Available to any authenticated tenant member, not just admin.
Update a provider (tri-state PATCH)
Section titled “Update a provider (tri-state PATCH)”curl -X PATCH https://cyoda.example.com/api/oauth/oidc/providers/${PROVIDER_ID} \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "expectedAudiences": ["cyoda-prod", "cyoda-staging"] }'Per-field tri-state semantics: absent = unchanged, null or [] = clear, value = set. Same for issuers and rolesClaim.
Invalidate / reactivate / delete
Section titled “Invalidate / reactivate / delete”# Invalidate — token validation stops accepting this provider's JWTs immediately.curl -X POST https://cyoda.example.com/api/oauth/oidc/providers/${PROVIDER_ID}/invalidate \ -H "Authorization: Bearer ${ADMIN_TOKEN}"
# Reactivate — by default refreshes JWKS from upstream.curl -X POST https://cyoda.example.com/api/oauth/oidc/providers/${PROVIDER_ID}/reactivate \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"reactivateKeys": true}'
# Delete permanently.curl -X DELETE https://cyoda.example.com/api/oauth/oidc/providers/${PROVIDER_ID} \ -H "Authorization: Bearer ${ADMIN_TOKEN}"Reload JWKS for all providers
Section titled “Reload JWKS for all providers”curl -X POST https://cyoda.example.com/api/oauth/oidc/providers/reload \ -H "Authorization: Bearer ${ADMIN_TOKEN}"Forces an immediate JWKS refresh for every active provider on the receiving node. In a multi-node cluster the reload is broadcast.
Present an IdP-issued JWT
Section titled “Present an IdP-issued JWT”Once registered, clients present the IdP’s JWT directly:
curl -X GET https://cyoda.example.com/api/clients \ -H "Authorization: Bearer ${IDP_ISSUED_JWT}"The token is validated against the provider’s JWKS; roles are read from the configured rolesClaim; identity is bound to the tenant that registered the provider.
JWTs issued by the federated IdP must conform to the universal cyoda claim contract documented in auth.tokens. In particular:
-
issmust match per theissuers/ discovery-document rule above. -
audmust matchexpectedAudiencesif set. -
The configured
rolesClaim(per-provider override orCYODA_OIDC_ROLES_CLAIM) is looked up as a literal top-level JWT claim name. The value at that key may be any of:- JSON array of strings —
["admin","warehouse"]→ used as-is. - JSON object —
{ "admin": {…}, "warehouse": {…} }→ roles are the top-level keys (["admin","warehouse"]); inner values are ignored. This is the shape Zitadel emits forurn:zitadel:iam:org:project:roleswithprojectRoleAssertion=true. - String —
"admin warehouse"→ split on whitespace per RFC 6749 §3.3 / RFC 8693 §4.2; a lone token"admin"yields one role.
Empty / absent / non-collection scalar (number, bool) → no roles, no error (the user is authenticated but unprivileged). Role names containing a comma are dropped silently (cyoda comma-joins roles for downstream serialisation; no major IdP emits commas in role names by convention).
Common per-IdP values for
rolesClaim: - JSON array of strings —
IdP rolesClaim value ------------------------------- ----------------------------------------------- Default (no override) roles Amazon Cognito cognito:groups Zitadel (project-wide) urn:zitadel:iam:org:project:roles Zitadel (per-project) urn:zitadel:iam:org:project:<projectId>:roles Auth0 (namespaced custom claim) https://your-app.example.com/rolesNote: there is no path/dot-walking syntax — whatever string you configure is used as a single literal claim-key lookup against the JWT, so colons, slashes, and dots inside the value are treated as part of the key name (which is exactly what Zitadel, Cognito, and Auth0 need).
Keycloak’s default realm_access.roles lives one level deep inside the realm_access object, which the no-dot-walking rule means cyoda cannot reach. Configure a Keycloak token mapper (hardcoded-claim or role-list, at realm or client scope) to flatten the role list to a top-level roles claim, then point rolesClaim at that top-level key.
Cyoda does not re-mint federated tokens — they are validated and trusted directly.
DIAGNOSTICS
Section titled “DIAGNOSTICS”Every token-validation failure surfaces as 401 UNAUTHORIZED with a uniform problem-detail body. No precise error code distinguishes audience mismatch, expired token, unknown kid, JWKS-unreachable, claims-invalid, or ambiguous-tenant routing. A precise wire code would enumerate IdP / tenant / kid / claim-shape recognition to an unauthenticated caller, so the wire stays uniform by design.
The diagnostic path for every failure mode is server-side logs, not the HTTP response. Look at oidc.registry (KID resolution), oidc.validator (claims + signature), and oidc.discovery / oidc.jwks (transport) events. The bearer-auth middleware emits one structured WARN per failed request with the failure reason slug.
Symptom → cause map for the common cases:
- “I get 401 but my token looks valid”: check
oidc.registryresolve logs. The most common cause is the provider’s JWKS hasn’t warmed yet (registration succeeds before discovery completes); force-warm via/oauth/oidc/providers/reload. - “After re-registering, valid tokens are rejected”: the second-most-common case — two tenants registered the same IdP with overlapping or empty
expectedAudiences. InternallyErrAmbiguousProvider(internal/auth/oidc/registry.go) wraps toErrUnknownKID. To resolve, pick disjointexpectedAudiencesper tenant. - “Tokens reject for the right tenant but the IdP is up”: verify the JWT
issclaim matches either an explicitissuersentry or the discovery document’sissuerbyte-for-byte. Trailing slashes count.
Registration-time failures (these DO carry precise codes — see ERRORS) are SSRF, duplicate URI per tenant, malformed tenant UUID, and provider-not-found / invalid-state for lifecycle ops. Registration succeeds even when the IdP itself is unreachable — discovery failures don’t fail the response; they delay the warmup.
ERRORS
Section titled “ERRORS”Registration / lifecycle (precise codes — admin-facing surface):
errors.OIDC_INVALID_TENANT(400) — caller’s tenant ID is not UUID-shaped (commonly: bootstrapdefault-tenantliteral).errors.OIDC_SSRF_BLOCKED(400) —wellKnownConfigUriresolves to a blocked address range (setCYODA_OIDC_ALLOW_PRIVATE_NETWORKS=truefor dev).errors.OIDC_PROVIDER_DUPLICATE(409) — samewellKnownConfigUrialready registered for this tenant.errors.OIDC_PROVIDER_NOT_FOUND(404) — referenced provider ID absent in this tenant.errors.OIDC_PROVIDER_INACTIVE(409) — update or operation attempted on an invalidated provider; reactivate first.
Token validation (always opaque — see DIAGNOSTICS):
errors.UNAUTHORIZED(401) — every token-validation failure path. The wire body never distinguishes audience mismatch, expired token, unknownkid, JWKS-unreachable, claims-invalid, or ambiguous-tenant routing.
SEE ALSO
Section titled “SEE ALSO”auth.tokens— universal claim contractconfig.auth—CYODA_OIDC_*env varsopenapi—cyoda help openapi tagsand look forOAuth, OIDC Providers
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 OIDC_INVALID_TENANT— cyoda treats legal entity identifiers as UUIDs. OIDC provider ownership is recorded as auuid.UUIDOwnerLegalEntityIDfield, which keys both the per-tenant KV blob storage and the validated user-context tenant binding at token validation time.cyoda help errors OIDC_PROVIDER_DUPLICATE— Each tenant may register a givenwellKnownConfigUrionly once. SubmittingPOST /oauth/oidc/providerswith a URI that is already registered for the caller’s tenant returns this error.cyoda help errors OIDC_PROVIDER_NOT_FOUND— The provider UUID supplied in the path parameter does not correspond to a registered OIDC provider for the caller’s tenant. Either the provider was never registered, was deleted, or the UUID belongs to a different tenant (cross-tenant existence is not disclosed).cyoda help errors OIDC_PROVIDER_INACTIVE—PATCH /oauth/oidc/providers/{id}requires the provider to be in the active state. If the provider has been invalidated viaPOST /oauth/oidc/providers/{id}/invalidate, this error is returned.cyoda help errors OIDC_SSRF_BLOCKED— When registering an OIDC provider, cyoda validates thewellKnownConfigUriagainst a blocklist of private address ranges (loopback, RFC1918, link-local, IPv6 ULA). If the URI’s hostname resolves to any blocked range, the registration is rejected to prevent Server-Side Request Forgery attacks.cyoda help errors UNAUTHORIZED— Returned when theAuthorizationheader is missing, the bearer token is expired, the token signature is invalid, or the token was issued by an untrusted issuer. Also returned when a request reaches a protected route with no identity context established by the auth middleware.
Raw formats
Section titled “Raw formats”/help/auth/oidc.json— full descriptor (matchesGET /help/{topic}envelope)/help/auth/oidc.md— body only