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:
| Carrier | Enforce |
|---|---|
| Identity token | aud is your client ID, custd-app-<environmentID>-<audienceSlug> |
| Access token | aud is exactly your configured application audience |
| Both | groups 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_idprincipal_kind(alwaysapplication),application_principal_id,application_roleprovider_issuer,provider_subject(the directory's mapping namespace and subject, not the token issuer or thesub)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 sharehttps://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
#rrggbbcolour 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_linkandsocial, 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.