Skip to content

@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.


Terminal window
npm install @mantlejs/auth-local @mantlejs/auth @node-rs/argon2 jsonwebtoken

A Mantle plugin that registers a "local" strategy with the auth engine. When POST /authentication is called with { strategy: "local", email, password }, it:

  1. Queries the entity service (default: "users") for a record matching the usernameField
  2. Verifies the submitted password against the stored Argon2id hash
  3. Issues an access + refresh token pair via engine.createTokenPair(sub)
  4. Returns { accessToken, refreshToken, user }

The lookup is an internal call — it bypasses any authenticate("jwt") hooks on the users service.

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.


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);

POST /users
Content-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" }
POST /authentication
Content-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 /users
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

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")
}));
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.

A before hook that reads context.data[field], hashes it with Argon2id, and writes the hash back in place.

// Default: hashes context.data.password
app.service("users").hooks({
before: { create: [hashPassword()] },
});
// Custom field name
app.service("accounts").hooks({
before: { create: [hashPassword("passphrase")] },
});

The hook is a no-op when:

  • context.data is undefined
  • The target field is absent from context.data
  • The target field value is not a string

localStrategy() must be configured after auth() because it calls app.get("auth") at registration time to grab the engine and register the strategy:

// Correct
app
.configure(auth({ secret }))
.configure(localStrategy());
// Wrong — throws "@mantlejs/auth must be configured before @mantlejs/auth-local"
app
.configure(localStrategy())
.configure(auth({ secret }));

Use any unique field as the login identifier:

app.configure(localStrategy({ usernameField: "username" }));

The strategy will query app.service("users").find({ query: { username: value } }).


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.


  • 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 the password and passwordHash fields from all service results. Always apply it on the after: all phase of your entity service.

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)

Terminal window
npx nx build auth-local # compile
npx nx test auth-local # run tests
npx nx lint auth-local # lint

Build before publishing:

Terminal window
npx nx build auth-local

First publish (scoped packages require --access public):

Terminal window
cd packages/auth-local
npm publish --access public

Subsequent releases — bump version in packages/auth-local/package.json, then:

Terminal window
cd packages/auth-local
npm publish
Terminal window
# Terminal 1 — start the local registry
npx nx run @mantle/source:local-registry
# Terminal 2 — publish to it
cd packages/auth-local
npm publish --registry http://localhost:4873
# Install from it in another project
npm install @mantlejs/auth-local --registry http://localhost:4873