Authentication (JWT)¶
The auth module ships signup/login/refresh, JWT middleware and role guards —
decoupled from the ORM via a UserStore interface.
Mounting the auth router¶
import {
JWTUtils,
PasswordUtils,
UserAuthService,
type AuthUser,
type UserStore,
createApp,
createOpenApiRegistry,
makeAuthRouter,
runServer,
} from "tempest-express-sdk";
// 1. Implement UserStore over your database (here, in memory).
class MemoryStore implements UserStore {
private users = new Map<string, AuthUser>();
private seq = 0;
async findByEmail(email: string) {
return [...this.users.values()].find((u) => u.email === email) ?? null;
}
async findById(id: string) {
return this.users.get(id) ?? null;
}
async create(data: { email: string; passwordHash: string; name: string | null }) {
this.seq += 1;
const user: AuthUser = {
id: `user-${this.seq}`,
...data,
isActive: true,
roles: ["user"],
};
this.users.set(user.id, user);
return user;
}
}
// 2. Compose the service.
const jwt = new JWTUtils(process.env.JWT_SECRET ?? "dev-secret");
const service = new UserAuthService({
store: new MemoryStore(),
password: new PasswordUtils(),
jwt,
passwordMinLength: 12,
});
// 3. Mount the router.
const registry = createOpenApiRegistry();
const app = await createApp({
openapi: { registry, info: { title: "Auth", version: "1.0.0" } },
configure: (a) => {
a.use(makeAuthRouter({ service, jwt, registry }));
},
});
await runServer(app, { port: 8000 });
Exposed endpoints: POST /auth/signup, POST /auth/login,
POST /auth/refresh and GET /auth/me (protected).
Protecting routes¶
import { makeJwtAuthMiddleware, requireRoles } from "tempest-express-sdk";
const auth = makeJwtAuthMiddleware(jwt);
app.get("/api/admin", auth, requireRoles("admin"), (req, res) => {
res.json({ you: req.auth });
});
makeJwtAuthMiddleware(jwt)decodes theBearertoken and populatesreq.auth.requireRoles("admin")requires the role; otherwise → 403.- Missing/invalid token → 401 in the canonical envelope.
Soft auth
Use makeJwtAuthMiddleware(jwt, { required: false }) to populate req.auth
when a token is present, without rejecting anonymous callers.
Flows: MFA, activation and password reset¶
Pass optional services to makeAuthRouter to mount the flows. Each uses a
dedicated store (implement only the ones you use).
import {
ActivationService,
MfaService,
PasswordResetService,
TOTPHelper,
makeAuthRouter,
} from "tempest-express-sdk";
a.use(
makeAuthRouter({
service,
jwt,
activation: new ActivationService({ store }), // POST /auth/activate
passwordReset: new PasswordResetService({ store, password }), // /auth/password-reset/*
mfa: new MfaService({ store, totp: new TOTPHelper({ issuer: "My App" }) }),
}),
);
Mounted routes:
| Route | Description |
|---|---|
POST /auth/activate |
{ token } → activate the account |
POST /auth/password-reset/request |
{ email } → always 202 (no enumeration) |
POST /auth/password-reset/confirm |
{ token, password } → set the new password |
POST /auth/mfa/enroll |
guarded (JWT) → { secret, otpauthUri } (QR) |
POST /auth/mfa/confirm |
{ code } → enable MFA |
POST /auth/mfa/disable |
{ code } → disable MFA |
- Opaque tokens: activation/reset store only the SHA-256 hash; the plaintext travels in the emailed link. Invalid/expired token → 401.
- MFA: native
TOTPHelper(RFC 6238).enrollgenerates the secret + QR URI;confirmverifies a code and turns MFA on; a wrong code → 422. - MFA at login (challenge): also pass
mfatoUserAuthService. For users with MFA enabled,POST /auth/loginreturns{ mfaRequired: true, mfaToken }(no tokens); the client completes it withPOST /auth/mfa/challenge { mfaToken, code }→ tokens. An invalid code/token → 401. Without MFA enabled, login returns tokens directly. - Anti-enumeration:
password-reset/requestalways returns 202; thetokenis echoed only in dev setups (in production the service emails it).
HTML pages (optional)¶
The SDK favors the JSON API + a decoupled frontend, but an email link
(activation, reset) sometimes has to land on a server page — there's no SPA
to route to. renderAuthResultPage and renderPasswordResetFormPage produce
self-contained, theme-aware HTML pages (no template engine, no external assets):
import { renderAuthResultPage, renderPasswordResetFormPage } from "tempest-express-sdk";
app.get("/activate", async (req, res) => {
const ok = await activation.activate(String(req.query.token)).then(() => true).catch(() => false);
res.type("html").send(
renderAuthResultPage({
ok,
title: ok ? "Account activated" : "Invalid link",
message: ok ? "You can sign in now." : "The link expired or was already used.",
...(ok ? { cta: { href: "https://app/login", label: "Sign in" } } : {}),
}),
);
});
app.get("/reset", (req, res) => {
res.type("html").send(
renderPasswordResetFormPage({ action: "/auth/password-reset/confirm", token: String(req.query.token) }),
);
});
All interpolated values are escaped (anti-XSS).
Recap¶
UserStore decouples auth from your database; UserAuthService handles hashing
and tokens; the middleware guards routes by role; the MFA/activation/reset
services mount the full flows over dedicated stores. Optional HTML pages cover
email landings. Everything shows up in Swagger/Redoc.