Skip to main content

Sign In Your Application

Before you start​

Your application signs users in with the public OAuth client Custd provides for its project environment and audience. Registration and the first administrator are currently established by HaakCo; from that point your own administrator owns this workflow.

First record the binding your application needs in Admin → Auth Setup (/admin?tab=auth-setup) for the project environment:

  • the audience you are giving this application,
  • the exact redirect URIs it will return sign-ins to, plus any post-logout redirect URI,
  • the allowed origins the browser will call from.

Custd reconciles that recorded binding to the provider, which creates a public client with the derived client ID custd-app-<environmentID>-<audienceSlug>. There is no client secret: only a public authorization-code client with PKCE is supported on this path, and PKCE (S256) is enforced for the whole deployment. A saved binding is not a working one until reconciliation has run; read the Auth Setup status.

Browser sign-in with authorization code + PKCE​

Read the issuer's discovery document, <issuer>/.well-known/openid-configuration, and use its authorization_endpoint, token_endpoint and jwks_uri. Replace https://auth.example.com/ below with the issuer Custd gives your environment.

Send the browser to the authorization endpoint with exactly these parameters:

response_type=code
client_id=custd-app-<environmentID>-<audienceSlug>
redirect_uri=<one of your registered redirect URIs>
scope=openid profile offline_access
audience=<your configured application audience>
state=<opaque anti-CSRF value you verify on return>
code_challenge=<S256 challenge for your verifier>
code_challenge_method=S256

Request exactly openid profile offline_access. The issuer refuses consent when audience is absent, so supply your application audience explicitly rather than relying on a gateway default. On return, verify state, then exchange the code:

POST <token_endpoint>
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
code=<returned code>
code_verifier=<the PKCE verifier for your challenge>
client_id=custd-app-<environmentID>-<audienceSlug>
redirect_uri=<the same registered redirect URI>

Validate the tokens you receive​

Validate the signature against the published JWKS, and validate iss, aud and exp yourself. The two carriers carry different audiences and are verified separately:

CarrierEnforce
Identity tokenaud is your client ID, custd-app-<environmentID>-<audienceSlug>
Access tokenaud is exactly your configured application audience
Bothgroups contains the exact derived group custd-group-<environmentID>-<audienceSlug>

Persist the verified (iss, sub) pair as your stable identity key; sub is Custd's application principal ID. Never substitute an email address or the directory mapping identifiers below for it. The tokens carry these extension claims on both carriers, in addition to the standard OIDC envelope:

  • project_id, environment_id, identity_pool_id, organisation_id
  • principal_kind (always application), application_principal_id, application_role
  • provider_issuer, provider_subject (the directory's mapping namespace and subject, not the token issuer or the sub)
  • groups, audience, scope, token_purpose

Custd adds no email, name, given_name, family_name, preferred_username or email_verified claim. Do not invent a display name or an email assertion from the token; your application owns how it names and displays people.

Headless sign-in with a device code​

On a server with no browser and no reachable inbound callback port, use custdctl device-code login. Configure a named profile first, then request the device method:

custdctl config init --profile production \
--api-url https://api.example.com \
--issuer-url https://auth.example.com/ \
--client-id <operator-client-id> \
--audience custd \
--scope 'openid profile offline_access admin'
custdctl login --profile production --method device

The device grant belongs to the operator client only; your application client keeps its authorization-code and refresh grants. The terminal prints a verification URL, a short user code and the expiry, then waits. Open the URL on another computer or phone, confirm the code matches the one on the terminal, and approve. Approval continues through the ordinary Custd sign-in and consent screens, where the resolved account and the requested access are shown before you agree. The terminal then completes its account proof and stores the token in that profile.

With a terminal attached and no --method, login offers a method menu and preselects browser login; a piped or scripted invocation never prompts and uses browser login unless --method device is explicit.

This is human-authorized sign-in, not unattended machine authentication: a person still approves in a browser. For unattended automation, use the machine credential path in Get an Access Token.

Refresh and sign out​

The CLI refreshes an expired access token automatically on the next authenticated command. auth status reads the stored profile locally, with no network request; account proves the profile against the API, so it does make a request and refreshes the access token when needed:

custdctl auth status --profile production
custdctl account --profile production --json

If the refresh token itself was expired or revoked, the CLI tells you to run custdctl login --profile production again. An application refreshes with the same public client:

POST <token_endpoint>
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
refresh_token=<stored refresh token>
client_id=custd-app-<environmentID>-<audienceSlug>

Refresh tokens rotate. Store the returned token, and tolerate a delayed or duplicate refresh within the provider's short rotation grace period instead of signing the person out.

To end the local CLI session, remove that profile's saved token:

custdctl logout --profile production

For a browser application, clearing your own session is your operation. To end the provider session too, send the browser to the end_session_endpoint from discovery; a registered post_logout_redirect_uri decides where the person lands. Keep four operations distinct — clearing your own session, ending the provider session, ending a directory session, and withdrawing the application's authority. Signing out does not withdraw authority; that is a suspend, a membership removal, or an environment offboard.

Expired, already-used or refused requests​

An expired, already-used or unknown browser sign-in request answers with a neutral restart page:

This sign-in link is no longer valid The sign-in request behind this page is missing, expired, or already used. Return to the app you were signing in from and start a new sign-in.

That page is deliberately neutral: an unusable request cannot name a trusted client or realm, so it never guesses branding or sends the person to an unrelated sign-in. The exact recovery is to start a new sign-in from the application. Do not reload or retry the old link, and do not treat it as a bad password.

If the person does not approve at consent, no authorization code is issued: the consent page offers only Continue, and closing it grants nothing. Treat that as "not signed in" and let the person start a new sign-in. When the provider itself refuses an authorization request — for example a client this deployment does not serve — it returns the standard access_denied error to your redirect URI, which is also a normal not-signed-in outcome rather than a fault. A genuine provider fault shows its own page — "Sign-in could not be completed" — and is not a user mistake.

For a device request you did not approve, choose Do not approve on the verification page. The provider records the rejection, so the terminal's next status poll is answered access_denied and the CLI exits promptly with "device authorization was denied; start login again with a new code" instead of waiting for the code to expire. If you stop the terminal with Ctrl-C, or the code expires first, you get that same fresh-login instruction at the deadline. Neither denial, cancellation nor expiry replaces an existing stored credential, so a failed device login leaves a working profile intact.

Where to find your sign-in diagnostics​

A tenant administrator reads the authenticated, company-scoped audit history in Admin → Audit Log (/admin?tab=audit), or through the supported CLI commands:

custdctl audit events list --profile production --scope tenant --company-slug example
custdctl audit events describe --profile production --scope tenant --company-slug example <event-id>
custdctl audit events export --profile production --format csv --scope tenant --company-slug example

Records carry scoped facts and correlation identifiers, never passwords, tokens, codes or cookies. Company sign-in and consent activity narrows with --action auth.login and --action auth.consent.

Per-realm sign-in history is readable by the company that owns the realm. Application-realm sign-in activity is recorded against the realm's company, resolved from the realm the request already resolved, so the company-scoped read returns it and --environment-id narrows to a single realm:

custdctl audit events list --profile production --scope tenant --company-slug example \
--environment-id <environment-id>

Events recorded before privileged migration 216 installed the realm's company lookup carry the environment as their tenant instead, so those appear only in the whole-platform read:

custdctl audit events list --profile production --scope global \
--environment-id <environment-id>

The realm is recorded from the realm the request already resolved, never from anything a caller sent, and the filter only ever narrows what your account may already read, so naming another company's environment returns nothing. Events that happened before a realm could be established (for example a request that never named a trusted client) carry no realm and appear only in the unfiltered read. The same filter is available as environmentId on the audit API and as a field in Admin → Audit Log, where it narrows whatever scope your account can already read.

To separate "the request never arrived" from "the server refused it":

custdctl diagnose --profile production --slug example

Per-realm sign-in entry, theming and methods​

A realm (one project environment) can be addressed and branded for its own users. Configure it in Admin → Auth Setup for the environment, or through the same auth-project desired state the rest of the realm settings use (entryPath, brandAccent, signInMethods on the desired document):

  • Entry path. Set a short lower-case path (3-63 characters, for example acme-demo) and share https://auth.custd.com/r/acme-demo/oauth2/auth?client_id=<your client id>&… with your users. The request is forwarded to the provider with your parameters untouched, and the path is only an address: it still has to name a client that belongs to that realm, so a guessed path cannot sign anyone in. A path that is already configured for another environment is refused, not silently reassigned.
  • Accent. One #rrggbb colour drives the primary button and links on that realm's sign-in and consent pages. No CSS, selectors or style strings are accepted.
  • Logo. Upload one PNG or WebP, at most 256 KB and 1024×1024, and Custd serves it from its own origin (/r/<environment-id>/brand/logo). A remote logo URL is deliberately not accepted, so a sign-in page can never load an asset from somebody else's host; a realm without a logo keeps the shared mark.
  • Sign-in methods. Offer a subset of password, passkey, code, magic_link and social, or leave it empty for every method your directory can complete. The page never offers a method the realm cannot finish.

Branding is presentation only: it never selects a client, audience, scope or realm, and an expired or unusable request still shows the neutral restart page.

Not yet available​

Do not plan around capabilities that do not exist yet:

  • Realm-specific entry by hostname. Entry is path-based on the shared auth host. A per-realm domain is not configurable.
  • Arbitrary copy, CSS or templates. A realm configures one accent colour and one logo; page copy and layout stay with Custd.
  • Per-realm email templates or sender identity. Invitation and sign-in email rendering is not yet a per-realm setting.

See Manage Users and Access for the surrounding identity administration surface.