release: WRNexusJS 0.5.0
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
# @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.
|
||||
Reference in New Issue
Block a user