Registering an application
Three registration paths, one per application type. Pick the one that matches what you are building; the fields are documented in Client properties.
All paths share one rule:
Base path below is /api/v1, which is API_PREFIX and therefore a
default rather than a constant. Read
/.well-known/openid-configuration if you need the live paths.
Path A — admin registers an interactive application
For: anything the operator owns, anything that needs a civic:* scope,
trusted, an app_code, PKCE turned off, or the client_credentials
grant.
Who: system_admin or super_admin (the platform-admin chain).
1. Create
curl -X POST https://auth.civicgate.org/api/v1/admin/oauth/clients \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "marketplace-web", "name": "Marketplace", "description": "Public marketplace, server-rendered", "logo_url": "https://marketplace.example/logo.png", "redirect_uris": ["https://marketplace.example/auth/callback"], "post_logout_uris": ["https://marketplace.example/"], "allowed_scopes": ["openid", "profile", "email", "offline_access"], "grant_types": ["authorization_code", "refresh_token"], "app_code": "marketplace-v2", "trusted": false, "require_pkce": true, "public": false }'{ "client_id": "marketplace-web", "client_secret": "cgs_4bK8s2Qm…", "warning": "Copy the client secret now. It cannot be shown again."}201 Created. Copy the secret now.
The request fields
| Field | Required | Default | Notes |
|---|---|---|---|
client_id | yes | — | Unique. You choose it. Lands in aud/azp. |
name | yes | — | Shown on the consent screen. |
redirect_uris | yes, ≥1 | — | Exact match. Validated at write time. |
description, logo_url | no | "" | |
post_logout_uris | no | [] | |
allowed_scopes | no | ["openid","profile","email"] | openid is force-prepended if missing. |
grant_types | no | ["authorization_code","refresh_token"] | New: settable. Validated against the three supported grants. |
app_code | no | NULL | See Users, apps and tenancy. |
trusted | no | false | Skips consent. First-party only. |
require_pkce | no | true | Note: *bool, so omitting it means true, not false. |
public | no | false | true → no secret is issued. |
status | no | active | Settable on PATCH. |
2. A service identity on the same application (optional)
Add client_credentials and the client can also authenticate as itself,
with no user present:
curl -X POST https://auth.civicgate.org/api/v1/admin/oauth/clients \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "marketplace-backend", "name": "Marketplace backend", "redirect_uris": ["https://marketplace.example/auth/callback"], "grant_types": ["authorization_code", "refresh_token", "client_credentials"], "public": false }'public must be false. Create refuses the combination outright:
{"error":{"code":"INVALID_INPUT","details":{"field":"grant_types"}, "message":"client_credentials requires a confidential client; do not set public"}}3. A public client (SPA / mobile)
curl -X POST https://auth.civicgate.org/api/v1/admin/oauth/clients \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "marketplace-spa", "name": "Marketplace (web app)", "redirect_uris": [ "https://app.marketplace.example/auth/callback", "http://localhost:5173/auth/callback" ], "public": true }'{ "client_id": "marketplace-spa", "client_secret": "", "warning": "Copy the client secret now. It cannot be shown again."}The empty client_secret is correct — a public client has none, and PKCE
is what protects it. (The warning is emitted unconditionally; ignore it
here.)
4. The rest of the admin surface
| Method | Path | Effect |
|---|---|---|
GET | /api/v1/admin/oauth/clients | Every client + the server’s supported_scopes vocabulary. |
PATCH | /api/v1/admin/oauth/clients/{clientId} | Only the supplied fields. A field you omit is left untouched, not blanked. |
POST | /api/v1/admin/oauth/clients/{clientId}/rotate-secret | New secret. The old one dies at once. |
DELETE | /api/v1/admin/oauth/clients/{clientId} | Also drops the client’s authorization codes and every standing consent. |
# Give an existing client the client_credentials grant.curl -X PATCH https://auth.civicgate.org/api/v1/admin/oauth/clients/marketplace-backend \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"grant_types":["authorization_code","refresh_token","client_credentials"]}'{"status": "updated"}Consents are deleted along with the client deliberately: leaving them would silently re-grant a future client that reused the id.
There is a ready-made admin UI — OidcClientManager from
@rw3iss/auth-client
(@rw3iss/auth-client/preact/ui/admin), backed by the SDK’s
OidcClientsFlow.
Path B — a developer registers their own application
For: “add Login with {Provider} to my site”, by anyone with an account. The same table, deliberately fewer capabilities — see the comparison in Application types.
Who: any authenticated member. Not an admin.
1. Create
curl -X POST https://auth.civicgate.org/api/v1/oidc/clients \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "My side project", "description": "Weekend build", "logo_url": "https://side.example/logo.png", "redirect_uris": ["https://side.example/callback", "http://localhost:3000/callback"], "post_logout_uris": ["https://side.example/"], "allowed_scopes": ["openid", "profile", "email"], "public": false }'{ "client_id": "my-side-project-9f2c41ba7d0e", "name": "My side project", "client_secret": "cgs_7Hs1p…", "warning": "Copy the client secret now. It cannot be shown again."}For "public": true you get no secret and a note instead:
{ "client_id": "my-side-project-9f2c41ba7d0e", "name": "My side project", "is_public": true, "note": "Public clients get no secret. PKCE is required and is what protects the flow."}What you cannot set here
| Field | Behaviour |
|---|---|
client_id | Ignored. The server mints it from your name + 6 random bytes. |
grant_types | Ignored. Fixed to authorization_code + refresh_token. |
allowed_scopes | Only openid, profile, email. Anything else is a 400 with an explanation, not a silent drop. |
trusted | Not readable from the body. Always false — you can never skip the consent screen. |
require_pkce | Not readable from the body. Always true. |
app_code | Not readable from the body. Always NULL. |
Requesting a scope outside the self-service set returns the reason verbatim, because it is the single least self-evident refusal here:
{"error":{"code":"INVALID_INPUT","details":{"field":"allowed_scopes"}, "message":"A self-registered application may request only these scopes: openid, profile, email. \"civic:positions\" is granted by a CivicGate administrator — contact us if your application needs it."}}2. Limits
| Limit | Value | Enforced by |
|---|---|---|
| Clients per account | 10 (oidc.MaxClientsPerOwner) | The INSERT … WHERE (count) < cap statement itself |
| Creates per hour per account | 10 | In-process limiter, applied after validation so fixing a typo does not burn your budget |
| Redirect URIs per client | 10 | oidc.MaxRedirectURIs |
| Name / description / logo URL | 100 / 500 / 512 chars |
Hitting the client cap:
{"error":{"code":"INVALID_INPUT","details":{"field":"client"}, "message":"You already have the maximum number of applications. Delete one you no longer use to register another."}}3. Managing your own clients
| Method | Path | Effect |
|---|---|---|
GET | /api/v1/oidc/clients | Your clients + allowed_scopes, max_clients, max_redirect_uris |
GET | /api/v1/oidc/clients/{clientId} | One of yours |
PATCH | /api/v1/oidc/clients/{clientId} | name, description, logo_url, redirect_uris, post_logout_uris, allowed_scopes, status |
POST | /api/v1/oidc/clients/{clientId}/rotate-secret | New secret; old one dies |
DELETE | /api/v1/oidc/clients/{clientId} | Client + its codes + its consents |
status accepts only "active" or "disabled" here. Redirect URIs are
re-validated on update exactly as on create — a rule enforced only at
creation is not a rule, because the edit form would be the way around it.
Path C — admin registers a machine client
For: a cron job, a CI runner, a batch process, or a downstream service that calls the API with no user involved.
Who: system_admin only — a stricter gate than Path A.
curl -X POST https://auth.civicgate.org/api/v1/admin/m2m-clients \ -H "Authorization: Bearer $SYSTEM_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "nightly-reconcile", "name": "Nightly reconciliation job", "description": "Runs 02:00 UTC from the ops box", "scopes": ["users:read", "orgs:read"], "allowed_audiences": [] }'{ "client": { "id": "…uuid…", "client_id": "nightly-reconcile", "name": "Nightly reconciliation job", "description": "Runs 02:00 UTC from the ops box", "scopes": ["users:read", "orgs:read"], "allowed_audiences": [], "status": "active", "created_at": "…" }, "client_secret": "…"}201 Created. client_secret_hash is json:"-" on the model, so no read
ever returns the secret.
| Method | Path | Effect |
|---|---|---|
POST | /api/v1/admin/m2m-clients | Create. Secret once. |
GET | /api/v1/admin/m2m-clients | Every non-revoked client. No pagination — the inventory is meant to be small. |
GET | /api/v1/admin/m2m-clients/{clientId} | By UUID, not by the operator-chosen client_id string. |
DELETE | /api/v1/admin/m2m-clients/{clientId} | Soft revoke; idempotent. |
Revocation is not retroactive. Future grants fail immediately; outstanding tokens remain valid until natural expiry (~15 minutes), because service tokens are stateless and no row records them.
After registration
- Interactive clients: run the flow in The grants → authorization_code.
- Machine clients: The grants → client_credentials.
- Deciding which people may use the application: Users, apps and tenancy.