Authentication

JWT, sessions, OAuth, captcha & the full auth surface

Authentication is the largest and most consequential block in NucleusConfigOptions. It decides whether your service is an Identity Provider or a resource server, how tokens are minted and delivered, what makes a valid password, and which of the dozen-plus auth routes are exposed.

The design philosophy is opt-in: nothing is mounted unless you ask for it. You enable the routes you need, and each carries a small, predictable set of switches. Secrets are always referenced by environment-variable name, and the frontend components in nucleus-core-ts/fe read this very config so the UI and API can never drift apart.

This page walks the block top to bottom — modes and tokens first, then every route, then the external integrations (captcha, OAuth, API keys, cohorts).

Modes — IDP vs consumer#

Authentication runs in one of two modes, and the choice reshapes what the framework mounts. full mode makes the service an Identity Provider; consumer mode makes it a resource server that trusts tokens minted elsewhere.

enabledbooleanOptional

Master switch for the entire authentication subsystem.

Defaultfalse
mode'full' | 'consumer'Required

Required when enabled. full provisions the auth tables, mounts all auth routes and runs the session middleware — this is your IDP. consumer mounts no auth tables or routes; it only verifies incoming JWTs, delegating login to a separate IDP.

  • fullIDP: auth tables + auth routes + JWT/session middleware.
  • consumerResource server: JWT-verify middleware only, no auth tables or routes.
defaultRolestringOptional

Role assigned to every newly created user (register, OAuth auto-create, invite). If the named role is missing a warning is logged and sign-up still succeeds — default-role assignment is intentionally non-fatal.

Example: "user-free"

idpUrlstringOptional

For consumer mode with authorization enabled: the URL (or env-var name) of the IDP whose POST /auth/check this service calls for scope-based decisions.

Example: "http://idp-api:9000"

Cookies & cross-subdomain auth#

Tokens are delivered as HttpOnly cookies by default. These two settings control how those cookies behave across your domains and how aggressively the middleware refreshes them.

cookieDomainstringOptional

Cookie Domain attribute for sharing auth across subdomains. Use a leading dot — ".vorion.ai" — to cover every subdomain. Accepts a literal value or an env-var name. Omit for host-only cookies.

cookieMaxAgeBufferSecondsnumberOptional

A safety buffer applied to cookie Max-Age so the middleware proactively refreshes a token slightly before it actually expires, avoiding edge-of-expiry race conditions.

Default0

Password policy#

Server-enforced rules applied on register, password-reset and password-change. The frontend components read the same policy to render a live strength meter, so client and server never disagree.

passwordPolicyobjectOptional

Composable rules for what makes an acceptable password.

minLengthnumberOptional

Minimum character count.

maxLengthnumberOptional

Maximum character count (guards against DoS via huge inputs).

requireUppercasebooleanOptional

Require at least one A–Z character.

requireLowercasebooleanOptional

Require at least one a–z character.

requireNumberbooleanOptional

Require at least one digit.

requireSpecialCharbooleanOptional

Require one character from specialChars.

specialCharsstringOptional

The exact set of characters counted as special.

preventCommonPasswordsbooleanOptional

Reject passwords found in a common-password blocklist.

preventUserInfoInPasswordbooleanOptional

Reject passwords containing the user's email or name fragments.

showStrengthIndicatorbooleanOptional

Hint the FE to render a live strength meter.

Token strategy#

Nucleus issues three JWTs with distinct jobs. The access token authorizes API calls (short-lived). The refresh token mints new access tokens (long-lived). The session token identifies the device session for management and revocation. All three share the same field shape, shown under accessToken below.

accessTokenobjectOptional

Short-lived API credential — minutes, not days. Verified on every protected request.

secretstringOptional

Name of the env var holding the signing secret (e.g. "JWT_SECRET"). Resolved at boot — never the literal key.

expiresInstringOptional

Lifetime as a duration string: 15m, 1h, 7d, 30d. Drives both the JWT exp claim and the cookie Max-Age.

algorithmAlgorithmOptional

JWT signing algorithm — HS256 by default.

issuerstringOptional

The iss claim written into and verified on the token.

audiencestringOptional

The aud claim written into and verified on the token.

namestringOptional

The cookie name this token is stored under (e.g. access_token, refresh_token, session_token).

setHeadersEnabledbooleanOptional

When true the token is written as an HttpOnly Set-Cookie on auth responses. Disable for pure header/JSON token flows.

Defaulttrue
returnJsonbooleanOptional

When true the raw token is also included in the JSON response body — useful for native/mobile clients that don't use cookies.

Defaulttrue
refreshTokenobjectOptional

Long-lived (days). Exchanged at the refresh route for a fresh access token. Same fields as accessToken, with its own secret, name and a longer expiresIn (e.g. 7d).

sessionTokenobjectOptional

Identifies the device session (e.g. 30d) so users can list and revoke devices. Same fields as accessToken with its own secret and name.

The shape of an auth route#

Every auth route shares three fields, so once you understand these you understand them all. Routes you don't configure simply aren't mounted — you opt in to exactly the surface you need.

<route>objectOptional

Each route object (login, logout, register, …) carries at least these three fields, plus route-specific extras documented below.

enabledbooleanOptional

Whether the route is mounted at all.

isPublicbooleanOptional

When true the route skips the auth middleware (anyone can call it). Login and register are public; logout and me are not.

routestringOptional

The path the handler is mounted at, e.g. /auth/login.

Login#

Email + password sign-in that mints the three tokens.

loginobjectOptional

Credential sign-in configuration.

rememberMebooleanOptional

Allow a persistent session — extends cookie lifetimes when the client opts in.

redirectUrlstringOptional

Where the FE should land after a successful login.

captcha{ enabled?: boolean }Optional

Require a solved captcha before accepting credentials — a strong brute-force deterrent.

Register & email verification#

Self-service sign-up. It can optionally create a profile row, surface name fields, link terms/privacy URLs, and gate the account behind email verification with fully templated mails.

registerobjectOptional

Sign-up configuration.

showFirstName / showLastNamebooleanOptional

Toggle name fields on the FE register form.

createProfileOnRegisterbooleanOptional

Insert a linked profile row as part of sign-up.

termsUrl / privacyUrlstringOptional

Links rendered beside the consent checkbox.

emailVerificationobjectOptional

Gate the account until the email is confirmed. Fields: enabled, provider ('gmail' | 'azure'), tokenExpiresIn, redirectUrl, resendCooldown, maxResendAttempts, and templates for the verification and welcome emails (subject + htmlTemplatePath).

Sessions & device management#

Beyond issuing tokens, Nucleus tracks each session as a device. Users can list active devices and revoke them; new devices can require email approval before they're trusted.

sessionsobjectOptional

Device-session policy.

maxActiveSessionsnumberOptional

Cap on concurrent sessions per user; oldest is evicted past the limit.

inactivityTimeoutstringOptional

Idle duration after which a session expires (e.g. 30d).

allowMultipleDevicesbooleanOptional

Permit simultaneous sessions on different devices.

trustNewDevicesbooleanOptional

When false, a login from an unrecognised device requires approval before it's granted full access.

notifyOnNewDevicebooleanOptional

Email the user when a new device signs in.

approvalRedirectUrlstringOptional

Production URL the device-approval email links to. Always set this to a real URL — proxies make request-origin unreliable.

Passwordless, recovery & lifecycle routes#

The remaining routes round out the identity surface. Each follows the route triad (enabled / isPublic / route) plus the extras noted here.

magicLinkobjectOptional

Passwordless sign-in via emailed link. Extras: verifyRoute, expiresIn (e.g. 15m), redirectUrl.

inviteobjectOptional

Admin-initiated user creation. Extras: tokenExpiresIn (e.g. 7d), redirectUrl to a set-password page.

passwordResetobjectOptional

Forgot-password flow. Extra: redirectUrl to the reset form.

passwordChangeobjectOptional

Change password while authenticated (requires current password).

passwordSetobjectOptional

Set an initial password (invited/OAuth users with none yet).

emailVerificationobjectOptional

Standalone verify + resend endpoints (route, resendRoute).

refreshobjectOptional

Exchanges a refresh token for a new access token. Usually isPublic.

logoutobjectOptional

Revokes the session and clears auth cookies.

WebAuthn / passkeys#

Hardware-backed, phishing-resistant authentication. Configure the relying party so browsers bind credentials to your origin.

webauthnobjectOptional

Passkey configuration.

rpNamestringOptional

Human-readable relying-party name shown in the OS/browser prompt.

rpIDstringOptional

Relying-party ID — your registrable domain (e.g. vorion.ai).

expectedOriginsstring[]Optional

Allowed origins that may complete a ceremony.

challengeTtlstringOptional

How long a registration/authentication challenge stays valid.

Current user (/auth/me)#

Returns the authenticated user. The include* flags let you eager-load related records in one round-trip instead of N follow-up calls.

meobjectOptional

Profile endpoint configuration.

includeProfilebooleanOptional

Embed the linked profile row.

includeAddressesbooleanOptional

Embed the user's addresses.

includePhonesbooleanOptional

Embed the user's phone numbers.

includeFilesbooleanOptional

Embed associated files (e.g. avatar).

includeRolesbooleanOptional

Embed assigned roles.

Captcha#

A self-hosted challenge generated and validated server-side, backed by Redis. No third-party script, no external network call — useful in front of login and register.

captchaobjectOptional

Challenge configuration.

type'math' | 'image' | 'puzzle' | 'text'Optional

Challenge style: arithmetic, distorted image, drag-to-fit puzzle, or text transcription.

difficulty'easy' | 'medium' | 'hard'Optional

How hard the generated challenge is.

expiresInstringOptional

How long a generated challenge stays solvable (e.g. 5m).

maxAttemptsnumberOptional

Wrong answers allowed before a challenge is burned.

caseSensitivebooleanOptional

Whether text answers are compared case-sensitively.

rateLimitobjectOptional

Per-client generation caps: maxGeneratePerMinute and maxGeneratePerHour — stops attackers farming fresh challenges.

OAuth & social login#

Sign in with external identity providers. Nucleus handles the full authorization-code dance, optional account linking, and auto-provisioning of users on first login.

oauthobjectOptional

Social-login configuration.

basePathstringOptional

Prefix for the OAuth start/callback routes.

successRedirectUrl / errorRedirectUrlstringOptional

Where the browser lands after the flow resolves.

allowAccountLinkingbooleanOptional

Link a provider identity to an existing user matched by email rather than rejecting the login.

autoCreateUserbooleanOptional

Create a user automatically on first successful OAuth login.

sendInviteOnCreatebooleanOptional

Email an invite/set-password link when a user is auto-created.

stateTtlSecondsnumberOptional

Lifetime of the CSRF state token guarding the callback.

providersobjectOptional

Per-provider credentials for google, github, microsoft, discord, facebook, twitter, apple and a custom provider. Each takes clientId, clientSecret, redirectUri, optional scopes[] and extraAuthParams. microsoft adds tenantId; custom adds authorizationUrl, tokenUrl and userInfoUrl.

API keys#

Programmatic credentials for server-to-server access, independent of user sessions. Keys can belong to a user or to an application.

apiKeysobjectOptional

API-key issuance policy.

keyPrefix'nk_live' | 'nk_test'Optional

Prefix stamped on generated keys so live and test credentials are visually distinct.

Default'nk_live'
maxKeysPerUsernumberOptional

Cap on active keys a single user may hold.

Default10
defaultExpiresInstringOptional

Default key lifetime when the caller doesn't specify one.

allowApplicationKeysbooleanOptional

Permit application-owned keys (not tied to a user). Requests for application keys are rejected with 403 when false.

Defaulttrue
preventApiKeyManagementbooleanOptional

Lock down the key-management endpoints entirely.

Defaultfalse

Cohorts & email-exempt domains#

Two smaller switches that round out the auth surface — group-based user organisation and a verification bypass for trusted domains.

cohorts{ enabled?: boolean; basePath?: string }Optional

Enables cohort endpoints for grouping users into segments. When enabled the cohort endpoints are auto-added to the generated client. basePath sets their route prefix.

emailExemptDomainsstring[]Optional

Email domains whose users skip email verification on register (matched by isEmailExempt). Perfect for trusted internal domains where confirmation is redundant.

Example: ["yourcompany.com", "internal.local"]

Under the hood — tokens & sessions#

A successful login runs issueSession, which choreographs three primitives: it creates a server-side session, mints a refresh token, then signs the access token — rolling everything back if any step fails. Understanding the pieces explains why sessions are revocable and multi-device.

issueSession()orchestratorOptional

generateSession (writes a SessionRecord) → generateRefreshToken (writes a Redis-backed refresh record) → signJWT (the access token). If the refresh step fails the just-created session is deleted, so a half-issued login never lingers. Returns accessToken, refreshToken, their expiries and the sessionId.

access tokenHS256 JWT · ~15m defaultOptional

A hand-rolled HS256 JWT (sub = userId, iss = 'auth', aud = 'api', plus iat/exp). It carries the sessionId and embeds the refresh token id as a custom claim, so the server can tie an access token back to its session and refresh record. Default TTL is 15 minutes; accessToken config tunes it.

refresh tokenJWT (aud:'refresh') · Redis-backedOptional

Also a signed JWT, but the source of truth is its record in Redis. Validation verifies the signature, asserts aud === 'refresh' (so an access token can't be replayed as a refresh token), then looks the record up in Redis and checks expiry. Because the record lives in Redis, revocation is simply deleting the key — that is how logout, logout-everywhere and per-session revoke work.

single-flight refreshrefreshAccessTokenWithLockOptional

Token rotation is serialised with a Redis lock (acquireLock), so a burst of tabs refreshing at once rotates exactly once instead of racing and invalidating each other. The losers wait on the lock and read the freshly-minted token.

SessionRecordRedis — multi-deviceOptional

Each session stores id, userId, createdAt/expiresAt/lastActiveAt, deviceInfo (name, type desktop|mobile|tablet, browser, OS, IP, userAgent), fingerprintHash, refreshTokenHash, loginMethod and rememberMe. This per-device record is exactly what the Sessions/Devices endpoints (and the DevicesPage UI) list and revoke.

Under the hood — passwords & device binding#

Two more primitives protect the credential and the session: how passwords are stored, and how a session is bound to the device that created it.

password hashingscrypt (node:crypto)Optional

Passwords are hashed with scryptSync using a fresh 16-byte random salt and a 64-byte derived key, stored as salt_hex:derivedKey_hex. Verification re-derives with the stored salt and compares — no external bcrypt/argon dependency, just the Node crypto primitive. The password-policy config governs what is allowed before this point.

device fingerprintGenerate / ValidateOptional

At login a fingerprint is derived from request characteristics and its hash saved on the session. On subsequent requests the fingerprint is recomputed and compared, so a stolen token presented from a very different device can be flagged (fingerprintValid in the validation result).

session validationValidateSessionResultOptional

Validating a request checks the JWT signature, that the session still exists in the store, and (when supplied) that the device fingerprint matches — returning { isValid, reason, context: { userId, sessionId, fingerprintValid } } so middleware can both authorise and explain a rejection.

Related sections