SSO (OAuth & OIDC)
Sign in to the dashboard with Google, GitHub, or any OIDC provider. Domain/org allowlists, OAuth-only mode.
Sign in to the dashboard with Google, GitHub, or any OIDC provider. Domain/org allowlists, OAuth-only mode.
The dashboard's session auth supports native
sign-in for Google, GitHub, and any OIDC-compliant provider (Okta,
Auth0, Keycloak, Microsoft Entra, Dex, …). Multiple OIDC providers can run side
by side, each its own button on the login screen. OAuth requires session auth to
be enabled (serveDashboard(queue, { authEnabled: true }), or
flexiq dashboard --auth; the dashboard serves openly by default) and is itself
off by default — setting any provider's env vars turns it on, alongside password
login unless you opt out.
ID-token validation for Google and generic OIDC runs on Node's built-in
node:crypto — signature verification against the provider's JWKS, then the
issuer / audience / nonce / expiry claim checks. Unlike the Java SDK (which
needs nimbus-jose-jwt on the classpath), the Node SDK bundles no JOSE
library and needs no extra install — everything works with @byteveda/flexiq
alone on Node 20+. GitHub login is plain OAuth2 (no id-token to verify) and
likewise needs nothing extra.
State is single-use — OAuthStateStore.consume deletes the row (from the
queue's settings store, under auth:oauth_state:<state>) before parsing it, so a
replayed callback finds nothing — and time-bounded (5-minute TTL). PKCE with
S256, the OIDC nonce, and the ID-token signature (against the provider's JWKS)
are all enforced server-side.
serveDashboard wires this up for you: with authEnabled: true and no explicit
oauth option, it builds the flow from the FLEXIQ_DASHBOARD_OAUTH_*
environment variables. Invalid or partial config is logged and OAuth is disabled
(password login still works) rather than crashing startup.
import { Queue, serveDashboard } from "@byteveda/flexiq";
const queue = new Queue({ dbPath: "flexiq.db" });
// authEnabled turns on session auth; OAuth is built from the
// FLEXIQ_DASHBOARD_OAUTH_* env vars when they are set.
serveDashboard(queue, { port: 8787, authEnabled: true });To configure providers in code instead of env vars, build the flow yourself
and pass it as oauth:
import { Queue, OAuthFlow, oauthConfigFromEnv, serveDashboard } from "@byteveda/flexiq";
const queue = new Queue({ dbPath: "flexiq.db" });
const config = oauthConfigFromEnv(); // or hand-build an OAuthConfig
serveDashboard(queue, {
authEnabled: true,
oauth: config ? new OAuthFlow(queue, config) : undefined,
});Create an OAuth client in the Google Cloud Console → APIs & Services → Credentials (type Web application) and register the callback URL:
https://flexiq.your-company.com/api/auth/oauth/callback/google
For local development, http://localhost:8787/api/auth/oauth/callback/google
works without HTTPS (8787 is the dashboard's default port).
Set env vars before starting the dashboard:
export FLEXIQ_DASHBOARD_OAUTH_REDIRECT_BASE_URL=https://flexiq.your-company.com
export FLEXIQ_DASHBOARD_OAUTH_GOOGLE_CLIENT_ID=...apps.googleusercontent.com
export FLEXIQ_DASHBOARD_OAUTH_GOOGLE_CLIENT_SECRET=...
# Restrict logins to your Google Workspace domain:
export FLEXIQ_DASHBOARD_OAUTH_GOOGLE_ALLOWED_DOMAINS=your-company.comStart the dashboard with session auth on
(serveDashboard(queue, { authEnabled: true }) or flexiq dashboard --auth).
The login screen now shows a "Continue with Google" button above the password
form.
When exactly one domain is allowlisted, the dashboard also passes it to Google as
the hd hint so the right Workspace account is pre-selected — a UX nicety only;
enforcement always happens server-side against the verified email.
GitHub has no OIDC id-token, so identity comes from GET /user and
GET /user/emails; org membership is verified via
GET /orgs/{org}/members/{login}.
Create a GitHub OAuth App. Set
the Authorization callback URL to
https://flexiq.your-company.com/api/auth/oauth/callback/github.
Env vars:
export FLEXIQ_DASHBOARD_OAUTH_REDIRECT_BASE_URL=https://flexiq.your-company.com
export FLEXIQ_DASHBOARD_OAUTH_GITHUB_CLIENT_ID=Iv1.xxxxx
export FLEXIQ_DASHBOARD_OAUTH_GITHUB_CLIENT_SECRET=...
# Restrict logins to members of these GitHub orgs:
export FLEXIQ_DASHBOARD_OAUTH_GITHUB_ALLOWED_ORGS=your-org,partner-orgWhen _ALLOWED_ORGS is set, the requested scope automatically grows from
read:user user:email to add read:org, so the membership check returns
reliable results for private orgs.
A GitHub account with no primary && verified email (from
GET /user/emails) always lands in the viewer role, even if listed in
FLEXIQ_DASHBOARD_OAUTH_ADMIN_EMAILS — an unverified email can't be
trusted to grant admin.
Generic OIDC providers are named slots — each gets its own callback URL, its own namespaced users, and its own button on the login screen.
export FLEXIQ_DASHBOARD_OAUTH_REDIRECT_BASE_URL=https://flexiq.your-company.com
# List the slots first.
export FLEXIQ_DASHBOARD_OAUTH_OIDC_PROVIDERS=okta,microsoft
# Then per-slot config (slot name uppercased; '-' becomes '_').
export FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_CLIENT_ID=...
export FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_CLIENT_SECRET=...
export FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_DISCOVERY_URL=https://acme.okta.com/.well-known/openid-configuration
export FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_LABEL="Acme SSO"
export FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_ALLOWED_DOMAINS=your-company.com
export FLEXIQ_DASHBOARD_OAUTH_OIDC_MICROSOFT_CLIENT_ID=...
export FLEXIQ_DASHBOARD_OAUTH_OIDC_MICROSOFT_CLIENT_SECRET=...
export FLEXIQ_DASHBOARD_OAUTH_OIDC_MICROSOFT_DISCOVERY_URL=https://login.microsoftonline.com/TENANT/v2.0/.well-known/openid-configuration
export FLEXIQ_DASHBOARD_OAUTH_OIDC_MICROSOFT_LABEL="Microsoft 365"Each slot requires _CLIENT_ID, _CLIENT_SECRET, and _DISCOVERY_URL — a slot
listed in _OIDC_PROVIDERS with any of those missing fails startup
(fail-fast). The callback URL for each slot is
{REDIRECT_BASE_URL}/api/auth/oauth/callback/{slot} — register that exact
URL with the identity provider.
Slot names must match ^[a-z][a-z0-9_-]{0,31}$ and can't collide with
google / github (the built-ins). A user signing in through an OIDC slot is
namespaced as {slot}:{subject}, so two providers issuing overlapping subject
values never collide. Because the env prefix uppercases the slot and turns -
into _, two slots that would map to the same prefix (for example okta-eu and
okta_eu) are rejected at startup rather than silently sharing one credential
set.
The first time someone signs in via any provider, the dashboard decides their role with this precedence:
FLEXIQ_DASHBOARD_OAUTH_ADMIN_EMAILS match — a case-insensitive match
against a verified email → admin.viewer. There is no first-user fallback: even the very
first OAuth login gets viewer unless its verified email is on the admin
list. Admin access comes only from the allowlist, the first-run setup screen,
or the FLEXIQ_DASHBOARD_ADMIN_USER / FLEXIQ_DASHBOARD_ADMIN_PASSWORD
bootstrap.export FLEXIQ_DASHBOARD_OAUTH_ADMIN_EMAILS=alice@your-company.com,bob@your-company.comIf providers are configured but FLEXIQ_DASHBOARD_OAUTH_ADMIN_EMAILS is
empty, the dashboard logs a startup warning — every OAuth login would get
viewer, so an OAuth-only deployment would have zero admins. Seed at least one
admin via the allowlist or the env bootstrap.
A user's role is not re-evaluated on later logins — change it from the dashboard once the user exists. Their email and display name refresh from each login's claims.
Disable password login entirely:
export FLEXIQ_DASHBOARD_PASSWORD_AUTH_ENABLED=falseStartup fails (an OAuthConfigError is raised at config-parse time) if you
disable password auth without configuring at least one provider — that would
leave no way to log in. With password auth off and at least one provider
configured, the login page hides the username/password form and renders only the
provider buttons. The flag accepts true/false, 1/0, yes/no, on/off and
defaults to true.
| Control | Implementation |
|---|---|
| PKCE | S256 challenge — base64url(sha256(verifier)) — derived from a 32-byte random verifier (RFC 7636). |
| State | 32-byte URL-safe random, persisted server-side in the queue's settings store under auth:oauth_state:<state> with a 5-minute TTL. Deleted on first read — a replayed callback always fails. |
| Nonce | 16-byte random, sent in the OIDC authorize request, checked against the ID-token's nonce claim. (GitHub has no id-token, so nonce is unused there; PKCE still applies.) |
| ID-token signature | Verified against the provider's JWKS using Node's built-in node:crypto. Only asymmetric algorithms are accepted — RS256/384/512, PS256/384/512, ES256/384/512; an alg:none or HMAC (HS*) token has no matching asymmetric key and is rejected. |
| iss / aud / exp | All checked; 60-second clock-skew tolerance on exp; a multi-audience token must additionally carry azp == client_id. |
| Discovery endpoints | The authorization_endpoint, token_endpoint, and jwks_uri read from the discovery document must be https (plain http only for local hosts) before any code or client secret is sent. |
| Open redirect | The next query param must be a bare rooted path (isSafeRedirect) — no scheme, no host, no // or /\ prefix. Falls back to /. |
| HTTPS required | The redirect base URL must be https:// unless the host is localhost / 127.0.0.1 / ::1. A misconfiguration is rejected at config-parse time. |
| Provider tokens | Never persisted — only the verified identity produces a FlexiQ session. |
| Cross-provider identity | A given (slot, subject) always maps to one user ({slot}:{subject}); the same email at two different providers creates two distinct users. |
| Method | Path | What it does |
|---|---|---|
GET | /api/auth/providers | Public. {password_enabled, providers: [{slot, label, type}]} for the login UI. When OAuth is unconfigured, falls through to the password-only handler. |
GET | /api/auth/oauth/start/{slot} | Public. Mints state, 302s to the provider's authorize URL. Accepts ?next=/path (validated). |
GET | /api/auth/oauth/callback/{slot} | Public. Validates state, exchanges the code, enforces the allowlist, creates/refreshes the user, sets cookies, 302s to next. |
The callback sets the same flexiq_session + flexiq_csrf cookies as
password login (HttpOnly on the session cookie, SameSite=Strict, Secure
unless secureCookies: false) — every other dashboard route works identically
once you're signed in.
Callback failures redirect to /login?error=<code> rather than rendering
JSON:
oauth_state_invalid — the state row expired (5-minute window) or was
already consumed. Usually the user pressed back/refresh after the provider
redirected; have them restart the flow.oauth_denied — the identity was fetched successfully but rejected by
an allowlist (ALLOWED_DOMAINS / ALLOWED_ORGS). Widen the allowlist or
remove it.oauth_failed — token exchange, signature verification, or a claim
check failed. Check server logs for the underlying message (issuer
mismatch, expired token, missing claim, provider error).oauth_not_configured (404 on start/callback) — the slot isn't
registered. Either the env vars weren't set, or config parsing failed at
startup (check for a startup warning) and OAuth degraded to disabled.Provider button missing — GET /api/auth/providers returns the list the
login screen renders. If a provider you configured isn't there, check the
server log at startup: buildOauthFlowFromEnv catches any OAuthConfigError
from a partial or invalid config (for example a Google client id with no
matching secret) and logs OAuth disabled — invalid configuration: …,
disabling OAuth entirely rather than failing to start.
# Required when any provider is configured.
FLEXIQ_DASHBOARD_OAUTH_REDIRECT_BASE_URL=https://flexiq.company.com
# Google.
FLEXIQ_DASHBOARD_OAUTH_GOOGLE_CLIENT_ID=...
FLEXIQ_DASHBOARD_OAUTH_GOOGLE_CLIENT_SECRET=...
FLEXIQ_DASHBOARD_OAUTH_GOOGLE_ALLOWED_DOMAINS=company.com,partner.com # optional
# GitHub.
FLEXIQ_DASHBOARD_OAUTH_GITHUB_CLIENT_ID=Iv1.xxxxx
FLEXIQ_DASHBOARD_OAUTH_GITHUB_CLIENT_SECRET=...
FLEXIQ_DASHBOARD_OAUTH_GITHUB_ALLOWED_ORGS=org1,org2 # optional
# Generic OIDC — list slots, then configure each one.
FLEXIQ_DASHBOARD_OAUTH_OIDC_PROVIDERS=okta,microsoft
FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_CLIENT_ID=...
FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_CLIENT_SECRET=...
FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_DISCOVERY_URL=https://acme.okta.com/.well-known/openid-configuration
FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_LABEL=Acme SSO # optional
FLEXIQ_DASHBOARD_OAUTH_OIDC_OKTA_ALLOWED_DOMAINS=company.com # optional
# Role bootstrap.
FLEXIQ_DASHBOARD_OAUTH_ADMIN_EMAILS=alice@company.com,bob@company.com # optional
# Disable password login (OAuth-only mode). Defaults to true.
FLEXIQ_DASHBOARD_PASSWORD_AUTH_ENABLED=false # optionalSession auth itself (required for OAuth) is enabled with
serveDashboard(queue, { authEnabled: true }) or flexiq dashboard --auth, and
an initial admin can be bootstrapped headlessly with
FLEXIQ_DASHBOARD_ADMIN_USER / FLEXIQ_DASHBOARD_ADMIN_PASSWORD — see the
dashboard guide.