{
  "topic": "auth.clients",
  "path": [
    "auth",
    "clients"
  ],
  "title": "auth.clients — M2M client lifecycle",
  "synopsis": "auth.clients — provision and manage machine-to-machine (M2M) clients that authenticate against cyoda via the `client_credentials` grant.",
  "body": "# auth.clients\n\n## NAME\n\nauth.clients — provision and manage machine-to-machine (M2M) clients that authenticate against cyoda via the `client_credentials` grant.\n\n## GOAL\n\nYou 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.\n\nUse this path when you control both the service and its cyoda registration. For user-facing flows, federate via `auth.oidc` instead.\n\n## PREREQUISITES\n\n**Admin (cyoda operator) sets up:**\n\n- `CYODA_IAM_MODE=jwt` (mock mode bypasses auth entirely — fine for dev, never for prod)\n- `CYODA_JWT_SIGNING_KEY` (PEM RSA key; `_FILE` suffix supported)\n- Optionally: `CYODA_BOOTSTRAP_CLIENT_ID` + `CYODA_BOOTSTRAP_CLIENT_SECRET` provisions a single admin M2M at startup, useful for CI. See `config.auth`.\n- 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`.\n\n**Client (you) needs:**\n\n- An `Authorization: Bearer …` token with `ROLE_ADMIN`. Every `/clients` endpoint (list, create, delete, reset-secret) requires admin role today.\n- The created client is scoped to the caller's tenant — there is no per-request tenant parameter on these endpoints.\n\n## REQUEST FLOW\n\nThe 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.\n\n### Provision a client\n\nThe 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.\n\n```bash\ncurl -X POST \"https://cyoda.example.com/api/clients?withAdminRole=false\" \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`200 OK`) — schema `TechnicalUserCredentialsDto`:\n\n```json\n{\n  \"client_id\":                \"abc523BCD\",\n  \"client_secret\":            \"mySecretKey123\",\n  \"grant_type\":               \"client_credentials\",\n  \"client_secret_expires_at\": 0,\n  \"roles\":                    [\"ROLE_M2M\"]\n}\n```\n\n**`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).\n\n### List clients in the caller's tenant\n\n```bash\ncurl -X GET https://cyoda.example.com/api/clients \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`200 OK`) — array of `TechnicalUserDto` (no secrets):\n\n```json\n[\n  {\n    \"clientId\":       \"abc523BCD\",\n    \"creationDate\":   \"2026-06-17T10:02:27.88Z\",\n    \"lastUpdateDate\": \"2026-06-17T10:02:27.88Z\",\n    \"roles\":          [\"ROLE_M2M\"]\n  }\n]\n```\n\n### Delete a client\n\n```bash\ncurl -X DELETE https://cyoda.example.com/api/clients/${CLIENT_ID} \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`200 OK`):\n\n```json\n{\n  \"message\":  \"M2M client deleted successfully\",\n  \"clientId\": \"abc523BCD\"\n}\n```\n\nThe deleted client's tokens remain valid until their natural `exp`; deletion stops new token issuance.\n\n### Reset a client secret\n\nRotates `client_secret` for an existing client. The verb is `PUT`, not `POST`.\n\n```bash\ncurl -X PUT https://cyoda.example.com/api/clients/${CLIENT_ID}/secret \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`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.\n\n## TOKEN\n\nClients 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`.\n\n## ERRORS\n\n- `errors.UNAUTHORIZED` (`401`) — bearer token missing, expired, signature invalid, or issuer untrusted.\n- `errors.FORBIDDEN` (`403`) — caller lacks `ROLE_ADMIN` (required for every `/clients` endpoint today).\n- `errors.M2M_CLIENT_NOT_FOUND` (`404`) — referenced `clientId` does not exist or belongs to a different tenant.\n- `errors.FEATURE_DISABLED` (`404`) — `withAdminRole=true` requested with `CYODA_IAM_M2M_ADMIN_ROLE_ENABLED=false`.\n- `errors.BAD_REQUEST` (`400`) — query-string parameter invalid (e.g. malformed `withAdminRole` value).\n\n## SEE ALSO\n\n- `auth.tokens` — the `/oauth/token` endpoint and JWT claim contract\n- `config.auth` — `CYODA_BOOTSTRAP_*`, `CYODA_IAM_M2M_ADMIN_ROLE_ENABLED`\n- `openapi` — `cyoda help openapi tags` and look for the `User, Machine` tag\n",
  "sections": [
    {
      "name": "NAME",
      "body": "auth.clients — provision and manage machine-to-machine (M2M) clients that authenticate against cyoda via the `client_credentials` grant."
    },
    {
      "name": "GOAL",
      "body": "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.\n\nUse this path when you control both the service and its cyoda registration. For user-facing flows, federate via `auth.oidc` instead."
    },
    {
      "name": "PREREQUISITES",
      "body": "**Admin (cyoda operator) sets up:**\n\n- `CYODA_IAM_MODE=jwt` (mock mode bypasses auth entirely — fine for dev, never for prod)\n- `CYODA_JWT_SIGNING_KEY` (PEM RSA key; `_FILE` suffix supported)\n- Optionally: `CYODA_BOOTSTRAP_CLIENT_ID` + `CYODA_BOOTSTRAP_CLIENT_SECRET` provisions a single admin M2M at startup, useful for CI. See `config.auth`.\n- 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`.\n\n**Client (you) needs:**\n\n- An `Authorization: Bearer …` token with `ROLE_ADMIN`. Every `/clients` endpoint (list, create, delete, reset-secret) requires admin role today.\n- The created client is scoped to the caller's tenant — there is no per-request tenant parameter on these endpoints."
    },
    {
      "name": "REQUEST FLOW",
      "body": "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.\n\n### Provision a client\n\nThe 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.\n\n```bash\ncurl -X POST \"https://cyoda.example.com/api/clients?withAdminRole=false\" \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`200 OK`) — schema `TechnicalUserCredentialsDto`:\n\n```json\n{\n  \"client_id\":                \"abc523BCD\",\n  \"client_secret\":            \"mySecretKey123\",\n  \"grant_type\":               \"client_credentials\",\n  \"client_secret_expires_at\": 0,\n  \"roles\":                    [\"ROLE_M2M\"]\n}\n```\n\n**`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).\n\n### List clients in the caller's tenant\n\n```bash\ncurl -X GET https://cyoda.example.com/api/clients \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`200 OK`) — array of `TechnicalUserDto` (no secrets):\n\n```json\n[\n  {\n    \"clientId\":       \"abc523BCD\",\n    \"creationDate\":   \"2026-06-17T10:02:27.88Z\",\n    \"lastUpdateDate\": \"2026-06-17T10:02:27.88Z\",\n    \"roles\":          [\"ROLE_M2M\"]\n  }\n]\n```\n\n### Delete a client\n\n```bash\ncurl -X DELETE https://cyoda.example.com/api/clients/${CLIENT_ID} \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`200 OK`):\n\n```json\n{\n  \"message\":  \"M2M client deleted successfully\",\n  \"clientId\": \"abc523BCD\"\n}\n```\n\nThe deleted client's tokens remain valid until their natural `exp`; deletion stops new token issuance.\n\n### Reset a client secret\n\nRotates `client_secret` for an existing client. The verb is `PUT`, not `POST`.\n\n```bash\ncurl -X PUT https://cyoda.example.com/api/clients/${CLIENT_ID}/secret \\\n  -H \"Authorization: Bearer ${ADMIN_TOKEN}\"\n```\n\nResponse (`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."
    },
    {
      "name": "TOKEN",
      "body": "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`."
    },
    {
      "name": "ERRORS",
      "body": "- `errors.UNAUTHORIZED` (`401`) — bearer token missing, expired, signature invalid, or issuer untrusted.\n- `errors.FORBIDDEN` (`403`) — caller lacks `ROLE_ADMIN` (required for every `/clients` endpoint today).\n- `errors.M2M_CLIENT_NOT_FOUND` (`404`) — referenced `clientId` does not exist or belongs to a different tenant.\n- `errors.FEATURE_DISABLED` (`404`) — `withAdminRole=true` requested with `CYODA_IAM_M2M_ADMIN_ROLE_ENABLED=false`.\n- `errors.BAD_REQUEST` (`400`) — query-string parameter invalid (e.g. malformed `withAdminRole` value)."
    },
    {
      "name": "SEE ALSO",
      "body": "- `auth.tokens` — the `/oauth/token` endpoint and JWT claim contract\n- `config.auth` — `CYODA_BOOTSTRAP_*`, `CYODA_IAM_M2M_ADMIN_ROLE_ENABLED`\n- `openapi` — `cyoda help openapi tags` and look for the `User, Machine` tag"
    }
  ],
  "see_also": [
    "auth",
    "auth.tokens",
    "config.auth",
    "errors.M2M_CLIENT_NOT_FOUND",
    "errors.FEATURE_DISABLED",
    "errors.UNAUTHORIZED",
    "errors.FORBIDDEN"
  ],
  "stability": "evolving",
  "actions": []
}
