# awesome-lang-auth > awesome-lang-auth is self-hosted authentication for Node.js, Go, AWS Lambda, Python, Rust and Dart, with Angular, React and Flutter clients: JWT, OAuth2, 2FA. This file contains all documentation content of https://awesomelangauth.com/ in a single document following the llmstxt.org standard. Links that start with / are relative to https://awesomelangauth.com. ## Node.js Authentication Quickstart ## Getting Started with awesome-node-auth **node-auth** is a production-ready, database-agnostic JWT authentication and communication bus for Node.js written in TypeScript. It establishes a 360-degree communication and access control layer compatible with any Node.js framework (NestJS, Next.js, Express, Fastify, etc.) and any database through a simple interface pattern. :::note Before you start Make sure you have **Node.js 18+** and a package manager (npm / yarn / pnpm) installed. ::: ### Pick a recipe Choose the authentication strategy that fits your application. You can combine multiple recipes. | Recipe | Description | |--------|-------------| | [Email / Password](/docs/authentication/local) | Classic login with bcrypt hashing | | [OAuth / Social](/docs/authentication/oauth) | Google, GitHub, custom providers | | [Magic Link](/docs/authentication/magic-link) | Passwordless email login | | [SMS OTP](/docs/authentication/sms) | Phone number verification | | [TOTP 2FA](/docs/authentication/totp) | Google Authenticator-compatible | | [API Keys](/docs/advanced/api-keys) | Machine-to-machine service tokens | | [Multi-Tenancy](/docs/advanced/multi-tenancy) | Isolated tenant support | | [Auth Tools & SSE](/docs/advanced/auth-tools) | Event bus, SSE streams, webhooks, telemetry | | [Dynamic Email Templates & i18n](/docs/advanced/mailer#dynamic-templates-with-itemplatestore) | Custom email templates with per-language translations via `ITemplateStore` | | [UI Internationalization (i18n)](/docs/advanced/built-in-ui#internationalization-i18n) | Translate built-in UI pages via `data-i18n` + `ITemplateStore` | --- ### **Step 1**: Install ```bash npm install @awesome-lang-auth/node ``` --- ### **Step 2**: Implement IUserStore node-auth is database-agnostic. You connect it to your database by implementing the `IUserStore` interface: ```typescript import { IUserStore, BaseUser } from '@awesome-lang-auth/node'; export class MyUserStore implements IUserStore { async findByEmail(email: string): Promise { return db.users.findOne({ email }); } async findById(id: string): Promise { return db.users.findOne({ id }); } async create(data: Partial): Promise { return db.users.insert(data); } async updateRefreshToken(userId: string, token: string | null, expiry: Date | null): Promise { await db.users.update({ id: userId }, { refreshToken: token, refreshTokenExpiry: expiry }); } async updateLastLogin(userId: string): Promise { await db.users.update({ id: userId }, { lastLogin: new Date() }); } async updatePassword(userId: string, hashedPassword: string): Promise { await db.users.update({ id: userId }, { password: hashedPassword }); } async updateResetToken(userId: string, token: string | null, expiry: Date | null): Promise { await db.users.update({ id: userId }, { resetToken: token, resetTokenExpiry: expiry }); } // ... implement other methods (2FA, Magic Link, SMS, etc.) as needed } ``` :::tip See [Database Integration](/docs/database) for ready-to-use implementations for SQLite, PostgreSQL, MySQL, and MongoDB. ::: --- ### **Step 3**: Configure AuthConfigurator **Express** ```typescript import express from 'express'; import { AuthConfigurator } from '@awesome-lang-auth/node'; import { MyUserStore } from './my-user-store'; const app = express(); app.use(express.json()); const auth = new AuthConfigurator( { accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!, refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!, accessTokenExpiresIn: '15m', refreshTokenExpiresIn: '7d', }, new MyUserStore() ); ``` **NestJS** ```typescript // auth.module.ts import { Module } from '@nestjs/common'; import { AuthConfigurator } from '@awesome-lang-auth/node'; import { MyUserStore } from './my-user-store'; const auth = new AuthConfigurator( { accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!, refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!, accessTokenExpiresIn: '15m', refreshTokenExpiresIn: '7d', }, new MyUserStore() ); @Module({ providers: [{ provide: 'AUTH', useValue: auth }], exports: ['AUTH'], }) export class AuthModule {} ``` **Next.js** ```typescript // lib/auth.ts import { AuthConfigurator } from '@awesome-lang-auth/node'; import { MyUserStore } from './my-user-store'; export const auth = new AuthConfigurator( { accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!, refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!, accessTokenExpiresIn: '15m', refreshTokenExpiresIn: '7d', }, new MyUserStore() ); ``` --- ### **Step 4**: Mount the router **Express** ```typescript // Mount all auth endpoints under /auth app.use('/auth', auth.router()); // Protect routes app.get('/protected', auth.middleware(), (req, res) => { res.json({ user: req.user }); }); app.listen(3000); ``` **NestJS** ```typescript // main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { auth } from './auth'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use('/auth', auth.router()); await app.listen(3000); } bootstrap(); ``` **Next.js App Router** ```typescript // See the Next.js integration guide for the full catch-all route handler // that bridges the App Router request/response to the Express-compatible router. // https://your-wiki/docs/frameworks/nextjs import { getAuth } from '@/lib/auth'; // Use auth.router() inside a catch-all API route handler. // The auth singleton is created in lib/auth.ts: // export const auth = new AuthConfigurator(config, userStore); ``` --- ### Next Steps - [Installation & Configuration](/docs/installation) – Full config options, CSRF, 2FA - [Authentication Strategies](/docs/authentication) – All available recipes - [Database Integration](/docs/database) – IUserStore implementations - [API Reference](/docs/api-reference) – All endpoints - [Use the docs with AI assistants](/docs/ai-assistants) – An index of the docs (llms.txt) and every page in one file (llms-full.txt) --- ## Live Node.js Authentication Demos on StackBlitz ## Live Demos Fully-functional, in-browser demos powered by [StackBlitz WebContainers](https://stackblitz.com). Each example uses an **in-memory store** so everything works out-of-the-box — no database setup required. Fill in the `.env.example` values to connect a real database or enable email/SMS features. --- ### Express (Basic) The simplest integration: Express + `awesome-node-auth` with HttpOnly JWT cookies, bcrypt password hashing, and an admin panel. | | | |---|---| | **Stack** | Node.js · Express · Vanilla HTML/JS | | **Auth** | Register · Login · Logout · `/auth/me` · Refresh token | | **Database** | In-memory | | **Start** | `npm start` | [![Open in StackBlitz](/img/open_in_stackblitz.svg)](https://stackblitz.com/github/awesome-lang-auth/awesome-node-auth/tree/main/demo/express-vanilla?title=awesome-node-auth%20Express%20Demo&startScript=start) --- ### NestJS Fullstack NestJS application with `AuthModule.forRoot()`, `JwtAuthGuard`, `@CurrentUser()` decorator, and a static HTML frontend. | | | |---|---| | **Stack** | Node.js · NestJS · TypeScript · ts-node · Vanilla HTML/JS | | **Auth** | Register · Login · Logout · `/auth/me` · Refresh · Protected endpoint | | **Database** | In-memory | | **Start** | `npm start` | [![Open in StackBlitz](/img/open_in_stackblitz.svg)](https://stackblitz.com/github/awesome-lang-auth/awesome-node-auth/tree/main/demo/nestjs-fullstack?title=awesome-node-auth%20NestJS%20Demo&startScript=start) --- ### Next.js Fullstack (Pages Router) Full-stack Next.js application with a catch-all `/api/auth/[...auth]` route, Edge Middleware route protection, and a login + dashboard UI. | | | |---|---| | **Stack** | Node.js · Next.js 15 · TypeScript · React | | **Auth** | Register · Login · Logout · Dashboard (protected) · Middleware redirect | | **Database** | In-memory | | **Start** | `npm start` (runs `next dev`) | [![Open in StackBlitz](/img/open_in_stackblitz.svg)](https://stackblitz.com/github/awesome-lang-auth/awesome-node-auth/tree/main/demo/nextjs-fullstack?title=awesome-node-auth%20Next.js%20Demo&startScript=start) --- ### Angular SSR (with Guards & Interceptors) Angular 19 application with Server-Side Rendering, a full auth layer, and all production-ready patterns: - **`AuthService`** — user state, login/logout/refresh via HttpOnly cookies - **`authInterceptor`** — CSRF header + automatic token-refresh queue (handles concurrent 401s) - **`authGuard`, `guestGuard`, `roleGuard`** — route protection - **`APP_INITIALIZER`** — silent session restore on page reload - **`ssrCookieInterceptor`** — forwards browser cookies to the API server during SSR | | | |---|---| | **Stack** | Node.js · Angular 19 SSR · Express (node-auth API) · TypeScript | | **Dev mode** | `npm start` → API server (port 3000) + `ng serve` (port 4200) with proxy | | **Production** | `npm run build && npm run serve:ssr` | | **Database** | In-memory | [![Open in StackBlitz](/img/open_in_stackblitz.svg)](https://stackblitz.com/github/awesome-lang-auth/awesome-node-auth/tree/main/demo/angular-ssr?title=awesome-node-auth%20Angular%20SSR%20Demo&startScript=start) --- ### Express + Angular SPA Separate Express API (node-auth) on port 3000 and Angular SPA on port 4200. Angular proxies `/auth/*` and `/admin/*` to Express during development. Same auth patterns as the Angular SSR demo (guards, interceptors, `APP_INITIALIZER`) — without SSR complexity. In production, Express serves the compiled Angular build. | | | |---|---| | **Stack** | Node.js · Angular 19 · Express (node-auth API) · TypeScript | | **Dev mode** | `npm start` → API server (port 3000) + `ng serve` (port 4200) with proxy | | **Production** | `npm run build && npm run serve:prod` | | **Database** | In-memory | [![Open in StackBlitz](/img/open_in_stackblitz.svg)](https://stackblitz.com/github/awesome-lang-auth/awesome-node-auth/tree/main/demo/express-angular-spa?title=awesome-node-auth%20Express%20%2B%20Angular%20SPA&startScript=start) --- ### Using your own database All demos use `InMemoryUserStore` by default. To connect a real database, replace it with one of the ready-made stores: | Database | Guide | |----------|-------| | SQLite | [docs/database/sqlite](/docs/database/sqlite) | | MySQL | [docs/database/mysql](/docs/database/mysql) | | PostgreSQL | [docs/database/postgresql](/docs/database/postgresql) | | MongoDB | [docs/database/mongodb](/docs/database/mongodb) | Copy the `.env.example` → `.env` (or `.env.local` for Next.js) and set `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, and your database connection string. :::tip In-memory data resets on restart The in-memory store is ephemeral — all registered users are lost when the server restarts. This is intentional for the demos. In production, use a persistent database. ::: ### Related - [Runnable Express authentication demo](/demo) — the same demo as copy-paste code to run locally - [Node.js authentication quickstart](/docs/intro) — go from the demo to your own app - [Express authentication](/docs/frameworks/express) — how the demo server is wired - [Database-agnostic auth with IUserStore](/docs/database) — swap the in-memory store for a real database --- ## Install awesome-node-auth in a Node.js App ## Installation & Configuration ### **Step 1**: Install the package **npm** ```bash npm install @awesome-lang-auth/node ``` **yarn** ```bash yarn add @awesome-lang-auth/node ``` **pnpm** ```bash pnpm add @awesome-lang-auth/node ``` :::note Package name The library is published on npm as `@awesome-lang-auth/node`, formerly published as `awesome-node-auth`. Imports use the same name as the package: `import { AuthConfigurator } from '@awesome-lang-auth/node'`. ::: :::info TypeScript node-auth is written in TypeScript and ships with its own type declarations — no `@types/` package needed. ::: --- ### **Step 2**: Configure AuthConfigurator Create an `AuthConfig` object and pass it to `AuthConfigurator` along with your `IUserStore` implementation: ```typescript import { AuthConfigurator, AuthConfig } from '@awesome-lang-auth/node'; import { MyUserStore } from './my-user-store'; const config: AuthConfig = { accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!, refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!, accessTokenExpiresIn: '15m', refreshTokenExpiresIn: '7d', }; const auth = new AuthConfigurator(config, new MyUserStore()); ``` #### AuthConfig Options | Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | `accessTokenSecret` | `string` | ✅ | — | Secret for signing access tokens | | `refreshTokenSecret` | `string` | ✅ | — | Secret for signing refresh tokens | | `accessTokenExpiresIn` | `string` | ❌ | `'15m'` | Access token TTL (e.g. `'15m'`) | | `refreshTokenExpiresIn` | `string` | ❌ | `'7d'` | Refresh token TTL (e.g. `'7d'`) | | `buildTokenPayload` | `(user) => object` | ❌ | — | Inject custom claims into JWT | | `csrf.enabled` | `boolean` | ❌ | `false` | Enable CSRF double-submit cookie | | `emailVerificationMode`| `'none'\|'lazy'\|'strict'` | ❌ | `'none'` | Email verification enforcement mode | | `requireEmailVerification`| `boolean` | ❌ | `false` | (Deprecated) Use `emailVerificationMode` instead | --- ### **Step 3**: Mount the router ```typescript import express from 'express'; import rateLimit from 'express-rate-limit'; const app = express(); app.use(express.json()); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 }); app.use('/auth', auth.router({ rateLimiter: limiter, onRegister: async (data, config) => { return userStore.create(data); }, })); app.listen(3000); ``` --- ### Complete Configuration Example ```typescript import express from 'express'; import rateLimit from 'express-rate-limit'; import { AuthConfigurator, AuthConfig } from '@awesome-lang-auth/node'; import { MyUserStore } from './stores/user-store'; import { MySessionStore } from './stores/session-store'; import { MyRbacStore } from './stores/rbac-store'; const app = express(); app.use(express.json()); const config: AuthConfig = { accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!, refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!, accessTokenExpiresIn: '15m', refreshTokenExpiresIn: '7d', emailVerificationMode: 'lazy', csrf: { enabled: true }, buildTokenPayload: async (user) => ({ tenantId: user.tenantId, plan: user.plan, }), }; const userStore = new MyUserStore(); const sessionStore = new MySessionStore(); const rbacStore = new MyRbacStore(); const auth = new AuthConfigurator(config, userStore); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 }); app.use('/auth', auth.router({ rateLimiter: limiter, sessionStore, rbacStore, onRegister: async (data, config) => { return userStore.create(data); }, })); app.listen(3000); ``` ### Related - [Node.js authentication quickstart](/docs/intro) — the shortest path from install to a working login - [Authentication methods for Node.js](/docs/authentication) — pick email/password, OAuth2, magic link, TOTP or SMS - [Database-agnostic auth with IUserStore](/docs/database) — wire the library to your own database - [Framework integrations](/docs/frameworks) — Express, NestJS, Next.js and any other Node HTTP server - [Auth API endpoints reference](/docs/api-reference/endpoints) — every route the auth router mounts --- ## Authentication Methods for Node.js ## Authentication Strategies node-auth supports multiple authentication strategies that can be combined: | Strategy | Description | |----------|-------------| | [Local](/docs/authentication/local) | Email/password with bcrypt hashing | | [OAuth 2.0](/docs/authentication/oauth) | Google, GitHub, or any custom provider | | [Magic Link](/docs/authentication/magic-link) | Passwordless email authentication | | [TOTP 2FA](/docs/authentication/totp) | Time-based one-time passwords | | [SMS OTP](/docs/authentication/sms) | Phone number verification codes | All strategies are plug-in — only configure what you need. --- ## Email and Password Authentication in Node.js ## Email / Password Recipe The local strategy provides classic email and password authentication with bcrypt hashing and a full password reset flow. :::caution Password strength node-auth does not enforce password complexity by default. Validate password strength in your `onRegister` callback before creating the user. ::: --- ### Register → Login → Refresh → Logout flow ```mermaid sequenceDiagram autonumber participant C as Client participant S as awesome-node-auth Router participant DB as IUserStore Note over C,DB: Registration C->>S: POST /auth/register {email, password} S->>DB: onRegister(data) DB-->>S: user {id, email, …} S-->>C: 201 {success: true, userId} Note over C,DB: Login C->>S: POST /auth/login {email, password} S->>DB: findByEmail(email) DB-->>S: user S->>S: bcrypt.compare(password, hash) S-->>C: 200 + Set-Cookie: accessToken, refreshToken, csrf-token Note over C,DB: Token refresh C->>S: POST /auth/refresh (refreshToken cookie) S->>DB: find user by refreshToken DB-->>S: user S->>S: sign new token pair S-->>C: 200 + Set-Cookie: new accessToken, new refreshToken, new csrf-token Note over C,DB: Logout C->>S: POST /auth/logout (accessToken cookie) S->>DB: updateRefreshToken(userId, null, null) S-->>C: 200 {success: true} + cleared cookies ``` --- ### Password Reset flow ```mermaid sequenceDiagram autonumber participant C as Client participant S as awesome-node-auth Router participant DB as IUserStore participant E as Email Provider C->>S: POST /auth/forgot-password {email} S->>DB: findByEmail(email) DB-->>S: user S->>S: generate reset token (32 random bytes) S->>DB: updateResetToken(userId, token, expiry+1h) S->>E: sendPasswordReset(email, token, link) S-->>C: 200 {success: true} Note over C,E: User clicks link in email C->>S: POST /auth/reset-password {token, newPassword} S->>DB: findByResetToken(token) DB-->>S: user (validates expiry) S->>S: bcrypt.hash(newPassword) S->>DB: updatePassword(userId, hash) S->>DB: updateResetToken(userId, null, null) S-->>C: 200 {success: true} ``` --- ### **Step 1**: Enable LocalStrategy The local strategy is active by default — no extra configuration needed. Just create your `AuthConfigurator`: ```typescript import { AuthConfigurator } from '@awesome-lang-auth/node'; const auth = new AuthConfigurator(config, userStore); ``` --- ### **Step 2**: Implement IUserStore Ensure your `IUserStore` implements the methods required for local auth: ```typescript export class MyUserStore implements IUserStore { async findByEmail(email: string) { /* … */ } async findById(id: string) { /* … */ } async create(data: Partial) { /* … */ } async updateRefreshToken(userId: string, token: string | null, expiry: Date | null) { /* … */ } async updatePassword(userId: string, hashedPassword: string) { /* … */ } async updateResetToken(userId: string, token: string | null, expiry: Date | null) { /* … */ } async findByResetToken(token: string) { /* … */ } } ``` --- ### **Step 3**: Mount the router **Express** ```typescript app.use('/auth', auth.router({ onRegister: async (data, config) => { // Validate, then persist return userStore.create(data); }, })); ``` **NestJS** ```typescript // In AuthController constructor — see NestJS guide this.router = auth.router({ onRegister: async (data) => userStore.create(data), }); ``` **Next.js** ```typescript // pages/api/auth/[...auth].ts export default function handler(req, res) { const router = getAuth().router({ onRegister: async (data) => userStore.create(data), }); req.url = req.url!.replace(/^\/api\/auth/, '') || '/'; router(req as any, res as any, () => res.status(404).end()); } ``` --- ### **Step 4**: Test the endpoints ```bash # Register curl -X POST http://localhost:3000/auth/register \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"secret123","firstName":"Jane"}' # Login curl -X POST http://localhost:3000/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"secret123"}' ``` --- ### Endpoint Reference | Method | Path | Auth | Description | |--------|------|:---:|-------------| | `POST` | `/auth/login` | — | Login with email + password | | `POST` | `/auth/register` | — | Register new user (requires `onRegister`) | | `POST` | `/auth/logout` | ✅ | Invalidate refresh token + clear cookies | | `POST` | `/auth/refresh` | — | Exchange refresh token for new access token | | `GET` | `/auth/me` | ✅ | Get current user profile | | `POST` | `/auth/forgot-password` | — | Send password reset email | | `POST` | `/auth/reset-password` | — | Reset password with token | | `POST` | `/auth/change-password` | ✅ | Change password for authenticated user | ### Related - [Passwordless magic link login](/docs/authentication/magic-link) — the same flow without a password - [Email verification modes](/docs/advanced/email-verification) — decide how strictly unverified users are blocked - [Session management and token revocation](/docs/advanced/sessions) — invalidate a login before the token expires - [Database-agnostic auth with IUserStore](/docs/database) — where the password hash is actually stored --- ## OAuth2 Social Login in Node.js (Google, GitHub) ## OAuth / Social Recipe node-auth supports Google, GitHub, and any custom OAuth 2.0 provider via `GenericOAuthStrategy`. --- ### OAuth flow ```mermaid sequenceDiagram autonumber participant U as User participant B as Browser participant S as awesome-node-auth Router participant P as OAuth Provider (Google/GitHub) participant DB as IUserStore U->>B: Click "Sign in with Google" B->>S: GET /auth/oauth/google S->>S: build authorization URL + state param S-->>B: 302 redirect to Google OAuth B->>P: Authorization request (scope: email, profile) P-->>U: Consent screen U->>P: Approve P-->>B: 302 redirect to callback URL with code + state B->>S: GET /auth/oauth/google/callback?code=…&state=… S->>P: Exchange code for access_token P-->>S: access_token + profile S->>DB: findOrCreateUser(profile) DB-->>S: user S->>S: sign token pair S-->>B: 302 redirect to app + Set-Cookie: accessToken, refreshToken, csrf-token ``` :::info Account conflicts If the OAuth provider email matches an existing local account, awesome-node-auth returns a `409 Conflict`. Use `IPendingLinkStore` to resolve conflicts automatically. See [Account Linking](/docs/advanced/account-linking). ::: --- ### **Step 1**: Configure your OAuth app Register your application in the provider's developer console and note the **Client ID** and **Client Secret**. Set the callback URL to: ``` https://yourapp.com/auth/oauth//callback ``` --- ### **Step 2**: Create a strategy **Google** ```typescript import { GoogleStrategy, AuthConfig } from '@awesome-lang-auth/node'; class MyGoogleStrategy extends GoogleStrategy { constructor(config: AuthConfig) { super(config); } async findOrCreateUser(profile: any) { // Always use providerAccountId + provider for lookup — never email alone // (prevents account-takeover via email spoofing) let user = await userStore.findByProviderAccount?.('google', profile.id); if (!user) { user = await userStore.create({ email: profile.email, firstName: profile.name, providerAccountId: profile.id, loginProvider: 'google', isEmailVerified: true, }); } return user; } } const googleStrategy = new MyGoogleStrategy(config); ``` **GitHub** ```typescript import { GithubStrategy, AuthConfig } from '@awesome-lang-auth/node'; class MyGithubStrategy extends GithubStrategy { constructor(config: AuthConfig) { super(config); } async findOrCreateUser(profile: any) { let user = await userStore.findByProviderAccount?.('github', profile.id); if (!user) { user = await userStore.create({ email: profile.email, firstName: profile.name, providerAccountId: profile.id, loginProvider: 'github', isEmailVerified: true, }); } return user; } } const githubStrategy = new MyGithubStrategy(config); ``` **Custom Provider** ```typescript import { GenericOAuthStrategy } from '@awesome-lang-auth/node'; const discordStrategy = new GenericOAuthStrategy({ name: 'discord', clientId: process.env.DISCORD_CLIENT_ID!, clientSecret: process.env.DISCORD_CLIENT_SECRET!, authorizationUrl: 'https://discord.com/api/oauth2/authorize', tokenUrl: 'https://discord.com/api/oauth2/token', userInfoUrl: 'https://discord.com/api/users/@me', callbackUrl: 'https://yourapp.com/auth/oauth/discord/callback', scope: ['identify', 'email'], mapProfile: (raw) => ({ id: raw.id, email: raw.email, name: raw.username, }), findOrCreateUser: async (profile) => { return userStore.findOrCreate({ ...profile, loginProvider: 'discord' }); }, }); ``` --- ### **Step 3**: Mount the router ```typescript app.use('/auth', auth.router({ googleStrategy, githubStrategy, // or for custom providers: oauthStrategies: [discordStrategy], })); ``` --- ### **Step 4**: Implement `findByProviderAccount` (recommended) Add this to your `IUserStore` to enable safe provider-based lookup (avoids email-based account takeover): ```typescript async findByProviderAccount(provider: string, providerAccountId: string): Promise { return db('users').where({ loginProvider: provider, providerAccountId }).first() ?? null; } ``` --- ### OAuth Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/auth/oauth/google` | Redirect to Google OAuth | | `GET` | `/auth/oauth/google/callback` | Google OAuth callback | | `GET` | `/auth/oauth/github` | Redirect to GitHub OAuth | | `GET` | `/auth/oauth/github/callback` | GitHub OAuth callback | | `GET` | `/auth/oauth/:name` | Redirect to custom provider | | `GET` | `/auth/oauth/:name/callback` | Custom provider callback | --- ## Passwordless Magic Link Login in Node.js ## Magic Link Recipe Magic links provide passwordless authentication via email. No password required — users click a link to log in. :::tip Email verification bonus The first magic-link login automatically counts as email verification. No separate verification step needed. ::: --- ### Magic Link flow ```mermaid sequenceDiagram autonumber participant C as Client participant S as awesome-node-auth Router participant DB as IUserStore participant E as Email Provider C->>S: POST /auth/magic-link/send {email} S->>DB: findByEmail(email) DB-->>S: user S->>S: generate secure token (32 bytes) S->>DB: updateMagicLinkToken(userId, token, expiry+15m) S->>E: sendMagicLink(email, token, link) S-->>C: 200 {success: true} Note over C,E: User clicks link in email, app extracts token C->>S: POST /auth/magic-link/verify {token} S->>DB: findByMagicLinkToken(token) DB-->>S: user (validates expiry) S->>DB: updateMagicLinkToken(userId, null, null) S->>DB: updateEmailVerified(userId, true) S->>S: sign token pair S-->>C: 200 + Set-Cookie: accessToken, refreshToken, csrf-token ``` :::info Bearer mode For native/mobile clients, add `X-Auth-Strategy: bearer` to `/auth/magic-link/verify` to receive tokens in the response body instead of cookies. ::: --- ### Login mode and 2FA mode Both endpoints take an optional `mode` in the request body: | `mode` | `POST /auth/magic-link/send` body | `POST /auth/magic-link/verify` body | What the link does | |--------|-----------------------------------|-------------------------------------|--------------------| | `'login'` (default) | `{ email, emailLang? }` | `{ token }` | Passwordless login | | `'2fa'` | `{ mode: '2fa', tempToken, emailLang? }` | `{ mode: '2fa', token, tempToken }` | Second factor after the password | :::note Passwordless login and 2FA In `mode: 'login'`, `POST /auth/magic-link/verify` issues a session directly and does not ask for the second factor, even for accounts with TOTP enabled. The second-factor flow is `mode: '2fa'`, after the password: `POST /auth/login` answers `{ requiresTwoFactor: true, tempToken, available2faMethods }`, `POST /auth/magic-link/send` with `{ mode: '2fa', tempToken }` emails a link to that user, and `POST /auth/magic-link/verify` with `{ mode: '2fa', token, tempToken }` issues the session. ::: --- ### **Step 1**: Configure AuthConfig Magic links are enabled by providing a `sendMagicLink` callback (or a `mailer` configuration) in your `AuthConfig`: ```typescript const config: AuthConfig = { // ... secrets email: { // Option A: Use a custom callback sendMagicLink: async (email, token, link, lang) => { await mailer.send({ to: email, subject: 'Your login link', html: `Click to log in`, }); }, // Option B: Use the built-in HTTP mailer (see Mailer guide) // mailer: { endpoint: '...', apiKey: '...' } } }; ``` --- ### **Step 2**: Mount the router The magic-link endpoints are active as long as the email configuration is present. No additional strategy needs to be passed to the router: **Express** ```typescript app.use('/auth', auth.router()); ``` **NestJS** ```typescript this.router = auth.router(); ``` --- ### **Step 3**: Implement IUserStore methods Magic link requires these store methods: ```typescript async updateMagicLinkToken(userId: string, token: string | null, expiry: Date | null): Promise async findByMagicLinkToken(token: string): Promise ``` --- ### **Step 4**: Test the flow ```bash # Step 1: Request magic link curl -X POST http://localhost:3000/auth/magic-link/send \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com"}' # Step 2: Verify (token from email) curl -X POST http://localhost:3000/auth/magic-link/verify \ -H 'Content-Type: application/json' \ -d '{"token":""}' # Cookie mode: { "success": true } + HttpOnly cookies # Bearer mode: add -H 'X-Auth-Strategy: bearer' → { "success": true, "accessToken": "…", "refreshToken": "…" } ``` --- ### Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/auth/magic-link/send` | Generate token + send magic-link email. Body: `{ email }` for login; `{ mode: '2fa', tempToken }` for the second factor | | `POST` | `/auth/magic-link/verify` | Verify token → issue session tokens. Body: `{ token }` for login; `{ mode: '2fa', token, tempToken }` for the second factor | ### Related - [Email and password authentication](/docs/authentication/local) — the classic alternative to magic links - [Built-in HTTP mailer](/docs/advanced/mailer) — how the magic-link email is delivered - [Email verification modes](/docs/advanced/email-verification) — why the first magic-link click verifies the address - [TOTP two-factor authentication](/docs/authentication/totp) — the authenticator-app second factor after a password login --- ## TOTP Two-Factor Authentication in Node.js ## TOTP 2FA Recipe Time-based One-Time Passwords (TOTP) provide Google Authenticator-compatible two-factor authentication. TOTP is built into the auth router — no extra strategy configuration is needed. :::caution Backup codes TOTP does not provide built-in backup codes. Consider implementing your own backup code mechanism so users can recover access if they lose their authenticator app. ::: --- ### TOTP enrollment flow ```mermaid sequenceDiagram autonumber participant C as Client (authenticated) participant S as awesome-node-auth Router participant DB as IUserStore participant App as Authenticator App C->>S: POST /auth/2fa/setup (accessToken cookie) S->>S: otplib.authenticator.generateSecret() S->>S: build otpauthUrl + QR code data URI S-->>C: 200 {secret, otpauthUrl, qrCode} C->>App: User scans QR code C->>S: POST /auth/2fa/verify-setup {token: "123456", secret} S->>S: otplib.authenticator.check(token, secret) S->>DB: updateTotpSecret(userId, secret) S-->>C: 200 {success: true} ``` --- ### 2FA login flow ```mermaid sequenceDiagram autonumber participant C as Client participant S as awesome-node-auth Router participant DB as IUserStore C->>S: POST /auth/login {email, password} S->>DB: findByEmail(email) DB-->>S: user (isTotpEnabled: true) S->>S: bcrypt.compare(password, hash) ✓ S->>S: sign short-lived tempToken S-->>C: 200 {requiresTwoFactor: true, tempToken, available2faMethods: ["totp"]} Note over C,S: User opens authenticator app for 6-digit code C->>S: POST /auth/2fa/verify {tempToken, totpCode: "123456"} S->>S: verify tempToken signature S->>DB: findById(sub from tempToken) DB-->>S: user S->>S: otplib.authenticator.check(totpCode, user.totpSecret) S->>S: sign full token pair S-->>C: 200 + Set-Cookie: accessToken, refreshToken, csrf-token ``` --- ### **Step 1**: Mount the router TOTP endpoints are always available. Just mount the standard auth router: ```typescript app.use('/auth', auth.router()); ``` --- ### **Step 2**: User enrolls TOTP #### Generate secret and QR code ```typescript // POST /auth/2fa/setup (authenticated) const { secret, otpauthUrl, qrCode } = await fetch('/auth/2fa/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', }).then(r => r.json()); // Display qrCode (a data: URI) in an tag // or show `secret` for manual entry in the authenticator app ``` #### Confirm activation ```typescript // POST /auth/2fa/verify-setup (authenticated) await fetch('/auth/2fa/verify-setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ token: '123456', secret }), }); ``` --- ### **Step 3**: Login with 2FA ```typescript // Step 1: Initial login → 2FA challenge const loginRes = await fetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }), }).then(r => r.json()); if (loginRes.requiresTwoFactor) { const { tempToken, available2faMethods } = loginRes; // Show the TOTP input to the user } // Step 2: Submit TOTP code → full session await fetch('/auth/2fa/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ tempToken, totpCode: '123456' }), }); ``` :::info Bearer mode For native/mobile clients add `X-Auth-Strategy: bearer` to `/auth/2fa/verify` to receive tokens in the JSON body. ::: --- ### TOTP Endpoints | Method | Path | Auth | Description | |--------|------|:----:|-------------| | `POST` | `/auth/2fa/setup` | ✅ | Generate TOTP secret + QR code data URI | | `POST` | `/auth/2fa/verify-setup` | ✅ | Confirm enrollment with a valid TOTP code | | `POST` | `/auth/2fa/verify` | — (temp token) | Complete 2FA login | | `POST` | `/auth/2fa/disable` | ✅ | Disable TOTP for the user | ### Related - [SMS OTP verification and 2FA](/docs/authentication/sms) — the SMS-based second factor - [Email and password authentication](/docs/authentication/local) — the first factor TOTP protects - [Session management and token revocation](/docs/advanced/sessions) — revoke sessions when a device is lost - [Bearer token mode for mobile apps](/docs/advanced/bearer-token) — 2FA from native clients --- ## SMS OTP Verification and 2FA in Node.js ## SMS OTP Recipe Send one-time passwords via SMS for phone number verification or two-factor authentication. --- ### SMS OTP flow ```mermaid sequenceDiagram autonumber participant C as Client participant S as awesome-node-auth Router participant DB as IUserStore participant SMS as SMS Provider C->>S: POST /auth/sms/send {userId} S->>DB: findById(userId) DB-->>S: user (must have phoneNumber) S->>S: generate 6-digit OTP code S->>DB: updateSmsCode(userId, code, expiry+10m) S->>SMS: sendSms(phoneNumber, "Your code: 123456") S-->>C: 200 {success: true} Note over C,SMS: User reads OTP from SMS C->>S: POST /auth/sms/verify {userId, code} S->>DB: findById(userId) DB-->>S: user S->>S: compare code + check expiry S->>DB: updateSmsCode(userId, null, null) S->>S: sign token pair S-->>C: 200 + Set-Cookie: accessToken, refreshToken, csrf-token ``` :::info Bearer mode For native/mobile clients add `X-Auth-Strategy: bearer` to `/auth/sms/verify` to receive tokens in the JSON body. ::: --- ### Login mode and 2FA mode Both endpoints take an optional `mode` in the request body: | `mode` | `POST /auth/sms/send` body | `POST /auth/sms/verify` body | What the code does | |--------|----------------------------|------------------------------|--------------------| | `'login'` (default) | `{ userId }` or `{ email }` | `{ userId, code }` | Login with the SMS code alone | | `'2fa'` | `{ mode: '2fa', tempToken }` | `{ mode: '2fa', tempToken, code }` | Second factor after the password | :::note SMS login and 2FA In `mode: 'login'`, `POST /auth/sms/verify` issues a session directly and does not ask for the second factor, even for accounts with TOTP enabled. The second-factor flow is `mode: '2fa'`, after the password: `POST /auth/login` answers `{ requiresTwoFactor: true, tempToken, available2faMethods }`, `POST /auth/sms/send` with `{ mode: '2fa', tempToken }` texts a code to that user's `phoneNumber`, and `POST /auth/sms/verify` with `{ mode: '2fa', tempToken, code }` issues the session. ::: --- ### **Step 1**: Configure AuthConfig SMS OTP is enabled by configuring the built-in HTTP SMS gateway in your `AuthConfig.sms` block: ```typescript const config: AuthConfig = { // ... secrets sms: { endpoint: 'https://sms.yourprovider.com/send', apiKey: process.env.SMS_API_KEY!, username: process.env.SMS_USERNAME!, password: process.env.SMS_PASSWORD!, codeExpiresInMinutes: 10, // default: 10 }, }; ``` The built-in `SmsService` makes a `GET` request to `{endpoint}?username=…&password=…&phone=…&message=…` with an `X-API-Key` header. See [SmsService](#smsservice) below. --- ### **Step 2**: Mount the router SMS endpoints are active as long as the SMS configuration is present in `AuthConfig`. No strategy needs to be passed to the router: ```typescript app.use('/auth', auth.router()); ``` --- ### **Step 3**: Implement IUserStore methods SMS OTP requires: ```typescript async updateSmsCode(userId: string, code: string | null, expiry: Date | null): Promise ``` The user record must also have `phoneNumber` set. --- ### **Step 4**: Test the flow ```bash # Send OTP to user's phone curl -X POST http://localhost:3000/auth/sms/send \ -H 'Content-Type: application/json' \ -d '{"userId":"123"}' # Verify OTP code curl -X POST http://localhost:3000/auth/sms/verify \ -H 'Content-Type: application/json' \ -d '{"userId":"123","code":"123456"}' ``` --- ### SmsService The `SmsService` is exported and can be used directly in your own code: ```typescript import { SmsService } from '@awesome-lang-auth/node'; const smsService = new SmsService({ endpoint: 'https://sms.yourprovider.com/send', apiKey: process.env.SMS_API_KEY!, username: process.env.SMS_USERNAME!, password: process.env.SMS_PASSWORD!, codeExpiresInMinutes: 10, }); // Send a custom SMS await smsService.sendSms('+39123456789', 'Your verification code: 847261'); // Generate a random OTP code const code = smsService.generateCode(6); // '847261' ``` The service makes a `GET` request to `{endpoint}?username={u}&password={p}&phone={phone}&message={msg}` with an `X-API-Key` header. --- ### SMS Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/auth/sms/send` | Generate OTP + send to user's `phoneNumber`. Body: `{ userId }` or `{ email }` for login; `{ mode: '2fa', tempToken }` for the second factor | | `POST` | `/auth/sms/verify` | Verify OTP → issue session tokens. Body: `{ userId, code }` for login; `{ mode: '2fa', tempToken, code }` for the second factor | ### Related - [TOTP two-factor authentication](/docs/authentication/totp) — an app-based second factor with no SMS costs - [Email and password authentication](/docs/authentication/local) — the first factor SMS OTP protects - [Bearer token mode for mobile apps](/docs/advanced/bearer-token) — phone verification from native clients - [Flutter authentication client](/docs/frameworks/flutter) — SMS flows in a mobile app --- ## Database-Agnostic Auth: The IUserStore Interface ## Database Integration node-auth is completely **database-agnostic**. Implement the `IUserStore` interface for your database and pass the instance to `AuthConfigurator`. :::info Any database As long as you implement `IUserStore`, awesome-node-auth works with any storage backend — SQL, NoSQL, or even in-memory for tests. ::: ### IUserStore Interface ```typescript import { IUserStore, BaseUser } from '@awesome-lang-auth/node'; export class MyUserStore implements IUserStore { // Core CRUD async findByEmail(email: string): Promise { /* ... */ } async findById(id: string): Promise { /* ... */ } async create(data: Partial): Promise { /* ... */ } // Token field updates async updateRefreshToken(userId: string, token: string | null, expiry: Date | null): Promise { /* ... */ } async updateLastLogin(userId: string): Promise { /* ... */ } async updateResetToken(userId: string, token: string | null, expiry: Date | null): Promise { /* ... */ } async updatePassword(userId: string, hashedPassword: string): Promise { /* ... */ } async updateTotpSecret(userId: string, secret: string | null): Promise { /* ... */ } async updateMagicLinkToken(userId: string, token: string | null, expiry: Date | null): Promise { /* ... */ } async updateSmsCode(userId: string, code: string | null, expiry: Date | null): Promise { /* ... */ } // Important optional methods async findByResetToken(token: string): Promise { /* ... */ } async findByMagicLinkToken(token: string): Promise { /* ... */ } async findByProviderAccount(provider: string, providerAccountId: string): Promise { /* ... */ } async updateProfile(userId: string, data: { firstName?: string | null; lastName?: string | null }): Promise { /* ... */ } } ``` ### Required Methods by Feature | Method | Required For | |--------|-------------| | `findByEmail` | Login, magic link, password reset | | `findById` | Token refresh, 2FA, SMS | | `create` | OAuth strategies, registration | | `updateRefreshToken` | Login, logout, refresh | | `updateResetToken` | Password reset flow | | `updatePassword` | Change/reset password | | `updateTotpSecret` | TOTP setup | | `updateMagicLinkToken` | Magic link flow | | `updateSmsCode` | SMS OTP flow | | `findByResetToken` | `POST /auth/reset-password` | | `findByMagicLinkToken` | `POST /auth/magic-link/verify` | | `findByProviderAccount` | OAuth (recommended) | ### BaseUser ```typescript interface BaseUser { id: string; email: string; password?: string; firstName?: string | null; lastName?: string | null; role?: string; loginProvider?: string | null; providerAccountId?: string | null; refreshToken?: string | null; refreshTokenExpiry?: Date | null; lastLogin?: Date | null; // ... other fields for 2FA, Magic Link, SMS } ``` ### Available Implementations **In-Memory** Perfect for testing and prototyping. No setup required. [View docs →](/docs/database/in-memory) **SQLite** Uses `better-sqlite3`. Great for small apps and local development. [View docs →](/docs/database/sqlite) **MySQL** Uses `mysql2`. Production-ready relational database. [View docs →](/docs/database/mysql) **MongoDB** Uses the `mongodb` driver. Flexible document storage. [View docs →](/docs/database/mongodb) **PostgreSQL** Uses `pg`. Robust SQL with full ACID compliance. [View docs →](/docs/database/postgresql) **PostgREST** Use `fetch` to talk to a REST API. Write no SQL. [View docs →](/docs/database/postgrest) **PHP-CRUD-API** Use `fetch` to talk to a REST API. Write no SQL. [View docs →](/docs/database/php-crud-api) --- ## In-Memory User Store for Tests and Prototypes ## In-Memory Store The in-memory store is ideal for testing, prototyping, and development. It's included in the `examples/` directory. ### Usage ```typescript import { InMemoryUserStore } from './examples/in-memory-user-store'; const userStore = new InMemoryUserStore(); const auth = new AuthConfigurator(config, userStore); ``` ### Example Implementation ```typescript import { IUserStore, BaseUser } from '@awesome-lang-auth/node'; export class InMemoryUserStore implements IUserStore { private users: BaseUser[] = []; private nextId = 1; async findByEmail(email: string) { return this.users.find(u => u.email === email) ?? null; } async findById(id: string) { return this.users.find(u => u.id === id) ?? null; } async create(data: Partial): Promise { const user: BaseUser = { id: String(this.nextId++), email: data.email!, ...data, }; this.users.push(user); return user; } async updateRefreshToken(userId: string, token: string | null, expiry: Date | null) { const user = this.users.find(u => u.id === userId); if (user) { user.refreshToken = token; user.refreshTokenExpiry = expiry; } } // ... other methods } ``` ### Related - [Database-agnostic auth with IUserStore](/docs/database) — the interface this store implements - [SQLite authentication store](/docs/database/sqlite) — the smallest persistent alternative - [Live Node.js authentication demos](/docs/live-demo) — demos that run on this store - [Express authentication](/docs/frameworks/express) — the server used in the runnable demo --- ## SQLite Authentication Store (better-sqlite3) ## SQLite with better-sqlite3 A production-ready SQLite implementation using `better-sqlite3`. ### Install ```bash npm install better-sqlite3 npm install -D @types/better-sqlite3 ``` ### Setup ```typescript import Database from 'better-sqlite3'; import { SqliteUserStore } from './examples/sqlite-user-store.example'; const db = new Database('app.db'); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); const userStore = new SqliteUserStore(db); // creates `users` table automatically const auth = new AuthConfigurator(config, userStore); ``` The `SqliteUserStore` from `examples/sqlite-user-store.example.ts` automatically creates the `users` table on first run. ### Schema ```sql CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password TEXT, name TEXT, role TEXT DEFAULT 'user', loginProvider TEXT DEFAULT 'local', providerAccountId TEXT, refreshToken TEXT, refreshTokenExpiry TEXT, resetToken TEXT, resetTokenExpiry TEXT, totpSecret TEXT, isTotpEnabled INTEGER DEFAULT 0, magicLinkToken TEXT, magicLinkTokenExpiry TEXT, smsCode TEXT, smsCodeExpiry TEXT, isEmailVerified INTEGER DEFAULT 0, phoneNumber TEXT, createdAt TEXT DEFAULT CURRENT_TIMESTAMP ); ``` ### Related - [Database-agnostic auth with IUserStore](/docs/database) — the interface this store implements - [PostgreSQL authentication store](/docs/database/postgresql) — when SQLite stops being enough - [Extending BaseUser and auth interfaces](/docs/advanced/extending-interfaces) — add your own columns safely - [Session management and token revocation](/docs/advanced/sessions) — persist sessions alongside users --- ## MySQL and MariaDB Authentication Store (mysql2) ## MySQL / MariaDB with mysql2 ### Install ```bash npm install mysql2 ``` ### Setup ```typescript import mysql from 'mysql2/promise'; import { MySqlUserStore } from './examples/mysql-user-store.example'; const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, }); const userStore = new MySqlUserStore(pool); await userStore.init(); // creates the `users` table automatically const auth = new AuthConfigurator(config, userStore); ``` ### Schema ```sql CREATE TABLE IF NOT EXISTS users ( id VARCHAR(36) PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255), name VARCHAR(255), role VARCHAR(50) DEFAULT 'user', loginProvider VARCHAR(50) DEFAULT 'local', providerAccountId VARCHAR(255), refreshToken TEXT, refreshTokenExpiry DATETIME, resetToken VARCHAR(255), resetTokenExpiry DATETIME, totpSecret VARCHAR(255), isTotpEnabled TINYINT(1) DEFAULT 0, magicLinkToken VARCHAR(255), magicLinkTokenExpiry DATETIME, smsCode VARCHAR(10), smsCodeExpiry DATETIME, isEmailVerified TINYINT(1) DEFAULT 0, phoneNumber VARCHAR(20), createdAt DATETIME DEFAULT CURRENT_TIMESTAMP ); ``` ### Related - [Database-agnostic auth with IUserStore](/docs/database) — the interface this store implements - [PostgreSQL authentication store](/docs/database/postgresql) — the other SQL adapter - [Authentication on top of PHP-CRUD-API](/docs/database/php-crud-api) — reach MySQL over REST instead of SQL - [Extending BaseUser and auth interfaces](/docs/advanced/extending-interfaces) — add your own columns safely --- ## MongoDB Authentication Store for Node.js ## MongoDB ### Install ```bash npm install mongodb ``` ### Setup ```typescript import { MongoClient } from 'mongodb'; import { MongoDbUserStore } from './examples/mongodb-user-store.example'; const client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); const userStore = new MongoDbUserStore(client.db('myapp')); await userStore.init(); // creates indexes automatically const auth = new AuthConfigurator(config, userStore); ``` ### Collection Schema ```typescript interface UserDocument { _id: string; // same as BaseUser.id email: string; password?: string; name?: string; role: string; loginProvider: string; providerAccountId?: string; refreshToken?: string | null; refreshTokenExpiry?: Date | null; resetToken?: string | null; resetTokenExpiry?: Date | null; totpSecret?: string | null; isTotpEnabled: boolean; magicLinkToken?: string | null; magicLinkTokenExpiry?: Date | null; smsCode?: string | null; smsCodeExpiry?: Date | null; isEmailVerified: boolean; phoneNumber?: string; createdAt: Date; } ``` ### Indexes The `init()` method creates: - Unique index on `email` - Index on `refreshToken` - Index on `resetToken` - Index on `magicLinkToken` ### Related - [Database-agnostic auth with IUserStore](/docs/database) — the interface this store implements - [PostgreSQL authentication store](/docs/database/postgresql) — the SQL counterpart of this adapter - [User metadata without schema changes](/docs/advanced/user-metadata) — store extra fields per user - [Extending BaseUser and auth interfaces](/docs/advanced/extending-interfaces) — type-safe custom documents --- ## PostgreSQL Authentication Store (node-postgres) ## PostgreSQL with pg ### Install ```bash npm install pg npm install -D @types/pg ``` ### Setup ```typescript import { Pool } from 'pg'; import { IUserStore, BaseUser } from '@awesome-lang-auth/node'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); export class PgUserStore implements IUserStore { async findByEmail(email: string): Promise { const { rows } = await pool.query( 'SELECT * FROM users WHERE email = $1', [email] ); return rows[0] ?? null; } async findById(id: string): Promise { const { rows } = await pool.query( 'SELECT * FROM users WHERE id = $1', [id] ); return rows[0] ?? null; } async create(data: Partial): Promise { const { rows } = await pool.query( `INSERT INTO users (id, email, password, name, role, login_provider) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [crypto.randomUUID(), data.email, data.password, data.name, data.role ?? 'user', data.loginProvider ?? 'local'] ); return rows[0]; } async updateRefreshToken(userId: string, token: string | null, expiry: Date | null): Promise { await pool.query( 'UPDATE users SET refresh_token = $1, refresh_token_expiry = $2 WHERE id = $3', [token, expiry, userId] ); } // ... implement remaining methods } ``` ### Schema ```sql CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255), name VARCHAR(255), role VARCHAR(50) DEFAULT 'user', login_provider VARCHAR(50) DEFAULT 'local', provider_account_id VARCHAR(255), refresh_token TEXT, refresh_token_expiry TIMESTAMPTZ, reset_token VARCHAR(255), reset_token_expiry TIMESTAMPTZ, totp_secret VARCHAR(255), is_totp_enabled BOOLEAN DEFAULT false, magic_link_token VARCHAR(255), magic_link_token_expiry TIMESTAMPTZ, sms_code VARCHAR(10), sms_code_expiry TIMESTAMPTZ, is_email_verified BOOLEAN DEFAULT false, phone_number VARCHAR(20), created_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### Related - [Database-agnostic auth with IUserStore](/docs/database) — the interface this store implements - [Authentication on top of PostgREST](/docs/database/postgrest) — same database, no raw SQL - [MySQL and MariaDB authentication store](/docs/database/mysql) — the other SQL adapter - [Session management and token revocation](/docs/advanced/sessions) — persist sessions alongside users --- ## Authentication on Top of PostgREST (No Raw SQL) ## PostgREST Integration You don't need to write raw SQL to use `node-auth`. Because the library is entirely database-agnostic through the `IUserStore` interface, you can implement your storage layer using RESTful APIs like [PostgREST](https://postgrest.org/) (the engine behind Supabase). This approach allows you to keep your database tightly secured behind PostgREST's API layer while `node-auth` handles the complex JWT flows, session management, and OAuth integrations. ### IUserStore Implementation Below is a complete example of how to implement the `IUserStore` interface using the native `fetch` API against a PostgREST server. ```typescript import { IUserStore, BaseUser } from '@awesome-lang-auth/node'; export class PostgRestUserStore implements IUserStore { constructor( private readonly apiUrl: string, private readonly serviceKey: string // API key with bypass-RLS permissions ) {} private get headers() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.serviceKey}`, 'Prefer': 'return=representation' }; } // ---- Core CRUD ----------------------------------------------------------- async findByEmail(email: string): Promise { const res = await fetch(`${this.apiUrl}/users?email=eq.${encodeURIComponent(email)}`, { method: 'GET', headers: this.headers, }); const users = await res.json(); return users[0] || null; } async findById(id: string): Promise { const res = await fetch(`${this.apiUrl}/users?id=eq.${encodeURIComponent(id)}`, { method: 'GET', headers: this.headers, }); const users = await res.json(); return users[0] || null; } async create(data: Partial): Promise { const res = await fetch(`${this.apiUrl}/users`, { method: 'POST', headers: this.headers, body: JSON.stringify(data), }); if (!res.ok) { throw new Error(`Failed to create user: ${await res.text()}`); } const users = await res.json(); return users[0]; } // ---- Token field updates ------------------------------------------------ private async updateField(userId: string, updates: Partial): Promise { const res = await fetch(`${this.apiUrl}/users?id=eq.${encodeURIComponent(userId)}`, { method: 'PATCH', headers: this.headers, body: JSON.stringify(updates), }); if (!res.ok) { throw new Error(`Failed to update user: ${await res.text()}`); } } async updateRefreshToken(userId: string, token: string | null, expiry: Date | null): Promise { await this.updateField(userId, { refreshToken: token, refreshTokenExpiry: expiry }); } async updateResetToken(userId: string, token: string | null, expiry: Date | null): Promise { await this.updateField(userId, { resetToken: token, resetTokenExpiry: expiry }); } async updatePassword(userId: string, hashedPassword: string): Promise { await this.updateField(userId, { password: hashedPassword }); } async updateTotpSecret(userId: string, secret: string | null): Promise { await this.updateField(userId, { totpSecret: secret, isTotpEnabled: !!secret }); } async updateMagicLinkToken(userId: string, token: string | null, expiry: Date | null): Promise { await this.updateField(userId, { magicLinkToken: token, magicLinkTokenExpiry: expiry }); } async updateSmsCode(userId: string, code: string | null, expiry: Date | null): Promise { await this.updateField(userId, { smsCode: code, smsCodeExpiry: expiry }); } // ---- Optional finders --------------------------------------------------- async findByResetToken(token: string): Promise { const res = await fetch(`${this.apiUrl}/users?resetToken=eq.${encodeURIComponent(token)}`, { method: 'GET', headers: this.headers, }); const users = await res.json(); return users[0] || null; } async findByMagicLinkToken(token: string): Promise { const res = await fetch(`${this.apiUrl}/users?magicLinkToken=eq.${encodeURIComponent(token)}`, { method: 'GET', headers: this.headers, }); const users = await res.json(); return users[0] || null; } async findByProviderAccount(provider: string, providerAccountId: string): Promise { const res = await fetch(`${this.apiUrl}/users?loginProvider=eq.${encodeURIComponent(provider)}&providerAccountId=eq.${encodeURIComponent(providerAccountId)}`, { method: 'GET', headers: this.headers, }); const users = await res.json(); return users[0] || null; } } ``` ### Setup 1. Make sure your PostgREST instance exposes a `users` table or view that matches the `BaseUser` interface. 2. Initialize the store in your application: ```typescript import { AuthConfigurator } from '@awesome-lang-auth/node'; import { PostgRestUserStore } from './postgrest-user-store'; const userStore = new PostgRestUserStore( 'http://localhost:3000', // URL to PostgREST process.env.POSTGREST_SERVICE_KEY! ); const auth = new AuthConfigurator(config, userStore); ``` ### Related - [PostgreSQL authentication store](/docs/database/postgresql) — the direct-SQL alternative - [Authentication on top of PHP-CRUD-API](/docs/database/php-crud-api) — the same pattern for MySQL and SQL Server - [Database-agnostic auth with IUserStore](/docs/database) — the interface a REST store implements - [Extending BaseUser and auth interfaces](/docs/advanced/extending-interfaces) — type your REST payloads --- ## Authentication on Top of PHP-CRUD-API ## PHP-CRUD-API Integration Just like PostgREST, you don't need to write raw SQL to use `node-auth`. The `IUserStore` interface allows you to implement your storage layer using RESTful APIs like [PHP-CRUD-API](https://github.com/mevdschee/php-crud-api). This approach allows you to deploy a lightweight, zero-configuration API layer over your MySQL, PostgreSQL, or SQL Server database, while `node-auth` handles the complex authentication, cryptography, and OAuth integrations. ### IUserStore Implementation Below is a complete example of how to implement the `IUserStore` interface using the native `fetch` API against a PHP-CRUD-API backend. > **Note:** PHP-CRUD-API uses a query parameter format like `?filter=field,eq,value` for filtering and returns the actual record in the body. Ensure your table structure matches `BaseUser`. ```typescript import { IUserStore, BaseUser } from '@awesome-lang-auth/node'; export class PhpCrudApiUserStore implements IUserStore { constructor( private readonly apiUrl: string, // e.g., 'http://localhost/api.php/records/' ) {} private get headers() { return { 'Content-Type': 'application/json', // 'X-API-Key': 'your-secret-key-if-configured' }; } // ---- Core CRUD ----------------------------------------------------------- async findByEmail(email: string): Promise { const res = await fetch(`${this.apiUrl}users?filter=email,eq,${encodeURIComponent(email)}`, { method: 'GET', headers: this.headers, }); const data = await res.json(); return data.records?.[0] || null; } async findById(id: string): Promise { const res = await fetch(`${this.apiUrl}users/${encodeURIComponent(id)}`, { method: 'GET', headers: this.headers, }); if (res.status === 404) return null; return await res.json(); } async create(data: Partial): Promise { const res = await fetch(`${this.apiUrl}users`, { method: 'POST', headers: this.headers, body: JSON.stringify(data), }); if (!res.ok) { throw new Error(`Failed to create user: ${await res.text()}`); } // PHP-CRUD-API returns the ID of the new record (usually an integer, depending on your DB) const newId = await res.json(); // Fetch the complete created user to satisfy the interface const createdUserRes = await fetch(`${this.apiUrl}users/${newId}`, { method: 'GET', headers: this.headers }); return await createdUserRes.json(); } // ---- Token field updates ------------------------------------------------ private async updateField(userId: string, updates: Partial): Promise { const res = await fetch(`${this.apiUrl}users/${encodeURIComponent(userId)}`, { method: 'PUT', headers: this.headers, body: JSON.stringify(updates), }); if (!res.ok) { throw new Error(`Failed to update user: ${await res.text()}`); } } async updateRefreshToken(userId: string, token: string | null, expiry: Date | null): Promise { await this.updateField(userId, { refreshToken: token, refreshTokenExpiry: expiry }); } async updateResetToken(userId: string, token: string | null, expiry: Date | null): Promise { await this.updateField(userId, { resetToken: token, resetTokenExpiry: expiry }); } async updatePassword(userId: string, hashedPassword: string): Promise { await this.updateField(userId, { password: hashedPassword }); } async updateTotpSecret(userId: string, secret: string | null): Promise { await this.updateField(userId, { totpSecret: secret, isTotpEnabled: !!secret }); } async updateMagicLinkToken(userId: string, token: string | null, expiry: Date | null): Promise { await this.updateField(userId, { magicLinkToken: token, magicLinkTokenExpiry: expiry }); } async updateSmsCode(userId: string, code: string | null, expiry: Date | null): Promise { await this.updateField(userId, { smsCode: code, smsCodeExpiry: expiry }); } // ---- Optional finders --------------------------------------------------- async findByResetToken(token: string): Promise { const res = await fetch(`${this.apiUrl}users?filter=resetToken,eq,${encodeURIComponent(token)}`, { method: 'GET', headers: this.headers, }); const data = await res.json(); return data.records?.[0] || null; } async findByMagicLinkToken(token: string): Promise { const res = await fetch(`${this.apiUrl}users?filter=magicLinkToken,eq,${encodeURIComponent(token)}`, { method: 'GET', headers: this.headers, }); const data = await res.json(); return data.records?.[0] || null; } async findByProviderAccount(provider: string, providerAccountId: string): Promise { const res = await fetch(`${this.apiUrl}users?filter=loginProvider,eq,${encodeURIComponent(provider)}&filter=providerAccountId,eq,${encodeURIComponent(providerAccountId)}`, { method: 'GET', headers: this.headers, }); const data = await res.json(); return data.records?.[0] || null; } } ``` ### Setup 1. Make sure your PHP-CRUD-API backend is running and exposes a `users` table that matches the `BaseUser` properties. 2. Initialize the store in your Node.js application: ```typescript import { AuthConfigurator } from '@awesome-lang-auth/node'; import { PhpCrudApiUserStore } from './php-crud-api-user-store'; const userStore = new PhpCrudApiUserStore( 'http://localhost:8080/api.php/records/' ); const auth = new AuthConfigurator(config, userStore); ``` ### Related - [Authentication on top of PostgREST](/docs/database/postgrest) — the same pattern for PostgreSQL - [MySQL and MariaDB authentication store](/docs/database/mysql) — talk to MySQL directly instead - [Database-agnostic auth with IUserStore](/docs/database) — the interface a REST store implements - [Extending BaseUser and auth interfaces](/docs/advanced/extending-interfaces) — type your REST payloads --- ## API Reference: Auth, Admin and Tools Routers ## API Reference **node-auth** is a 360-degree communication and access control layer that exposes its functionality through **three core routers**. You can mount any combination of these routers depending on your application's needs. ### 1. Auth Router (Default: `/auth`) Handles all user-facing authentication flows (login, registration, password resets, 2FA, OAuth, etc). ### 2. Tools Router (Default: `/tools`) Handles machine-to-machine communication, telemetry, Server-Sent Events (SSE), and inbound/outgoing dynamic webhooks. See [Tools Router Endpoints](/docs/advanced/swagger#tools-router-endpoints) for the full list. ### 3. Admin Router (Default: `/admin/api`) Provides a secure REST API (and a built-in UI) for administrative tasks like user management, role assignments, UI webhooks, settings, and session revocation. See [Admin REST API](/docs/advanced/admin#rest-api-reference) for the full list. --- ### Auth Router Endpoints Overview | Method | Path | Description | Auth Required | |--------|------|-------------|---------------| | `POST` | `/auth/login` | Email/password login | ❌ | | `POST` | `/auth/register` | Register new user (requires `onRegister`) | ❌ | | `POST` | `/auth/logout` | Log out | ✅ | | `POST` | `/auth/refresh` | Refresh tokens | ❌ (refresh token) | | `GET` | `/auth/me` | Get current user profile | ✅ | | `POST` | `/auth/forgot-password` | Request password reset | ❌ | | `POST` | `/auth/reset-password` | Reset with token | ❌ | | `POST` | `/auth/change-password` | Change password | ✅ | | `POST` | `/auth/send-verification-email` | Send email verification | ✅ | | `GET` | `/auth/verify-email` | Verify email token | ❌ | | `POST` | `/auth/change-email/request` | Request email change | ✅ | | `POST` | `/auth/change-email/confirm` | Confirm email change | ❌ | | `POST` | `/auth/magic-link/send` | Send magic link | ❌ | | `POST` | `/auth/magic-link/verify` | Verify magic link | ❌ | | `POST` | `/auth/sms/send` | Send SMS OTP | ❌ | | `POST` | `/auth/sms/verify` | Verify SMS OTP | ❌ | | `POST` | `/auth/2fa/setup` | Get TOTP secret + QR code | ✅ | | `POST` | `/auth/2fa/verify-setup` | Activate TOTP | ✅ | | `POST` | `/auth/2fa/verify` | Complete 2FA login | ❌ (temp token) | | `POST` | `/auth/2fa/disable` | Disable 2FA | ✅ | | `GET` | `/auth/oauth/:provider` | OAuth redirect | ❌ | | `GET` | `/auth/oauth/:provider/callback` | OAuth callback | ❌ | | `POST` | `/auth/sessions/cleanup` | Delete expired sessions | ❌ | | `GET` | `/auth/linked-accounts` | List linked OAuth accounts | ✅ | | `DELETE` | `/auth/linked-accounts/:provider/:id` | Unlink OAuth account | ✅ | | `POST` | `/auth/link-request` | Link new email address | ✅ | | `POST` | `/auth/link-verify` | Confirm account link | ❌ | | `DELETE` | `/auth/account` | Delete account | ✅ | See [Endpoints](/docs/api-reference/endpoints) for full request/response documentation. ### Related - [Auth API endpoints reference](/docs/api-reference/endpoints) — request and response bodies for every route - [ITemplateStore: custom emails and UI i18n](/docs/api-reference/template-store) — override transactional emails and UI strings - [Swagger UI and OpenAPI 3.0 spec](/docs/advanced/swagger) — the machine-readable version of this reference - [Built-in admin panel](/docs/advanced/admin) — the admin router's own REST API --- ## Auth API Endpoints Reference ## Endpoint Reference This page documents the endpoints exposed by the **Auth Router** (typically mounted at `/auth`). For the other parts of the 360-degree layer, see: - **Tools Router** (SSE, Webhooks, Telemetry): [Tools Router Endpoints](/docs/advanced/swagger#tools-router-endpoints) - **Admin Router** (User, Role, and Tenant management): [Admin REST API](/docs/advanced/admin#rest-api-reference) - **API Keys**: [API Keys Guide](/docs/advanced/api-keys) :::info Authentication Protected endpoints require either a valid `accessToken` **HttpOnly cookie** (set automatically on login) or an `Authorization: Bearer ` header. ::: :::info Token delivery By default, token-issuing endpoints (`/login`, `/refresh`, `/2fa/verify`, `/magic-link/verify`, `/sms/verify`) set **HttpOnly cookies** and return `{ "success": true }`. To receive tokens in the JSON response body instead (for native/mobile clients), add the `X-Auth-Strategy: bearer` request header. See [Bearer Token Mode](/docs/advanced/bearer-token) for details. ::: --- ### Authentication #### `POST` /auth/login **Request body:** ```json { "email": "user@example.com", "password": "secret" } ``` **Response `200` (cookie mode — default):** ```json { "success": true } ``` HttpOnly cookies `accessToken`, `refreshToken` (and `csrf-token` if CSRF is enabled) are set automatically. **Response `200` (bearer mode — `X-Auth-Strategy: bearer`):** ```json { "success": true, "accessToken": "eyJ...", "refreshToken": "eyJ..." } ``` **Response `200` (2FA required):** ```json { "requiresTwoFactor": true, "tempToken": "eyJ...", "available2faMethods": ["totp", "sms"] } ``` --- #### `POST` /auth/register Requires `onRegister` callback in router options. **Request body:** ```json { "email": "user@example.com", "password": "secret", "name": "Jane" } ``` **Response `201`:** ```json { "success": true, "userId": "123" } ``` --- #### `POST` /auth/logout Clears the refresh token server-side. Requires valid access token (cookie or bearer). **Response `200`:** ```json { "success": true } ``` --- #### `POST` /auth/refresh Uses the refresh token (from HttpOnly cookie or from `refreshToken` in request body) to issue new tokens. **Response `200` (cookie mode — default):** ```json { "success": true } ``` New `accessToken` and `refreshToken` cookies are set automatically. **Response `200` (bearer mode — `X-Auth-Strategy: bearer`):** ```json { "success": true, "accessToken": "eyJ...", "refreshToken": "eyJ..." } ``` --- #### `GET` /auth/me Returns the current user's full profile. Requires authentication. **Response `200`:** ```json { "sub": "123", "email": "user@example.com", "role": "user", "loginProvider": "local", "isEmailVerified": true, "isTotpEnabled": false, "metadata": { "theme": "dark" }, "roles": ["editor"], "permissions": ["posts:read", "posts:write"] } ``` --- ### Password Management #### `POST` /auth/forgot-password **Request body:** ```json { "email": "user@example.com" } ``` #### `POST` /auth/reset-password **Request body:** ```json { "token": "reset-token-from-email", "newPassword": "newpassword123" } ``` #### `POST` /auth/change-password Requires authentication. **Request body:** ```json { "currentPassword": "old", "newPassword": "new" } ``` --- ### Email Verification #### `POST` /auth/send-verification-email Sends a verification email to the authenticated user. Requires authentication. #### `GET` /auth/verify-email?token=... Verifies the email token from the link. Redirects on success. --- ### Email Change #### `POST` /auth/change-email/request Sends a confirmation email to the new address. Requires authentication. **Request body:** ```json { "newEmail": "newemail@example.com" } ``` #### `POST` /auth/change-email/confirm Confirms the email change using the token from the confirmation email. **Request body:** ```json { "token": "token-from-email" } ``` --- ### Magic Link | Method | Path | Description | |--------|------|-------------| | `POST` | `/auth/magic-link/send` | Request magic link. Body: `{ email }` for login; `{ tempToken, mode: '2fa' }` for 2FA second factor | | `POST` | `/auth/magic-link/verify` | Verify token → set session cookies (or return tokens in bearer mode). Body: `{ token, mode?, tempToken? }` | --- ### SMS OTP | Method | Path | Description | |--------|------|-------------| | `POST` | `/auth/sms/send` | Send OTP. Body: `{ email }` or `{ userId }` for login; `{ tempToken, mode: '2fa' }` for 2FA | | `POST` | `/auth/sms/verify` | Verify OTP → set session cookies (or return tokens in bearer mode). Body: `{ userId, code }` for login; `{ tempToken, code, mode: '2fa' }` for 2FA | --- ### TOTP (Two-Factor Authentication) | Method | Path | Auth | Description | |--------|------|:----:|-------------| | `POST` | `/auth/2fa/setup` | ✅ | Generate TOTP secret and QR code data URI | | `POST` | `/auth/2fa/verify-setup` | ✅ | Confirm TOTP enrollment. Body: `{ token, secret }` | | `POST` | `/auth/2fa/verify` | — (temp token) | Complete 2FA login. Body: `{ tempToken, totpCode }` | | `POST` | `/auth/2fa/disable` | ✅ | Disable TOTP for the current user | --- ### OAuth | Method | Path | Description | |--------|------|-------------| | `GET` | `/auth/oauth/:provider` | Redirect to provider | | `GET` | `/auth/oauth/:provider/callback` | Handle OAuth callback | --- ### Sessions | Method | Path | Description | |--------|------|-------------| | `POST` | `/auth/sessions/cleanup` | Delete expired sessions (requires `sessionStore.deleteExpiredSessions`) | Active session listing and per-session revocation are available through the [Admin Panel](/docs/advanced/admin) at `/admin/api/sessions`. --- ### Account Linking | Method | Path | Auth | Description | |--------|------|:----:|-------------| | `GET` | `/auth/linked-accounts` | ✅ | List linked OAuth accounts for the current user | | `DELETE` | `/auth/linked-accounts/:provider/:id` | ✅ | Unlink an OAuth account | | `POST` | `/auth/link-request` | ✅ | Request to link a new email address (sends verification email) | | `POST` | `/auth/link-verify` | ❌ | Complete linking using token from email | --- ### Account #### `DELETE` /auth/account Deletes the authenticated user's account and all associated data. --- ## ITemplateStore: Custom Emails and UI i18n ## ITemplateStore `ITemplateStore` is the optional interface that powers the **dynamic email template** and **UI i18n** systems in awesome-node-auth. Implement it against your database to: - Override any built-in transactional email (password reset, magic link, welcome, etc.) with custom HTML/text and per-language translations - Inject page-specific UI translation strings into the built-in login/register pages at render time --- ### Types #### `MailTemplate` ```typescript export interface MailTemplate { /** Template identifier. Built-in IDs: 'magic-link' | 'password-reset' | 'verify-email' | 'welcome' | 'email-changed' | 'invitation' */ id: string; /** HTML body. Use {{T.key}} for translated strings and {{VAR}} for data variables. Leave empty to use the built-in fallback template. */ baseHtml: string; /** Plain-text fallback body. Same placeholder syntax. Leave empty to use the built-in fallback template. */ baseText: string; /** Per-language translation map: lang → { key: value } */ translations: Record>; } ``` #### `UiTranslation` ```typescript export interface UiTranslation { /** Page identifier. Built-in pages: 'login' | 'register' | 'forgot-password' | 'reset-password' | 'magic-link' | 'verify-email' | '2fa' | 'link-verify' | 'account-conflict' */ page: string; /** Per-language translation map: lang → { data-i18n-key: text } */ translations: Record>; } ``` --- ### Interface definition ```typescript export interface ITemplateStore { /** Retrieve a mail template by ID. Returns null if not found (built-in fallback is used). */ getMailTemplate(id: string): Promise; /** List all stored mail templates. */ listMailTemplates(): Promise; /** Create or update a mail template. */ updateMailTemplate(id: string, template: Partial): Promise; /** Retrieve UI translations for a specific page. Returns null if not found. */ getUiTranslations(page: string): Promise; /** List all stored UI translation sets. */ listUiTranslations(): Promise; /** Create or update UI translations for a page. */ updateUiTranslations(page: string, translations: Record>): Promise; } ``` --- ### Built-in implementation `MemoryTemplateStore` is a zero-dependency in-memory implementation ready for development and testing: ```typescript import { MemoryTemplateStore } from '@awesome-lang-auth/node'; const templateStore = new MemoryTemplateStore(); ``` Replace it with a database-backed implementation in production (see [Mailer → Dynamic templates](/docs/advanced/mailer#dynamic-templates-with-itemplatestore)). --- ### Wiring Pass `templateStore` to **both** `AuthConfigurator` and `createAdminRouter`: ```typescript import { AuthConfigurator, createAdminRouter, MemoryTemplateStore, buildUiRouter } from '@awesome-lang-auth/node'; const templateStore = new MemoryTemplateStore(); const auth = new AuthConfigurator( { // ... JWT secrets & mailer config ... templateStore, // ← enables dynamic email templates }, userStore, ); // (optional) Enable i18n injection into built-in UI pages app.use('/auth/ui', buildUiRouter({ authConfig, apiPrefix: '/auth', templateStore, })); // (optional) Enable the 📧 Email & UI tab in the Admin Panel app.use('/admin', createAdminRouter(userStore, { accessPolicy: 'first-user', jwtSecret: process.env.ACCESS_TOKEN_SECRET!, templateStore, })); ``` --- ### Interpolation reference #### Email templates | Syntax | Source | Example output | |--------|--------|----------------| | `{{T.key}}` | `MailTemplate.translations[lang][key]` | `"Reset your password"` | | `{{link}}` | Reset / magic-link / verify URL | `"https://app.com/reset?token=…"` | | `{{token}}` | Raw token (rarely needed in template HTML) | `"abc123…"` | | `{{newEmail}}` | New email address (`email-changed` template) | `"new@example.com"` | | `{{loginUrl}}` | Login page URL (`welcome` template) | `"https://app.com/login"` | | `{{tempPassword}}` | Temporary password (`welcome` template, admin-created users) | `"Tmp@1234"` | A missing key renders as `[key]` to help with debugging. > **Reset to default:** save a template with empty `baseHtml` and `baseText` (or use the > "Reset to default" button in the admin panel) and `MailerService` will automatically fall > back to the built-in template for that ID. #### UI translations Each `data-i18n` attribute value on a built-in HTML element is looked up in `UiTranslation.translations[lang]`. The key must exactly match the attribute value: ```html

Sign in

``` --- ### Admin REST API When `templateStore` is provided to `createAdminRouter`, the following endpoints are activated: | Method | Path | Description | |--------|------|-------------| | `GET` | `/admin/api/templates/mail` | List all mail templates | | `POST` | `/admin/api/templates/mail` | Create or update a mail template (body: `{ id, baseHtml, baseText, translations }`) | | `GET` | `/admin/api/templates/ui` | List all UI translation sets | | `POST` | `/admin/api/templates/ui` | Create or update UI translations (body: `{ page, translations }`) | --- ## Advanced Authentication Features ## Advanced Features `awesome-node-auth` provides optional interfaces and hooks for advanced functionality. Every feature is opt-in — only configure what you need. | Feature | Guide | Description | |---------|-------|-------------| | [Sessions](/docs/advanced/sessions) | `ISessionStore` | External persistence for auditing and admin-level revocation | | [IdP Mode](/docs/advanced/idp-mode) | `IdProviderConfig` | RS256-signed JWTs + JWKS endpoint — act as a central Identity Provider | | [Roles & Permissions](/docs/advanced/roles-permissions) | `IRolesPermissionsStore` | RBAC — assign roles, permissions, tenant-scoped | | [Multi-Tenancy](/docs/advanced/multi-tenancy) | `ITenantStore` | Tenant CRUD, user↔tenant association | | [CSRF Protection](/docs/advanced/csrf) | `AuthConfig.csrf` | Double-submit cookie pattern | | [User Metadata](/docs/advanced/user-metadata) | `IUserMetadataStore` | Arbitrary per-user key/value data | | [Account Linking](/docs/advanced/account-linking) | `ILinkedAccountsStore` | Link multiple OAuth providers to one account | | [Admin Panel](/docs/advanced/admin) | `createAdminRouter()` | Built-in admin dashboard (HTML UI + REST API) | | [Dynamic Inbound Webhooks](/docs/advanced/webhooks#dynamic-inbound-execution) | `@webhookAction`, `ActionRegistry`, `node:vm` | Govern and execute JS scripts on inbound webhooks via the admin UI; scripts run with the server's privileges, so only trusted operators may write them | | [Bearer Token](/docs/advanced/bearer-token) | — | Mobile/native clients: token storage, interceptors | | [Mailer (HTTP)](/docs/advanced/mailer) | `MailerConfig` | Built-in HTTP email transport with bilingual templates | | [Email Verification](/docs/advanced/email-verification) | `emailVerificationMode` | `none` / `lazy` / `strict` verification enforcement | | [Change Email](/docs/advanced/change-email) | — | Two-step email address change with confirmation link | | [Account Deletion](/docs/advanced/account-deletion) | — | `DELETE /auth/account` + tenant cleanup | | [Custom JWT Claims](/docs/advanced/custom-claims) | `buildTokenPayload` | Embed tenant IDs, permissions, feature flags in JWT | | [AuthEventBus](/docs/advanced/auth-event-bus) | `AuthEventBus` | Central event bus — subscribe to 26 standardised identity events | | [API Keys](/docs/advanced/api-keys) | `ApiKeyService`, `IApiKeyStore` | Machine-to-machine API key authentication with bcrypt hashing | | [AuthTools](/docs/advanced/auth-tools) | `AuthTools` | Unified entry point for telemetry, SSE, and webhooks | | [SSE](/docs/advanced/sse) | `SseManager` | Real-time Server-Sent Events streaming by topic | | [Scaling SSE](/docs/advanced/sse-scaling) | Redis pub/sub adapter | Broadcast SSE events across multiple instances or a Kubernetes cluster | | [Real-time Decorators](/docs/advanced/sse-notify-decorator) | `@sseNotify` | Fire real-time notifications from any service or store method | | [Webhooks](/docs/advanced/webhooks) | `WebhookSender`, `IWebhookStore` | Outgoing webhooks with HMAC signing and retry | | [Telemetry](/docs/advanced/telemetry) | `ITelemetryStore` | Persist and query identity events | | [Swagger / OpenAPI](/docs/advanced/swagger) | `createToolsRouter()` | Auto-generated OpenAPI 3.0 spec + Swagger UI | | [Browser Client](/docs/advanced/browser-client) | `window.AwesomeNodeAuth` | Zero-config JS client shipped as `auth.js` — CSRF, auto-refresh, guards, all auth methods | | [Built-in UI](/docs/advanced/built-in-ui) | `buildUiRouter()` | Server-rendered login/register pages — activation, CSS variables, full customization guide | | [CORS & Dynamic siteUrl](/docs/advanced/cors) | `AuthConfig.cors` | Cross-domain setups and per-caller `siteUrl` resolution for emails and redirects | | [Extending Interfaces](/docs/advanced/extending-interfaces) | `BaseUser`, `BaseTenant`, `Session` | Add custom fields to the base types with full TypeScript safety | --- ## Built-in Auth UI: Login Pages Without a Frontend ## Built-in UI — Activation & CSS Customization `awesome-node-auth` ships a **zero-dependency, server-rendered HTML UI** for all authentication flows. No React, no bundler, no build step required — the server injects your theme and configuration at request time. --- ### Pages included | Page | Path | Description | |------|------|-------------| | Login | `{apiPrefix}/ui/login` | Email/password + OAuth + magic link + SMS | | Register | `{apiPrefix}/ui/register` | New account form (only when `onRegister` is set) | | Forgot Password | `{apiPrefix}/ui/forgot-password` | Password reset request form | | Reset Password | `{apiPrefix}/ui/reset-password` | New password form (via email token) | | Magic Link | `{apiPrefix}/ui/magic-link` | Passwordless email login | | Verify Email | `{apiPrefix}/ui/verify-email` | Email verification landing page | | 2FA Challenge | `{apiPrefix}/ui/2fa` | TOTP / SMS one-time password entry | | Link Verify | `{apiPrefix}/ui/link-verify` | OAuth account-linking confirmation | | Account Conflict | `{apiPrefix}/ui/account-conflict` | OAuth provider/email conflict resolution | --- ### Step 1 — Install the UI router `buildUiRouter` is separate from the auth API router. Mount both under the same prefix: ```typescript import express from 'express'; import { AuthConfigurator, buildUiRouter } from '@awesome-lang-auth/node'; import { MyUserStore } from './my-user-store'; const app = express(); app.use(express.json()); const config: AuthConfig = { // ... JWT secrets & expiry ui: { loginUrl: '/auth/ui/login' } }; const userStore = new MyUserStore(); const auth = new AuthConfigurator(config, userStore); const routerOptions = { // onRegister, oauthStrategies, etc. }; // 1. Mount the REST API router app.use('/auth', auth.router(routerOptions)); // 2. Mount the UI router at the same prefix + /ui app.use( '/auth/ui', buildUiRouter({ authConfig: config, routerOptions: routerOptions, // enables register/OAuth flags apiPrefix: '/auth', }) ); app.listen(3000, () => console.log('Running on http://localhost:3000')); ``` The built-in UI is now available at: - **`http://localhost:3000/auth/ui/login`** - **`http://localhost:3000/auth/ui/register`** - etc. --- ### Step 2 — Configure `AuthConfig.ui` All UI settings live under `authConfig.ui`. Every option is optional. ```typescript const auth = new AuthConfigurator( { // ... JWT secrets & expiry ... ui: { loginUrl: '/auth/ui/login', registerUrl: '/auth/ui/register', // default siteName: 'My App', customLogo: 'https://cdn.example.com/logo.png', primaryColor: '#6366f1', // indigo secondaryColor: '#64748b', bgColor: '#f0f4ff', cardBg: '#ffffff', bgImage: 'https://cdn.example.com/bg.jpg', // Raw CSS injected into every page — override any rule or variable customCss: ` :root { --primary-color: #6366f1; } .auth-container { border-radius: 20px; } `, }, }, userStore ); ``` #### `AuthConfig.ui` options | Option | Type | Default | Description | |--------|------|---------|-------------| | `loginUrl` | `string` | `{apiPrefix}/ui/login` | Login page URL (used for redirects) | | `registerUrl` | `string` | `{apiPrefix}/ui/register` | Register page URL | | `siteName` | `string` | `'Awesome Node Auth'` | Page title and `

` heading | | `customLogo` | `string` | — | URL of a logo image (displayed above the form) | | `primaryColor` | `string` | `#4a90d9` | Main brand colour (buttons, headings, focus rings) | | `secondaryColor` | `string` | `#6c757d` | Secondary colour (OAuth buttons, dividers) | | `bgColor` | `string` | `#f8fafc` | Page background colour | | `bgImage` | `string` | — | Page background image URL (full-cover) | | `cardBg` | `string` | `#ffffff` | Form/card background colour | | `customCss` | `string` | — | Raw CSS injected into every page `