OAuth Clients
Every downstream Blu application that authenticates through BluAuth needs an OAuth client record. The client record is what actually ties a tenant together — each client represents one app, binds to one theme, binds to a subset of providers, and defines which redirect URIs BluAuth is willing to hand users back to.
Clients live at /admin/clients. The list shows every client in the tenant; the detail page at /admin/clients/:id is where most configuration happens.
What a client record carries
| Field | Purpose |
|---|---|
| Name | Human-readable label shown in admin UI and invitation emails. |
| Slug | URL-safe identifier — used in URLs like /login?client_slug=bucket-indexer. |
| Client ID | OIDC client_id — issued by BluAuth, cannot be changed. |
| Client Type | public (PKCE-only, SPAs / mobile) or confidential (has a secret, server apps). |
| Client Secret | For confidential clients only. Shown once at rotation; not retrievable. |
| Redirect URIs | Exact-match allowlist of URLs BluAuth will redirect to after auth. |
| Allowed Scopes | Which OIDC scopes the client may request. |
| Access Token TTL | How long access tokens issued for this client live. Default: 1 hour. |
| Refresh Token TTL | Refresh token lifetime. Default: 30 days. |
| Bound Theme | The hosted login experience for users of this client. See theme studio. |
| Bound Providers | The subset of tenant-wide providers exposed on this client's login page. |
| PKCE Required | On for public; configurable on for confidential. |
Creating a client
From /admin/clients:
- Click New Client.
- Enter a Name (e.g. "Bucket Indexer — Production") and a Slug (URL-safe, lowercase, dashes — e.g.
bucket-indexer-prod). - Pick a Client Type:
- Public — Single-page apps, mobile apps. Uses PKCE only (no secret). Recommended for any client that runs in a browser or untrusted environment.
- Confidential — server-to-server apps, trusted backends. Generates a client secret at creation.
- Set at least one Redirect URI (see below).
- Save.
After creation, the detail page opens. For confidential clients, the secret is shown once in a banner — copy it immediately. BluAuth does not store the plaintext; if you lose it, you must rotate.
Client naming conventions for discoverability
Use these conventions — they aren't enforced, but they keep the admin UI navigable as your tenant grows:
- Name format:
{App Name} — {Environment}(e.g. "Bucket Indexer — Staging", "Bucket Indexer — Production"). - Slug format:
{app-name}-{env}(e.g.bucket-indexer-staging,bucket-indexer-prod). - Environment per client: don't reuse one client across staging and production. Separate clients make redirect URIs cleaner and avoid accidental cross-environment token issuance.
- One client per app, per environment. Resist the urge to make a "shared" client for multiple apps — theme and provider binding are per-client, so sharing a client means sharing a login experience.
Redirect URIs
BluAuth enforces exact-match on redirect URIs. If the requested redirect_uri doesn't string-match an entry in the allowlist, the authorize call returns invalid_redirect_uri — no substitution, no normalization.
Rules
- Must use
https://in production. http://localhost:*is allowed — any port, any path. Useful for local development without registering every port.- Query strings and fragments are not part of the match — BluAuth ignores them during comparison. The path and trailing slash do matter.
- Wildcards are not supported. Register each exact URI.
Common matching gotchas
https://app.example.com/callbackwill not matchhttps://app.example.com/callback/(trailing slash). Register both if both are used.- Case sensitivity: host is case-insensitive, path is case-sensitive.
- Scheme is required —
app.example.com/callbackis not a valid entry.
Allowlist syntax
Each entry is a single URI. You can register multiple:
https://app.example.com/callback
https://app.example.com/oauth/callback
http://localhost:3000/callback
http://localhost:4200/callback
Add them in the Redirect URIs panel on the client detail page. Changes apply immediately.
Client secrets (confidential clients)
Confidential clients have a client secret that's sent alongside the client ID when exchanging an auth code for tokens. The secret is encrypted at rest with AWS KMS (same pipeline as provider secrets — see the providers guide).
Shown once
At creation and at rotation, the plaintext is shown in a banner at the top of the detail page. Copy it immediately — BluAuth stores only the hashed/encrypted version; the UI cannot show it again.
Rotation
Click Rotate Secret on the detail page. BluAuth:
- Generates a new secret.
- Encrypts and stores it.
- Invalidates the old secret immediately — not after a grace period.
- Displays the new plaintext once.
Because rotation is immediate, coordinate with the downstream app's deploy. Recommended flow:
- Rotate in BluAuth.
- Deploy the new secret to the downstream app's environment.
- Verify the app can still mint tokens.
If you need a zero-downtime rotation, register two clients temporarily: the app reads from both, you cut over, then delete the old one.
Compromised secrets
If you suspect a secret is compromised, rotate immediately — don't wait for a deploy window. The minutes of downtime until the downstream app re-deploys with the new secret are vastly preferable to the open attack surface.
PKCE
PKCE is required for all public clients. The code_challenge + code_verifier pair on the authorize / token requests is the only proof of possession a public client has — without PKCE, a browser-based app has no way to defend against an intercepted authorization code.
For confidential clients, PKCE is optional but recommended. Many modern backend apps combine client authentication (the secret) with PKCE; it adds defense in depth at negligible cost. The Require PKCE toggle on the detail page enforces PKCE on confidential clients.
BluAuth supports S256 (SHA-256) and plain code challenge methods. Prefer S256; plain is available only for legacy compatibility and should not be used for new integrations.
Scopes
BluAuth supports the standard OIDC scopes:
| Scope | Grants |
|---|---|
openid | Required for every request. Issues an id_token. |
profile | name, preferred_username, picture claims in the id_token and /userinfo. |
email | email, email_verified claims. |
The allowedScopes field on the client limits what the client is permitted to request. If the client asks for a scope not on the list, the authorize call returns 400 with invalid_scope. Keep this list minimal — a client that only needs openid email should have exactly that, nothing more.
Access token TTL
Default: 1 hour (3600 seconds). Configurable per client in the Token Lifetimes panel.
- Minimum: 60 seconds.
- Maximum: 24 hours.
- Longer TTLs reduce refresh churn but extend the blast radius of a leaked token.
For high-security tenants, shorten to 15 minutes and rely on silent re-auth through the BluAuth session for longevity. For low-churn internal tools, the default is fine.
Client types: confidential vs public
| Trait | Confidential | Public |
|---|---|---|
| Has a secret | Yes | No |
| PKCE | Optional (recommended) | Required |
| Typical deployment | Server apps, trusted backends | SPAs, mobile apps, CLI tools |
| Client credentials grant | Supported | Not supported |
| Refresh tokens | Not issued today | Not issued today |
Rule of thumb: if the app runs on a machine you control and users can't see, it's confidential. If it runs on a device the user holds, it's public.
Client credentials grant
Confidential clients can use the client_credentials grant for service-to-service tokens with no user involved. The issued access token carries sub = clientId and all scopes the client is permitted to request. Use for:
- Machine integrations — webhooks, cron jobs, internal APIs.
- Non-user traffic — log ingestion, metrics pipelines.
Do not use for user-facing flows — machine tokens cannot represent a user and downstream apps that assume a sub points to a user will break.
Bound theme
Every client has exactly one bound theme. The theme drives the hosted login experience (layout, colors, copy, assets) for users authenticating against this client.
- Set the binding on the client detail page's Theme section.
- If no theme is bound, the default service theme (marked
isDefault) is used. - One theme can be bound to many clients — useful when two apps share branding.
See the theme studio overview for how themes resolve and what they control.
Bound providers
From the client detail page's Providers section, check the boxes for every tenant-wide provider you want to expose on this client's login page. Drag to reorder — the first provider in the list is the first button users see.
At least one sign-in method (a bound provider or the local password field) must be available; otherwise users have no way in.
See the providers guide for how providers are configured tenant-wide before they can be bound.
Integration guide tab
The client detail page includes an Integration Guide tab with a copy-ready OIDC configuration for your app developers:
- Issuer —
https://auth.blutools.io/(or the tenant subdomain). - Authorization endpoint —
https://auth.blutools.io/oauth2/authorize. - Token endpoint —
https://auth.blutools.io/oauth2/token. - Userinfo endpoint —
https://auth.blutools.io/oauth2/userinfo. - JWKS —
https://auth.blutools.io/.well-known/jwks.json. - Discovery —
https://auth.blutools.io/.well-known/openid-configuration. - Client ID — this client's ID.
- Allowed scopes — copy of the list.
- PKCE requirement — yes/no.
Hand this to your app developer as-is. Their OIDC library will pick up the rest from discovery.
Deletion
Deleting a client removes the client record, invalidates every token issued for it, and removes its theme and provider bindings. Existing user records and session history survive.
Deletion is irreversible. Prefer disabling (an Enabled toggle on the detail page) if you might bring the app back — disabled clients reject all authorize calls with invalid_client but remain in the UI for reference.
Common failure modes
| Symptom | Likely cause |
|---|---|
invalid_redirect_uri at authorize | Redirect URI not on the allowlist — check trailing slashes and case. |
invalid_client at token endpoint | Client secret wrong, rotated, or client disabled. |
invalid_scope at authorize | Requested scope not in allowedScopes. |
| Public client signs in once but silent re-auth fails | PKCE verifier/state handling is broken in the client, or the app is not re-entering the authorize flow after access-token expiry. |
| Login page has no buttons | No providers bound, no local password allowed — bind at least one. |
| Wrong theme renders | Theme binding stale or client slug typo in the upstream app's authorize URL. |
Next
- Theme studio overview — design the hosted login for this client.
- Providers — configure the providers you'll bind here.
- Invitations — onboard users against this client.