# @wrnexus/auth Framework-native authentication, identity, account-security, and session management for WRNexusJS. ## Capabilities - Password registration, login, recovery, reset, and authenticated password changes - Email, phone, and username identities with verification and generic resend responses - Magic links and passwordless email/SMS OTP login - MFA transactions using verified email OTP, verified SMS OTP, TOTP, or recovery codes - RFC 6238 TOTP with counter replay protection - One-use recovery codes; regeneration invalidates previous unused codes - Passkey/WebAuthn registration and strong passwordless sign-in through a provider contract - OAuth account linking and provider sign-in - Invitations, session rotation, idle and absolute expiry, revocation, and trusted devices - Deny-by-default audited support impersonation - Adaptive risk scoring, CAPTCHA escalation, temporary lockout, and optional login alerts - Memory and SQL stores - Optional encryption-keyring protection for TOTP and OAuth secrets - Automatic API routes, middleware, browser schemas, components, runtime, migrations, and DevToolbar checks Passkeys are a strong sign-in method. They are not currently exposed as a selectable second step in `TwoFactorChallenge`; the implemented MFA methods are email OTP, SMS OTP, TOTP, and recovery codes. ## Install ```bash bun add @wrnexus/auth ``` WRNexusJS discovers the package automatically. Do not copy package components, client scripts, schemas, or standard `/api/auth/*` route files into the application. ## Default configuration Create the engine: ```ts // app/lib/auth.ts import { createAuthEngine, MemoryAuthStore } from "@wrnexus/auth"; export const auth = createAuthEngine({ store: new MemoryAuthStore(), secret: process.env.AUTH_SECRET!, issuer: "My application", onSignedIn(ctx, returnTo) { const safe = returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/account"; return Response.redirect(new URL(safe, ctx.url), 303); }, onSignedOut(ctx) { return Response.redirect(new URL("/sign-in", ctx.url), 303); }, onSuccessfulSignUp() { return { autoSignIn: true, redirectTo: "/account", }; }, delivery: { async send(message) { // Queue email/SMS through your provider. Never log message.code or message.token. }, }, }); ``` Authentication behavior belongs in this engine definition: delivery, token URL mapping, successful sign-in/sign-out responses, password policy, risk thresholds, MFA, passkeys, and auditing can all be configured in one server-only location. The older `config.auth.onSignedIn` and `config.auth.onSignedOut` fields remain supported as compatibility overrides, but new applications should configure these hooks on `createAuthEngine`. ### Successful signup behavior Without `onSuccessfulSignUp`, a successful package registration redirects to `/sign-in`. To sign in immediately after registration: ```ts onSuccessfulSignUp(ctx, user) { return { autoSignIn: true, redirectTo: "/account", }; } ``` Automatic sign-in runs the normal login policy. It does not bypass required email or phone verification, CAPTCHA, MFA, account status, or risk checks. The hook may also return a `Response` for a completely custom HTTP result, or return `{ redirectTo: "/welcome" }` to redirect without creating a session. Register it through application configuration: ```ts // wrnexus.config.ts import type { AuthConfig } from "@wrnexus/auth"; import type { AppConfig } from "@wrnexus/styles"; import { auth } from "./app/lib/auth.ts"; const config = { auth: { engine: auth, routes: true, middleware: true, migrations: false, }, } satisfies AppConfig & { auth: AuthConfig }; export default config; ``` That configuration automatically activates package routes, auth-session middleware, components, browser validation schemas, and the auth client runtime. `setDefaultAuthEngine()` remains available only for advanced manual integrations and tests. ## SQL production configuration ```ts import { createAuthEngine, SqlAuthStore } from "@wrnexus/auth"; import { getDb } from "@wrnexus/db"; export const auth = createAuthEngine({ store: new SqlAuthStore(getDb()), secret: process.env.AUTH_SECRET!, }); ``` ```ts export default { db: { // Application database configuration. }, auth: { engine: auth, routes: true, middleware: true, migrations: true, }, }; ``` The package contributes both ordered migrations: ```text 001_auth.sql 002_auth_otp_purpose.sql ``` Migrations are enabled automatically only when `auth.engine` and a default `config.db` are present. Set `auth.migrations` explicitly when an application needs different behavior. ## Delivered action URLs By default, the engine builds links from the supplied `baseUrl` and token purpose. Applications can map those links to their own page structure without replacing package APIs: ```ts const auth = createAuthEngine({ store: new SqlAuthStore(getDb()), secret: process.env.AUTH_SECRET!, tokenUrl({ purpose, token, baseUrl }) { if (!baseUrl) return undefined; const paths = { "verify-email": `/verify-email?token=${encodeURIComponent(token)}`, "verify-phone": `/verify-phone?token=${encodeURIComponent(token)}`, "password-reset": `/recover/reset?token=${encodeURIComponent(token)}`, "magic-link": `/magic-link?token=${encodeURIComponent(token)}`, invite: `/invitation?token=${encodeURIComponent(token)}`, }; const path = paths[purpose as keyof typeof paths]; return path ? new URL(path, baseUrl).toString() : undefined; }, }); ``` Returning `undefined` intentionally omits the URL while still delivering the raw token. The callback runs only in trusted server code. ## Built-in validation Every packaged auth form has a built-in `@wrnexus/validation` schema. The same resolved schema is used by the browser and the package API handler. Default use requires no `app/schemas` files: ```wrn ``` To customize one schema, extend the package default and register only that override: ```ts // app/schemas/custom-password-request.ts import { authSchemas } from "@wrnexus/auth"; import { v } from "@wrnexus/validation"; export default authSchemas.passwordResetRequest.extend({ identifier: v .string() .trim() .required("Enter your registered email address") .email("Enter a valid registered email address"), }); ``` ```ts import customPasswordRequest from "./app/schemas/custom-password-request.ts"; export default { auth: { engine: auth, schemas: { passwordResetRequest: customPasswordRequest, }, }, }; ``` `` can keep its default `schema="auth-password-request"`. The plugin automatically publishes the overridden browser descriptor under that same built-in schema ID. All other forms continue using package defaults. ## Route controls Use a boolean to enable or disable all package routes: ```ts auth: { engine: auth, routes: true, } ``` Or control feature groups: ```ts routes: { enabled: true, registration: true, login: true, verification: true, password: true, invitations: true, magicLink: true, otp: true, mfa: true, sessions: true, impersonation: false, passkeys: true, } ``` Application routes have normal framework precedence. Disable a package group only when the application intentionally owns every endpoint in that group; no `excludeRoutes` list is required. ## Package endpoints ```text POST /api/auth/register POST /api/auth/login POST /api/auth/logout POST /api/auth/verification/request GET|POST /api/auth/verify/email POST /api/auth/verify/phone POST /api/auth/password/request POST /api/auth/password/reset POST /api/auth/password/change POST /api/auth/invitations/accept POST /api/auth/magic-link/request GET|POST /api/auth/magic-link POST /api/auth/otp/login/request POST /api/auth/otp/login/complete POST /api/auth/otp POST /api/auth/otp/verify POST /api/auth/totp/setup POST /api/auth/totp/confirm POST /api/auth/totp/disable POST /api/auth/recovery-codes POST /api/auth/mfa/otp POST /api/auth/mfa/complete GET /api/auth/sessions POST /api/auth/sessions/revoke POST /api/auth/impersonation/start POST /api/auth/impersonation/stop POST /api/auth/passkeys/register/options POST /api/auth/passkeys/register/verify POST /api/auth/passkeys/login/options POST /api/auth/passkeys/login/verify ``` Each URL uses a route-specific module, so rewritten framework request URLs cannot make the handler fall through to a shared-dispatcher `404`. Unsafe package routes validate the framework CSRF token by default. Set `auth.csrf: false` only when an external API gateway provides an equivalent protection model. ## Components ```wrn ``` `identifier` is optional on verification components. Supply it when an unauthenticated verification page should support resending a token. The response remains generic whether the account exists or not. ## CAPTCHA and risk The HTTP handlers never trust a browser `captchaVerified` field. CAPTCHA completion is accepted only from server-populated `ctx.locals.captcha.success` or `ctx.locals.captchaVerified === true`. Rate limiting remains an application or gateway responsibility. Apply it to registration, login, reset, magic-link, OTP, verification, passkey, invitation, and impersonation endpoints. ## MFA 1. Password, OAuth, magic-link, or OTP login may return `code: "mfa-required"` with a short-lived `mfaToken`. 2. The response lists only methods actually available to that user. 3. Email/SMS MFA is offered only for verified linked identities. 4. `beginMfaOtp()` issues an MFA-bound OTP when needed. 5. `completeMfa()` consumes the one-time transaction and creates the session. ## Passkeys The browser runtime coordinates `navigator.credentials.create()` and `navigator.credentials.get()`. A configured server-side `PasskeyProvider` must verify the challenge, RP ID, origin, signature, user presence or verification, counter, and credential ownership. Multi-process deployments must provide a shared `PasskeyChallengeStore`; the default memory implementation is process-local. Missing passkey providers return a controlled `503` response rather than crashing the route. ## Protect long-lived secrets ```ts import { createAuthSecretProtector } from "@wrnexus/auth"; import { createKeyring } from "@wrnexus/encryption"; const keyring = createKeyring([ { id: "auth-2026-01", secret: process.env.AUTH_ENCRYPTION_KEY!, active: true, }, ]); const auth = createAuthEngine({ store: new SqlAuthStore(getDb()), secret: process.env.AUTH_SECRET!, secretProtector: createAuthSecretProtector(keyring), }); ``` TOTP seeds and OAuth access/refresh tokens are protected before persistence. Keep old keys available during rotation. ## Custom HTTP integration `createAuthHttpHandlers()` remains available for custom route paths or response behavior. Prefer package routes for standard flows; copied application API files are unnecessary. ## Development ```bash bun run auth:dev bun run validate:auth ``` Read [SECURITY.md](./SECURITY.md) before production deployment. ## Package-owned UI blocks and route helpers Authentication forms continue to compose `@wrnexus/ui` inputs, buttons, cards, alerts, badges, avatars, and PIN controls. The package also provides: - `` - `` - `` - complete sign-in, sign-up, MFA, passkey, recovery, account-status, session, and impersonation blocks Server helpers include `authRoute`, `authSuccess`, `authFailure`, `requireAuthUser`, `optionalAuthUser`, `currentAuthSession`, and `authComponentProps`.