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.
enabledbooleanOptionalMaster switch for the entire authentication subsystem.
falsemode'full' | 'consumer'RequiredRequired 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.
full— IDP: auth tables + auth routes + JWT/session middleware.consumer— Resource server: JWT-verify middleware only, no auth tables or routes.
defaultRolestringOptionalRole 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"
idpUrlstringOptionalFor 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"
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.
passwordPolicyobjectOptionalComposable rules for what makes an acceptable password.
minLengthnumberOptionalMinimum character count.
maxLengthnumberOptionalMaximum character count (guards against DoS via huge inputs).
requireUppercasebooleanOptionalRequire at least one A–Z character.
requireLowercasebooleanOptionalRequire at least one a–z character.
requireNumberbooleanOptionalRequire at least one digit.
requireSpecialCharbooleanOptionalRequire one character from specialChars.
specialCharsstringOptionalThe exact set of characters counted as special.
preventCommonPasswordsbooleanOptionalReject passwords found in a common-password blocklist.
preventUserInfoInPasswordbooleanOptionalReject passwords containing the user's email or name fragments.
showStrengthIndicatorbooleanOptionalHint 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.
accessTokenobjectOptionalShort-lived API credential — minutes, not days. Verified on every protected request.
secretstringOptionalName of the env var holding the signing secret (e.g. "JWT_SECRET"). Resolved at boot — never the literal key.
expiresInstringOptionalLifetime as a duration string: 15m, 1h, 7d, 30d. Drives both the JWT exp claim and the cookie Max-Age.
algorithmAlgorithmOptionalJWT signing algorithm — HS256 by default.
issuerstringOptionalThe iss claim written into and verified on the token.
audiencestringOptionalThe aud claim written into and verified on the token.
namestringOptionalThe cookie name this token is stored under (e.g. access_token, refresh_token, session_token).
setHeadersEnabledbooleanOptionalWhen true the token is written as an HttpOnly Set-Cookie on auth responses. Disable for pure header/JSON token flows.
truereturnJsonbooleanOptionalWhen true the raw token is also included in the JSON response body — useful for native/mobile clients that don't use cookies.
truerefreshTokenobjectOptionalLong-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).
sessionTokenobjectOptionalIdentifies 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>objectOptionalEach route object (login, logout, register, …) carries at least these three fields, plus route-specific extras documented below.
enabledbooleanOptionalWhether the route is mounted at all.
isPublicbooleanOptionalWhen true the route skips the auth middleware (anyone can call it). Login and register are public; logout and me are not.
routestringOptionalThe path the handler is mounted at, e.g. /auth/login.
Login#
Email + password sign-in that mints the three tokens.
loginobjectOptionalCredential sign-in configuration.
rememberMebooleanOptionalAllow a persistent session — extends cookie lifetimes when the client opts in.
redirectUrlstringOptionalWhere the FE should land after a successful login.
captcha{ enabled?: boolean }OptionalRequire 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.
registerobjectOptionalSign-up configuration.
showFirstName / showLastNamebooleanOptionalToggle name fields on the FE register form.
createProfileOnRegisterbooleanOptionalInsert a linked profile row as part of sign-up.
termsUrl / privacyUrlstringOptionalLinks rendered beside the consent checkbox.
emailVerificationobjectOptionalGate 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.
sessionsobjectOptionalDevice-session policy.
maxActiveSessionsnumberOptionalCap on concurrent sessions per user; oldest is evicted past the limit.
inactivityTimeoutstringOptionalIdle duration after which a session expires (e.g. 30d).
allowMultipleDevicesbooleanOptionalPermit simultaneous sessions on different devices.
trustNewDevicesbooleanOptionalWhen false, a login from an unrecognised device requires approval before it's granted full access.
notifyOnNewDevicebooleanOptionalEmail the user when a new device signs in.
approvalRedirectUrlstringOptionalProduction 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.
magicLinkobjectOptionalPasswordless sign-in via emailed link. Extras: verifyRoute, expiresIn (e.g. 15m), redirectUrl.
inviteobjectOptionalAdmin-initiated user creation. Extras: tokenExpiresIn (e.g. 7d), redirectUrl to a set-password page.
passwordResetobjectOptionalForgot-password flow. Extra: redirectUrl to the reset form.
passwordChangeobjectOptionalChange password while authenticated (requires current password).
passwordSetobjectOptionalSet an initial password (invited/OAuth users with none yet).
emailVerificationobjectOptionalStandalone verify + resend endpoints (route, resendRoute).
refreshobjectOptionalExchanges a refresh token for a new access token. Usually isPublic.
logoutobjectOptionalRevokes the session and clears auth cookies.
WebAuthn / passkeys#
Hardware-backed, phishing-resistant authentication. Configure the relying party so browsers bind credentials to your origin.
webauthnobjectOptionalPasskey configuration.
rpNamestringOptionalHuman-readable relying-party name shown in the OS/browser prompt.
rpIDstringOptionalRelying-party ID — your registrable domain (e.g. vorion.ai).
expectedOriginsstring[]OptionalAllowed origins that may complete a ceremony.
challengeTtlstringOptionalHow 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.
meobjectOptionalProfile endpoint configuration.
includeProfilebooleanOptionalEmbed the linked profile row.
includeAddressesbooleanOptionalEmbed the user's addresses.
includePhonesbooleanOptionalEmbed the user's phone numbers.
includeFilesbooleanOptionalEmbed associated files (e.g. avatar).
includeRolesbooleanOptionalEmbed 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.
captchaobjectOptionalChallenge configuration.
type'math' | 'image' | 'puzzle' | 'text'OptionalChallenge style: arithmetic, distorted image, drag-to-fit puzzle, or text transcription.
difficulty'easy' | 'medium' | 'hard'OptionalHow hard the generated challenge is.
expiresInstringOptionalHow long a generated challenge stays solvable (e.g. 5m).
maxAttemptsnumberOptionalWrong answers allowed before a challenge is burned.
caseSensitivebooleanOptionalWhether text answers are compared case-sensitively.
rateLimitobjectOptionalPer-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.
oauthobjectOptionalSocial-login configuration.
basePathstringOptionalPrefix for the OAuth start/callback routes.
successRedirectUrl / errorRedirectUrlstringOptionalWhere the browser lands after the flow resolves.
allowAccountLinkingbooleanOptionalLink a provider identity to an existing user matched by email rather than rejecting the login.
autoCreateUserbooleanOptionalCreate a user automatically on first successful OAuth login.
sendInviteOnCreatebooleanOptionalEmail an invite/set-password link when a user is auto-created.
stateTtlSecondsnumberOptionalLifetime of the CSRF state token guarding the callback.
providersobjectOptionalPer-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.
apiKeysobjectOptionalAPI-key issuance policy.
keyPrefix'nk_live' | 'nk_test'OptionalPrefix stamped on generated keys so live and test credentials are visually distinct.
'nk_live'maxKeysPerUsernumberOptionalCap on active keys a single user may hold.
10defaultExpiresInstringOptionalDefault key lifetime when the caller doesn't specify one.
allowApplicationKeysbooleanOptionalPermit application-owned keys (not tied to a user). Requests for application keys are rejected with 403 when false.
truepreventApiKeyManagementbooleanOptionalLock down the key-management endpoints entirely.
falseCohorts & 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 }OptionalEnables 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[]OptionalEmail 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()orchestratorOptionalgenerateSession (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 defaultOptionalA 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-backedOptionalAlso 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 refreshrefreshAccessTokenWithLockOptionalToken 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-deviceOptionalEach 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)OptionalPasswords 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 / ValidateOptionalAt 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 validationValidateSessionResultOptionalValidating 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