Skip to content
Settings

auth.clients — M2M client lifecycle

cyoda-go version 0.8.2

auth.clients — provision and manage machine-to-machine (M2M) clients that authenticate against cyoda via the client_credentials grant.

You want a backend service or CI job to call cyoda APIs. Register an M2M client to obtain a client_id + client_secret. Your service then mints JWTs via POST /api/oauth/token (documented in auth.tokens) and presents them as Authorization: Bearer … on every request.

Use this path when you control both the service and its cyoda registration. For user-facing flows, federate via auth.oidc instead.

Admin (cyoda operator) sets up:

  • CYODA_IAM_MODE=jwt (mock mode bypasses auth entirely — fine for dev, never for prod)
  • CYODA_JWT_SIGNING_KEY (PEM RSA key; _FILE suffix supported)
  • Optionally: CYODA_BOOTSTRAP_CLIENT_ID + CYODA_BOOTSTRAP_CLIENT_SECRET provisions a single admin M2M at startup, useful for CI. See config.auth.
  • For admin-scoped M2M creation (withAdminRole=true): CYODA_IAM_M2M_ADMIN_ROLE_ENABLED=true. Off by default; when off, the withAdminRole=true request shape returns 404 FEATURE_DISABLED.

Client (you) needs:

  • An Authorization: Bearer … token with ROLE_ADMIN. Every /clients endpoint (list, create, delete, reset-secret) requires admin role today.
  • The created client is scoped to the caller’s tenant — there is no per-request tenant parameter on these endpoints.

The 4 /clients operations: provision, list, delete, reset-secret. Field names follow RFC 7591 (snake_case) for the credentials DTOs; list-item DTOs use cyoda’s customary camelCase.

The request takes no body. withAdminRole is a query parameter; the only role the new client receives unconditionally is ROLE_M2M, and ROLE_ADMIN is added when withAdminRole=true AND the IAM feature is enabled.

Terminal window
curl -X POST "https://cyoda.example.com/api/clients?withAdminRole=false" \
-H "Authorization: Bearer ${ADMIN_TOKEN}"

Response (200 OK) — schema TechnicalUserCredentialsDto:

{
"client_id": "abc523BCD",
"client_secret": "mySecretKey123",
"grant_type": "client_credentials",
"client_secret_expires_at": 0,
"roles": ["ROLE_M2M"]
}

client_secret is shown only at creation time. Capture it now; the server cannot return it again. client_secret_expires_at = 0 means the secret does not expire (per RFC 7591 §3.2.1).

Terminal window
curl -X GET https://cyoda.example.com/api/clients \
-H "Authorization: Bearer ${ADMIN_TOKEN}"

Response (200 OK) — array of TechnicalUserDto (no secrets):

[
{
"clientId": "abc523BCD",
"creationDate": "2026-06-17T10:02:27.88Z",
"lastUpdateDate": "2026-06-17T10:02:27.88Z",
"roles": ["ROLE_M2M"]
}
]
Terminal window
curl -X DELETE https://cyoda.example.com/api/clients/${CLIENT_ID} \
-H "Authorization: Bearer ${ADMIN_TOKEN}"

Response (200 OK):

{
"message": "M2M client deleted successfully",
"clientId": "abc523BCD"
}

The deleted client’s tokens remain valid until their natural exp; deletion stops new token issuance.

Rotates client_secret for an existing client. The verb is PUT, not POST.

Terminal window
curl -X PUT https://cyoda.example.com/api/clients/${CLIENT_ID}/secret \
-H "Authorization: Bearer ${ADMIN_TOKEN}"

Response (200 OK) is TechnicalUserCredentialsDto — same shape as creation, carrying the new client_secret. Capture it before the connection closes. Existing JWTs minted with the previous secret remain valid until their natural exp; only new /oauth/token requests need the new secret.

Clients are not tokens. After provisioning, the client uses auth.tokens (the /oauth/token endpoint) to mint JWTs from the client_id + client_secret. The JWT carries the client’s tenant in caas_org_id and its roles in user_roles. Full claim shape is in auth.tokens.

  • errors.UNAUTHORIZED (401) — bearer token missing, expired, signature invalid, or issuer untrusted.
  • errors.FORBIDDEN (403) — caller lacks ROLE_ADMIN (required for every /clients endpoint today).
  • errors.M2M_CLIENT_NOT_FOUND (404) — referenced clientId does not exist or belongs to a different tenant.
  • errors.FEATURE_DISABLED (404) — withAdminRole=true requested with CYODA_IAM_M2M_ADMIN_ROLE_ENABLED=false.
  • errors.BAD_REQUEST (400) — query-string parameter invalid (e.g. malformed withAdminRole value).
  • auth.tokens — the /oauth/token endpoint and JWT claim contract
  • config.authCYODA_BOOTSTRAP_*, CYODA_IAM_M2M_ADMIN_ROLE_ENABLED
  • openapicyoda help openapi tags and look for the User, Machine tag
  • 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 M2M_CLIENT_NOT_FOUND — Returned by /clients admin endpoints when the supplied clientId does not match any registered M2M client in the caller’s tenant:
  • cyoda help errors FEATURE_DISABLED — Returned by trusted-key endpoints when CYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED=false (the default):
  • cyoda help errors UNAUTHORIZED — Returned when the Authorization header 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.
  • cyoda help errors FORBIDDEN — The request was authenticated successfully but the caller’s JWT claims do not include the role required by the endpoint (for example, admin is required for administrative operations). Tenant mismatch — where the caller’s tenant does not match the resource — also produces this error.