@mantlejs/auth-local
Local (email + password) authentication strategy for Mantle JS. Built on top of @mantlejs/auth. Passwords are hashed with Argon2id via @node-rs/argon2 — the OWASP-recommended algorithm.
Installation
Section titled “Installation”npm install @mantlejs/auth-local @mantlejs/auth @node-rs/argon2 jsonwebtokenConcepts
Section titled “Concepts”localStrategy()
Section titled “localStrategy()”A Mantle plugin that registers a "local" strategy with the auth engine. When POST /authentication is called with { strategy: "local", email, password }, it:
- Queries the entity service (default:
"users") for a record matching theusernameField - Verifies the submitted password against the stored Argon2id hash
- Issues an access + refresh token pair via
engine.createTokenPair(sub) - Returns
{ accessToken, refreshToken, user }
The lookup is an internal call — it bypasses any authenticate("jwt") hooks on the users service.
hashPassword()
Section titled “hashPassword()”A before hook that replaces a plain-text password field with its Argon2id hash. Run it on create (and patch/update if your app allows password changes) before the record reaches the database.
Quick start
Section titled “Quick start”import { mantle } from "@mantlejs/mantle";import { express } from "@mantlejs/express";import { knex } from "@mantlejs/knex";import { auth, authenticate, sanitizeUser } from "@mantlejs/auth";import { localStrategy, hashPassword } from "@mantlejs/auth-local";
const app = mantle() .configure(express()) .configure(knex({ client: "pg", connection: process.env.DATABASE_URL })) .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()], // hash on registration patch: [authenticate("jwt"), hashPassword()], // protect + re-hash on update find: [authenticate("jwt")], get: [authenticate("jwt")], remove: [authenticate("jwt")], }, after: { all: [sanitizeUser()], // strip password from all responses },});
app.listen(3030);Register a user
Section titled “Register a user”POST /usersContent-Type: application/json
{ "email": "alice@example.com", "password": "hunter2" }The hashPassword() hook runs first, so "hunter2" is stored as an Argon2id hash.
{ "id": 1, "email": "alice@example.com" }Log in
Section titled “Log in”POST /authenticationContent-Type: application/json
{ "strategy": "local", "email": "alice@example.com", "password": "hunter2" }{ "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "user": { "id": 1, "email": "alice@example.com" }}Store accessToken client-side and include it in subsequent requests:
GET /usersAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9...localStrategy(config?)
Section titled “localStrategy(config?)”Returns a MantlePlugin. Must be called after auth().
app.configure(localStrategy({ usernameField: "email", // field in the request body to match against (default: "email") passwordField: "password", // field in the request body containing the plain-text password (default: "password") entityService: "users", // Mantle service name used to look up the entity (default: "users")}));LocalStrategyConfig
Section titled “LocalStrategyConfig”| Field | Type | Default | Description |
|---|---|---|---|
usernameField |
string |
"email" |
The field used to look up the user record. Must match the column name in your entity service. |
passwordField |
string |
"password" |
The field containing the submitted plain-text password. Also used to locate the stored hash in the user record. |
entityService |
string |
"users" |
Name of the registered Mantle service that holds user records. |
hashPassword(field?)
Section titled “hashPassword(field?)”A before hook that reads context.data[field], hashes it with Argon2id, and writes the hash back in place.
// Default: hashes context.data.passwordapp.service("users").hooks({ before: { create: [hashPassword()] },});
// Custom field nameapp.service("accounts").hooks({ before: { create: [hashPassword("passphrase")] },});The hook is a no-op when:
context.dataisundefined- The target field is absent from
context.data - The target field value is not a string
Plugin order
Section titled “Plugin order”localStrategy() must be configured after auth() because it calls app.get("auth") at registration time to grab the engine and register the strategy:
// Correctapp .configure(auth({ secret })) .configure(localStrategy());
// Wrong — throws "@mantlejs/auth must be configured before @mantlejs/auth-local"app .configure(localStrategy()) .configure(auth({ secret }));Custom username field
Section titled “Custom username field”Use any unique field as the login identifier:
app.configure(localStrategy({ usernameField: "username" }));The strategy will query app.service("users").find({ query: { username: value } }).
Multiple user entity services
Section titled “Multiple user entity services”If you have more than one kind of entity that can authenticate (e.g. users and admins), register multiple strategy instances under different names by implementing a custom strategy using the AuthEngine directly. localStrategy always registers as "local", so two instances would overwrite each other.
Security notes
Section titled “Security notes”- Argon2id is used instead of bcrypt. Argon2id is the winner of the Password Hashing Competition and is OWASP-recommended. Unlike bcrypt, it has no 72-character password truncation limit.
- All authentication failures return the same
"Invalid credentials"message regardless of whether the user was not found or the password was wrong. This prevents user enumeration. - The
sanitizeUser()hook (from@mantlejs/auth) strips thepasswordandpasswordHashfields from all service results. Always apply it on theafter: allphase of your entity service.
Error reference
Section titled “Error reference”| Error | Code | When thrown |
|---|---|---|
NotAuthenticated |
401 | User not found |
NotAuthenticated |
401 | Password does not match stored hash |
NotAuthenticated |
401 | Credentials missing from request body |
Error |
— | localStrategy() called before auth() (config-time failure) |
Development
Section titled “Development”npx nx build auth-local # compilenpx nx test auth-local # run testsnpx nx lint auth-local # lintPublishing
Section titled “Publishing”Build before publishing:
npx nx build auth-localFirst publish (scoped packages require --access public):
cd packages/auth-localnpm publish --access publicSubsequent releases — bump version in packages/auth-local/package.json, then:
cd packages/auth-localnpm publishTesting locally with Verdaccio
Section titled “Testing locally with Verdaccio”# Terminal 1 — start the local registrynpx nx run @mantle/source:local-registry
# Terminal 2 — publish to itcd packages/auth-localnpm publish --registry http://localhost:4873
# Install from it in another projectnpm install @mantlejs/auth-local --registry http://localhost:4873