OIDC clients

An OIDC client is an app (web, mobile, CLI, or service) registered to authenticate users via Huudis. Each client gets a client_id (and, for confidential clients, a client_secret), a set of allowed redirect_uris, and an allowed scopes list. Registering one is the prerequisite to using any of the /oidc/* endpoints.

This page is about managing clients. For the wire-level OIDC protocol (authorize, token, refresh, userinfo) see OIDC overview.

All requests on this page require a bearer admin JWT — see Authentication.

The client secret is returned exactly once, at create. Huudis stores only a bcrypt hash. If you lose the secret, the only recovery is to rotate it — there is no fetch-secret endpoint.

Endpoints

Method Path Purpose
GET /v1/oidc/clients List clients visible to the active workspace
POST /v1/oidc/clients Register a new client
PATCH /v1/oidc/clients/:id Edit name, URIs, scopes, logo
POST /v1/oidc/clients/:id/rotate-secret Mint a new secret, hash-revoke the old
DELETE /v1/oidc/clients/:id Delete a client

First-party clients (Huudis's own huudis-dashboard, the Plugipay portal, etc.) appear in GET results but are read-onlyPATCH, rotate, and delete return 403 FORBIDDEN. You only manage your own (isFirstParty: false).

List clients

GET /v1/oidc/clients

Returns clients visible in the active workspace:

  • Every client owned by the active workspace.
  • First-party clients whose service is in the workspace's enabledServices list (so the Plugipay portal client appears once you've enabled Plugipay).

First-party clients are returned with isFirstParty: true so the UI can show them as a different tier.

Response200 OK

{
  "data": [
    {
      "id": "oc_01KPG40HMM…",
      "accountId": "acc_01KPG30…",
      "clientId": "oc_a4c2b1f8d6e9",
      "name": "MejaStudio",
      "redirectUris": ["https://mejastudio.com/callback"],
      "scopes": ["openid", "profile", "email"],
      "isFirstParty": false,
      "logoUrl": null,
      "hasSecret": true,
      "createdAt": "2026-04-15T09:30:00.000Z",
      "updatedAt": "2026-05-01T14:22:00.000Z"
    }
  ]
}

hasSecret: false means the client was registered as public and uses PKCE without a confidential secret — the typical setup for SPAs and CLIs.

Register a client

POST /v1/oidc/clients

Request body

Field Type Required Description
name string (1–120) yes Display name (shown on the consent screen for third-party clients).
redirectUris string[] no HTTPS URLs Huudis will allow as redirect_uri on /oidc/authorize. Default []. Up to 20.
scopes string[] no Scopes the client may request. Default ["openid", "profile", "email"]. Extra scopes must be ones Huudis advertises in /.well-known/openid-configuration.
public boolean no When true, the client doesn't hold a secret — PKCE is required. Default false. Use for SPAs, mobile apps, and CLIs.
logoUrl string (URL, ≤500) no Square logo shown on the consent screen. Optional.

Response201 Created

{
  "data": {
    "id": "oc_01KPG50…",
    "accountId": "acc_01KPG30…",
    "clientId": "oc_4f2a1b9c8d7e",
    "name": "MejaStudio",
    "redirectUris": ["https://mejastudio.com/callback"],
    "scopes": ["openid", "profile", "email"],
    "isFirstParty": false,
    "logoUrl": null,
    "hasSecret": true,
    "clientSecret": "cs_lW8M…redacted…3kQpY",
    "createdAt": "2026-05-12T23:10:00.000Z",
    "updatedAt": "2026-05-12T23:10:00.000Z"
  }
}

clientSecret is shown exactly once on create. Capture it and store in your secret manager. Subsequent GET calls will return the same shape but without clientSecret.

Errors specific to this endpoint

Status error.code When
400 VALIDATION_ERROR Field shape wrong, URI not HTTPS (except http://localhost* for dev), unknown scope.
400 NO_ACTIVE_WORKSPACE The bearer token has no activeAccountId set.

Update a client

PATCH /v1/oidc/clients/:id

Partial update. Send only the fields you want to change. clientId, hasSecret, and accountId are immutable.

Request body

Field Type Description
name string New display name.
redirectUris string[] Replace the redirect-URI allowlist. To add a URI, send the full new list.
scopes string[] Replace the scope list.
logoUrl string | null New logo URL, or null to clear.

Errors

Status error.code When
403 FORBIDDEN The client is first-party, or owned by a different workspace.
404 NOT_FOUND No such client.

Rotate the client secret

POST /v1/oidc/clients/:id/rotate-secret

Mints a new secret and hashes-revokes the old. The new plaintext is returned exactly once on this response.

Response200 OK

{ "data": { "clientSecret": "cs_n8jM…redacted…W2vR" } }

Errors

Status error.code When
400 PUBLIC_CLIENT The client was registered as public — there is no secret to rotate.
403 FORBIDDEN First-party or cross-workspace.

After rotation, any existing access tokens issued by this client remain valid until they expire (typically ≤1 hour) — rotation does not retroactively invalidate sessions. Refresh attempts with the old secret fail immediately.

Delete a client

DELETE /v1/oidc/clients/:id

Removes the client. All existing access tokens and refresh tokens issued by it stop working immediately; any active sessions get invalid_client on next refresh.

204 No Content on success.

The OIDC client object

Field Type Description
id string (oc_…) Internal row ID. Used for PATCH, rotate, and delete.
accountId string (acc_…) Workspace that owns the client. null for global first-party clients.
clientId string (oc_…) Public OAuth client_id. Sent on every /oidc/authorize request.
name string Display name. Shown on the consent screen.
redirectUris string[] Allowed redirect_uri values. Exact-match.
scopes string[] Scopes this client may request.
isFirstParty boolean A built-in Forjio client (e.g., plugipay-portal). Read-only.
logoUrl string | null Consent-screen logo.
hasSecret boolean false for public clients (PKCE-only).
createdAt, updatedAt ISO 8601

The plaintext clientSecret appears only on create and rotate responses.

Public vs. confidential clients

Client kind public: true at create Authentication on /oidc/token
Server-side web app no client_id + client_secret (basic auth or post)
Browser SPA yes client_id + PKCE verifier (no secret)
Native / mobile app yes client_id + PKCE verifier
CLI yes client_id + PKCE verifier (device flow)
Confidential service-to-service no client_id + client_secret

Huudis enforces PKCE for every client, public or confidential. Confidential clients additionally send their secret.

First-party vs. third-party

A first-party client is one Forjio operates — the Plugipay portal, the Storlaunch dashboard, etc. Their consent screen is skipped (users already trust the brand). They're visible in your workspace's client list only when you opt into the corresponding service via Services.

A third-party client is one you (or a customer of yours) registered. Users get a one-time consent screen the first time they sign in — "MejaStudio wants to access your email and profile; allow?" — with their decision recorded in Consents.

You cannot create first-party clients via the API. Huudis seeds them.

Events

Event type Fires on
huudis.oidc.consent_granted.v1 A user consents to a client for the first time.

OIDC client CRUD does not emit events — it's considered admin noise. The audit log carries oidc_client_registered, oidc_client_updated, oidc_client_secret_rotated, and oidc_client_deleted entries.

Next