Sign in with Cluster

Register OIDC clients so end users can sign in to your apps with their Cluster account.

Sign in with Cluster

Add "Sign in with Cluster" to your app via standard OpenID Connect — end users authenticate with their Cluster account and your app receives an id_token containing their identifier and profile claims. The CLI registers and manages the OAuth 2.0 clients; the identity provider itself runs at https://accounts.cluster.app.

OIDC clients are top-level objects scoped to an organization: a registered client belongs to the org and is visible to every member. Each client carries its own redirect URIs, scopes, grant types, and a client_secret (issued once at create time — never recoverable later).

The Flow

# 1. Register the client (returns the one-time client_secret)
ccp oidc create --name myapp --redirect-uri https://myapp.com/auth/callback

# 2. Use the issuer + client_id + client_secret in your app's OIDC library
#    (issuer = https://accounts.cluster.app; client_id = the hydra_client_id from step 1)

# 3. Users hit /auth/login in your app → get redirected to accounts.cluster.app
#    → sign in → redirected back with an authorization code → exchange for tokens

The client_secret prints once at create time. If you lose it, rotate it with ccp oidc rotate-secret.

Register a Client

ccp oidc create --name myapp --redirect-uri https://myapp.com/auth/callback
# ✓ Created OIDC client myapp
#    id: oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
#
#    Issuer:         https://accounts.cluster.app
#    Client ID:      b6e3f2a1-90b5-4532-9139-13baa05ad0b3
#    Client Secret:  zKv3rT9pQ2mN8wL4xY7bF1jA5sH6gE0d
#
#    ⚠ Save the secret now — it cannot be recovered later.
#      Rotate it any time with `ccp oidc rotate-secret <id>`.

--name is unique per organization (1–128 chars). Two clients in the same org cannot share a name; two different orgs can each have a client named myapp.

--redirect-uri is repeatable — pass once per allowed callback URL:

ccp oidc create --name myapp \
  --redirect-uri https://myapp.com/auth/callback \
  --redirect-uri http://localhost:3000/auth/callback

Redirect URIs must be https://* or http://localhost(:port) / http://127.0.0.1(:port). Fragments, wildcards, and userinfo are rejected. Matching is exact: https://myapp.com/cb does not match https://myapp.com/cb/ or https://myapp.com/cb?foo=bar.

The org is resolved from cluster.toml, the --org-id flag, or — when ambiguous — an interactive picker:

ccp oidc create --name myapp --redirect-uri https://myapp.com/cb --org-id org_abc123

Two IDs to Track

create prints two UUIDs, and they're for different audiences. Don't paste the wrong one — the labels in the output are the disambiguator:

Output labelShapeGoes where
id:oc-<uuid>The CLI id. Use with ccp oidc info, rotate-secret, destroy. Your app never sees this.
Client ID:bare <uuid> (no prefix)The OAuth client_id your OIDC library expects. This is what goes in CLUSTER_CLIENT_ID in your app's env.

Both are real UUIDs. The oc- prefix on the CLI id keeps the two distinguishable when you're copy-pasting — paste whichever label matched what you're filling in.

Optional Flags

FlagDefaultWhen to set it
--subject-type {public,pairwise}publicPass pairwise only if you need per-RP pseudonymous sub claims. public is correct for most apps.
--first-partyoffMarks the client as first-party; the consent screen auto-accepts. Third-party (default) shows users a "myapp wants permission to..." prompt.
--scope <scope> (repeatable)openid email profile offline_accessOverride the default scope list.
--grant-type <type> (repeatable)authorization_code refresh_tokenOverride the default grant list.

The defaults are right for a standard authorization-code app with refresh tokens. Don't change them unless you have a specific reason — empty --scope / --grant-type lists let the API fill in the defaults; passing an empty list explicitly is not the same as omitting the flag.

When you run ccp oidc create from inside a project directory (one with .cluster/config.json), the new client also auto-links to that project — see Link the Client to a Project for what that unlocks. Pass --no-link to opt out (the client is created the same way, just without the project association).

OIDC clients enforce strict redirect-URI matching (no wildcards, no patterns). That means every deploy URL your users sign in from has to be pre-registered — manually tracking that list breaks the moment your project gets a new prod URL, a custom domain, or a preview environment.

Linking solves it. When a project is linked to an OIDC client, ccp deploy, ccp promote, and ccp domain link automatically sync the client's redirect URIs with whatever hosts the project actually serves. No env vars; no manual list management.

# From inside a project directory:
ccp oidc link oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
# ✓ Linked project to OIDC client myapp
#    id: oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
#   ✓ Registered https://myapp.clusterbase.dev/auth/callback with OIDC client

link validates that the client belongs to the same organization as the project (cross-org linking is rejected before any state change is written). It then writes oidc_client_id to .cluster/config.json and backfills by default: queries the project's production deployment URL plus every custom-linked domain and registers https://<host>/auth/callback for each in one atomic PATCH. Pass --no-backfill to defer registration to the next ccp deploy.

ccp oidc create does the same backfill-less link automatically when run inside a project dir (so the typical "create + use here" flow is one command). The --redirect-uri you pass to create is whatever you already needed for local dev — the prod URL gets registered on the first ccp deploy --prod.

Auto-register on Deploy

Once the project is linked, ccp deploy --prod and ccp promote register the production deploy URL, and ccp domain link registers any newly-attached custom domain:

ccp deploy --prod
# ◼ Function deployed!
# › https://myapp.clusterbase.dev
#  ✓ Registered https://myapp.clusterbase.dev/auth/callback with OIDC client

ccp domain link app.example.com
#   ✓ Domain app.example.com linked
#   ✓ TLS certificate will be issued automatically
#   ✓ Registered https://app.example.com/auth/callback with OIDC client

Behavior at a glance:

  • Idempotent. If the URI is already in redirect_uris, nothing happens — no print, no PATCH.
  • Production only. Preview deploys (without --prod) are NOT auto-registered. Each preview produces a unique <deployment-id>.clusterbase.dev host that would accumulate forever in the OIDC client; the practical client-config cap would be hit within weeks of active CI use. Use ccp oidc update --add-redirect-uri if you specifically need OIDC sign-in on a preview URL.
  • Non-fatal. If the sync fails (network blip, lost session, client deleted), the deploy itself still succeeds. You get a ⚠ Could not auto-register OIDC redirect URI line with a copy-pasteable retry invocation. No redeploy needed to recover.
  • Project-scoped on domain link. ccp domain link foo.com --function fn-OTHER only auto-registers if fn-OTHER matches the cwd project's function. Registering on the wrong client across projects is silently skipped.
  • Manual unlink. ccp domain unlink does NOT auto-deregister — the CLI can't tell which project the domain was previously attached to without an extra round-trip. Remove the URI manually via ccp oidc update --remove-redirect-uri if needed.

Custom Callback Path

The default callback path is /auth/callback. If your app uses a different path (e.g. /api/auth/callback, /oauth/callback), edit oidc_callback_path in .cluster/config.json:

{
  "function_id": "fn-...",
  "organization_id": "org_...",
  "oidc_client_id": "oc-...",
  "oidc_callback_path": "/api/auth/callback"
}

The setting applies to every subsequent auto-register (deploy, promote, domain link). Existing URIs on the client are not migrated — use ccp oidc update to fix them up.

Unlinking

Edit .cluster/config.json and set oidc_client_id to an empty string (or delete the field). Subsequent deploys won't auto-register. Already-registered URIs stay on the client; remove them with ccp oidc update --remove-redirect-uri if you're decommissioning the project.

Use the Client in Your App

The create output gives you everything your OIDC library needs:

// Example with `openid-client` (Node):
import { Issuer } from "openid-client";

const issuer = await Issuer.discover("https://accounts.cluster.app");
const client = new issuer.Client({
  client_id:     process.env.CLUSTER_CLIENT_ID,      // the "cluster-..." value
  client_secret: process.env.CLUSTER_CLIENT_SECRET,  // the one-time secret
  redirect_uris: ["https://myapp.com/auth/callback"],
  response_types: ["code"],
});

Same shape for any OIDC-compliant library — golang.org/x/oauth2, Python authlib, Rust openidconnect, browser-side oidc-client-ts. Point them all at https://accounts.cluster.app as the issuer and use the client_id / client_secret you got from ccp oidc create.

id_token claims that ship out of the box:

ClaimSource
isshttps://accounts.cluster.app
subStable user identifier (Kratos identity UUID)
audYour client_id
emailUser's email
nameUser's display name
exp, iat, nbfStandard JWT timestamps

Verify the signature against https://accounts.cluster.app/.well-known/jwks.json — every modern OIDC library does this for you when given the issuer URL.

List Clients

ccp oidc ls
#  • myapp  third-party
#    id:         oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
#    client_id:  b6e3f2a1-90b5-4532-9139-13baa05ad0b3
#    2 redirect URIs:  https://myapp.com/auth/callback, http://localhost:3000/auth/callback
#
#  • internal-tool  first-party
#    id:         oc-7c89df42-8a31-4d67-b023-1c4e5f6a78b9
#    client_id:  f48c1023-5d72-4a98-b6e1-9028fa37c145
#    1 redirect URI:  https://tool.internal.cluster.app/cb

list is an alias for ls. The org is resolved the same way as create.

Show Client Details

ccp oidc info oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
#  • myapp
#    id:                          oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
#    client_id:                   b6e3f2a1-90b5-4532-9139-13baa05ad0b3
#    organization_id:             org_abc123
#    first_party:                 false
#    token_endpoint_auth_method:  client_secret_basic
#    redirect_uris:
#      - https://myapp.com/auth/callback
#      - http://localhost:3000/auth/callback
#    scopes:
#      - openid
#      - email
#      - profile
#      - offline_access
#    grant_types:
#      - authorization_code
#      - refresh_token
#    created_at:  2026-05-24T17:08:21Z
#    updated_at:  2026-05-24T17:08:21Z

info never includes the client_secret. If you've lost it, see Rotate the Secret below.

Update a Client

Adjust the client's redirect URIs, name, or first-party flag without rotating the secret. Every invocation is one atomic PATCH; the client_secret is never touched.

# Add or remove redirect URIs (both flags repeatable, applied in one PATCH)
ccp oidc update oc-5d9f2c3b-... --add-redirect-uri https://staging.myapp.com/auth/callback
ccp oidc update oc-5d9f2c3b-... --remove-redirect-uri http://localhost:9999/auth/callback

# Combined add + remove for a URL rotation
ccp oidc update oc-5d9f2c3b-... \
  --add-redirect-uri https://new.myapp.com/auth/callback \
  --remove-redirect-uri https://old.myapp.com/auth/callback

# Rename the client (still unique per org)
ccp oidc update oc-5d9f2c3b-... --name myapp-v2

# Toggle the first-party flag
ccp oidc update oc-5d9f2c3b-... --first-party
ccp oidc update oc-5d9f2c3b-... --no-first-party

Most users won't run update directly — linked projects keep redirect URIs in sync automatically (see Auto-register on Deploy). Reach for update when:

  • You need to register a preview URL (which auto-register intentionally skips).
  • You're cleaning up stale URIs from old deploys or custom domains.
  • The project isn't linked and you're managing URIs by hand.
  • You're scripting a redirect-URI rotation across deploys.

Guardrails:

  • Self-canceling input is rejected. --add-redirect-uri X --remove-redirect-uri X errors before any network call, because the local merge would produce a zero-diff PATCH that prints success without changing anything.
  • Empty result is rejected. A removal that would leave the client with zero redirect URIs is rejected — the server enforces cardinality >= 1. Add a replacement first, then remove.
  • At least one flag required. Calling update without any of --add-redirect-uri, --remove-redirect-uri, --name, --first-party, or --no-first-party errors before any network call.

Rotate the Secret

ccp oidc rotate-secret oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
# Rotate secret for oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b? The current secret is invalidated immediately. (y/N): y
# ✓ Rotated secret for myapp
#
#    Issuer:         https://accounts.cluster.app
#    Client ID:      b6e3f2a1-90b5-4532-9139-13baa05ad0b3
#    Client Secret:  newSecretHere...
#
#    ⚠ Save the secret now — it cannot be recovered later.

Rotation invalidates the old secret immediately — there is no grace window. Any deployed instance of your app still using the old secret starts getting 401 invalid_client on its next /oauth2/token call. Plan a coordinated deploy: rotate, update the env var in your hosting platform, re-deploy.

-y skips the confirmation prompt.

Destroy a Client

ccp oidc destroy oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b
# Destroy OIDC client oc-5d9f2c3b-1e8a-4f7c-9b1a-2d6e3a8f0c4b? Every relying-party request using its credentials will fail. (y/N): y
# ✓ OIDC client destroyed

rm is an alias for destroy. Every active relying-party session that still has an unexpired id_token keeps working until the token expires, but no new tokens can be minted: /oauth2/auth returns 400 invalid_client immediately and /oauth2/token rejects authorization codes.

-y skips the confirmation prompt.

Capturing the Secret

create and rotate-secret print the secret to stdout; the "save it now" warning prints to stderr. So redirecting stdout captures the secret cleanly:

ccp oidc create --name myapp --redirect-uri https://myapp.com/cb > secret.txt
# warning lands on terminal (stderr)
# secret + Client ID + Issuer in secret.txt

In headless mode (CCP_HEADLESS=1) the same redirect works, and the rotate-secret / destroy confirmation prompts auto-accept — see the Headless mode reference for the broader contract.

Redirect URI Requirements

  • Must be https://* or http://localhost(:port) / http://127.0.0.1(:port).
  • Must not include a URL fragment (#anything).
  • Must not include a * substring (no wildcards).
  • Must not include userinfo (https://user:pass@host/cb).
  • Matching is exact. Re-register if you change the host, path, port, or query string.

For local development, use http://localhost:<port>/auth/callback — the validator accepts loopback over plain HTTP. For everything else, HTTPS is required.

Subcommand Reference

SubcommandAliasesDescription
createRegister a new client (prints one-time client_secret)
lslistList clients in the resolved org
info <client_id>Show details for one client (no secret)
link <client_id>Link the current project to an OIDC client; backfills redirect URIs by default
update <client_id>Change name, first-party flag, or add/remove redirect URIs (one atomic PATCH)
rotate-secret <client_id>Generate a new secret; old secret invalidated immediately
destroy <client_id>rmTear down the client

How It Works

  • create POSTs to /api/v1/oidc/clients against the org-scoped infra-api, which then registers the client with the identity provider (Ory Hydra) on the cluster's admin port. The plaintext client_secret is returned in the response — Hydra stores only a bcrypt hash, so subsequent reads will not include the secret.
  • update PATCHes the same endpoint with pointer-typed fields: omitted = preserve, present = replace. --add-redirect-uri / --remove-redirect-uri are merged locally against the current list and sent as one full-replace redirect_uris array; the rest of the update (name, first_party) ships in the same body.
  • rotate-secret is a composition: the API fetches the current client from Hydra, generates a new 32-byte random secret, and PUT-replaces the client with the new secret. There is no atomic rotate endpoint and no grace window.
  • link writes oidc_client_id to .cluster/config.json, then (by default) GETs /api/v1/serverless/functions/{id} to enumerate the project's production deploy URL + custom-linked domains, and registers them with the OIDC client in one batched PATCH. The deploy/promote/domain-link auto-register hooks read the same oidc_client_id field to know which client to update.
  • destroy deletes from both Hydra and the infra-api mirror table. The relying-party app starts getting 400 invalid_client from /oauth2/auth on the next request.
  • Org membership is enforced server-side on every write — a leaked oc-… id from another org cannot be probed.

Limits

LimitValue
OIDC clients per organization25
Redirect URIs per clientCapped by request body size (1MB); practical limit ~thousands
Client name length128 characters

The per-org cap is configurable on the server side; reach out if you have a legitimate need to register more than 25 clients in a single org.

On this page