Authentication
Authentication is split in two: @mantlejs/auth is the engine — JWT issuance and verification, the
POST /authentication endpoint, and the hooks — and strategy packages plug sign-in methods into it.
| Strategy | Sign-in method |
|---|---|
@mantlejs/auth-local |
Email + password, hashed with Argon2id |
@mantlejs/auth-google |
Google (authorization code + PKCE) |
@mantlejs/auth-github |
GitHub |
@mantlejs/auth-facebook |
|
@mantlejs/auth-apple |
Sign in with Apple (form_post callback) |
@mantlejs/auth-microsoft |
Microsoft Entra ID (PKCE) |
@mantlejs/auth-linkedin |
LinkedIn (OpenID Connect) |
@mantlejs/auth-twitter |
Sign in with X (PKCE) |
The OAuth strategies share one base, @mantlejs/auth-oauth — state handling, PKCE,
and find-or-create of the local user — and none of them uses Passport.js.
Local sign-in
Section titled “Local sign-in”import { mantle } from "@mantlejs/mantle";import { express } from "@mantlejs/express";import { auth, authenticate, sanitizeUser } from "@mantlejs/auth";import { localStrategy, hashPassword } from "@mantlejs/auth-local";
const app = mantle() .configure(express()) .configure(auth({ secret: process.env.JWT_SECRET! })) .configure(localStrategy());
app.use("users", new UserService(new UserRepository(app)), { methods: ["find", "get", "create", "patch", "remove"],});
app.service("users").hooks({ before: { create: [hashPassword()], // registration stays public; the password is stored as an Argon2id hash patch: [authenticate("jwt"), hashPassword()], find: [authenticate("jwt")], get: [authenticate("jwt")], remove: [authenticate("jwt")], }, after: { all: [sanitizeUser()] }, // never send password hashes back});The flow over HTTP:
POST /userswith{ "email", "password" }registers a user.POST /authenticationwith{ "strategy": "local", "email", "password" }returns an access token, a refresh token, and the user.- Requests carry
Authorization: Bearer <accessToken>;authenticate("jwt")verifies it and puts the decoded payload onparams.user.
Internal calls — a service calling another service without a provider — skip authenticate("jwt"), which is
how strategies look up users without tripping the guards they protect.
OAuth sign-in
Section titled “OAuth sign-in”import { googleStrategy } from "@mantlejs/auth-google";
app.configure( googleStrategy({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, redirectUrl: "https://app.example.com/", }),);Send the browser to GET /auth/google with a full-page navigation (a link, not fetch). After consent the
provider calls back, the strategy finds or creates the user, and — with redirectUrl set — redirects to your
frontend with #accessToken=…&refreshToken=… in the URL fragment, so tokens never reach a server log or a
Referer header. The auth-provider and oauth-buttons
UI blocks handle both ends of that for you.
Refresh tokens
Section titled “Refresh tokens”Every login returns an access + refresh token pair. Exchange the refresh token on the same endpoint:
POST /authentication{ "strategy": "refresh", "refreshToken": "eyJhbGciOiJIUzI1NiJ9..." }Refresh tokens rotate: each exchange consumes the token and issues a new pair. Replaying a consumed token is
treated as theft — every outstanding refresh token for that user is revoked. @mantlejs/client
does the exchange automatically on a 401, with concurrent requests sharing a single refresh.
Running more than one instance
Section titled “Running more than one instance”The engine’s default stores are in memory: fine for one process, wrong behind a load balancer, where a refresh
or an OAuth callback can land on a different instance than the one that issued the token or the state. Inject
the Redis-backed stores from @mantlejs/auth-redis — redisRefreshTokenStore into
auth({ refreshTokenStore }) and redisStateStore into each OAuth strategy’s stateStore option. Both consume entries atomically, so a
replayed callback or a double-submitted refresh can’t succeed twice.
Agents
Section titled “Agents”Calls from AI agents get their own short-lived, capability-scoped tokens — distinct from user JWTs and opt-in per route. See Agents.