Skip to content
rw3iss Auth

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

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

FieldRequiredDefaultNotes
client_idyes—Unique. You choose it. Lands in aud/azp.
nameyes—Shown on the consent screen.
redirect_urisyes, ≥1—Exact match. Validated at write time.
description, logo_urlno""
post_logout_urisno[]
allowed_scopesno["openid","profile","email"]openid is force-prepended if missing.
grant_typesno["authorization_code","refresh_token"]New: settable. Validated against the three supported grants.
app_codenoNULLSee Users, apps and tenancy.
trustednofalseSkips consent. First-party only.
require_pkcenotrueNote: *bool, so omitting it means true, not false.
publicnofalsetrue → no secret is issued.
statusnoactiveSettable 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:

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

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

MethodPathEffect
GET/api/v1/admin/oauth/clientsEvery 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-secretNew 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.
Terminal window
# 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

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

FieldBehaviour
client_idIgnored. The server mints it from your name + 6 random bytes.
grant_typesIgnored. Fixed to authorization_code + refresh_token.
allowed_scopesOnly openid, profile, email. Anything else is a 400 with an explanation, not a silent drop.
trustedNot readable from the body. Always false — you can never skip the consent screen.
require_pkceNot readable from the body. Always true.
app_codeNot 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

LimitValueEnforced by
Clients per account10 (oidc.MaxClientsPerOwner)The INSERT … WHERE (count) < cap statement itself
Creates per hour per account10In-process limiter, applied after validation so fixing a typo does not burn your budget
Redirect URIs per client10oidc.MaxRedirectURIs
Name / description / logo URL100 / 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

MethodPathEffect
GET/api/v1/oidc/clientsYour 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-secretNew 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.

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

MethodPathEffect
POST/api/v1/admin/m2m-clientsCreate. Secret once.
GET/api/v1/admin/m2m-clientsEvery 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