# @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
<SignUp />
<SignIn />
<ForgotPassword />
<ResetPassword token='{token}' />
<TwoFactorChallenge />
```

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,
    },
  },
};
```

`<ForgotPassword />` 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
<SignIn />
<SignUp />
<ForgotPassword />
<ResetPassword token='{token}' />
<OtpSignIn method="email-otp" />
<MagicLinkSignIn />
<PasskeyButton mode="authenticate" />
<TwoFactorChallenge mfaToken='{mfaToken}' challengeId='{challengeId}' />
<AuthenticatorSetup credentialId='{credentialId}' secret='{secret}' uri='{uri}' />
<RecoveryCodes codes='{codes}' />
<DeviceSessions sessions='{sessions}' currentSessionId='{currentSessionId}' />
<VerifyEmail token='{token}' identifier='{identifier}' />
<VerifyPhone token='{token}' identifier='{identifier}' />
<InvitationAccept token='{token}' />
<ImpersonationBanner targetName='{targetName}' />
<AccountStatus status='{account.status}' />
```

`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:

- `<AuthShell />`
- `<AuthProviderButtons />`
- `<AuthSecurityNotice />`
- 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`.
