@easyweb/authentication
v2.2.0
Published
Shared authentication primitives for Easyweb microservices: stateless JWT middleware, request context, and token verification helpers
Readme
@easyweb/authentication
Shared authentication primitives for Easyweb microservices — stateless JWT middleware, request-context middleware, and the token verification helpers services build on.
Companion to @easyweb/events for
messaging, and @easyweb/errors for
the HTTP error taxonomy.
2.1.0 — SUPER_ADMIN now satisfies authorize(["ADMIN"])
authorize() was an exact string match. Every admin route in the platform is written
authorize(["ADMIN"]), and auth-service's admin-seed.ts grants the bootstrap operator
SUPER_ADMIN by default — so the seeded account could not call a single admin route on
the platform.
SUPER_ADMIN now implies ADMIN. Nothing else implies anything: ADMIN deliberately
does not imply USER, because that edge would silently widen every future customer
route to staff.
Additive, and widening only. A service still on 2.0.0 behaves exactly as it did —
SUPER_ADMIN simply stays locked out there — so this needs no coordinated deploy. Since
every service declares ^2.0.0 but installs from a lockfile, pick it up with:
npm i @easyweb/authentication@^2.1.0The new effectiveRoles(roles) export does the same expansion for a service with its own
guard (auth-service's DB-backed middleware, in particular).
The status is unchanged: authorize() still throws NotAuthorizedError, which is 401
even for a signed-in caller with the wrong role. Every service's http suite pins that.
A caller needing to tell "not signed in" from "not permitted" must check the role itself.
Upgrading to 2.0.0 — BREAKING
The error classes moved to @easyweb/errors. Error handling is cross-cutting and has
nothing to do with JWTs; a service needing ConflictError should not have to depend on
an auth package for it.
- import { NotAuthorizedError } from "@easyweb/authentication";
+ import { NotAuthorizedError } from "@easyweb/errors";@easyweb/errors is a peerDependency — install it alongside this package. Do not let
it end up nested under node_modules/@easyweb/authentication/: two copies of
CustomError means instanceof fails across the boundary and the error handler answers
500 for everything, with no type error to warn you. Verify with npm ls @easyweb/errors.
Also new in 2.0.0: extractToken and isValidAccessPayload are exported, for services
that keep their own DB-backed middleware (see below) and would otherwise copy them.
Install
npm install @easyweb/authenticationexpress@^5 and jsonwebtoken@^9 are peer dependencies — the consuming service
provides them.
Usage
Nothing in this package reads process.env. Config is bound once at startup, so the
package never has to reach into a service's config module.
import express from "express";
import cookieParser from "cookie-parser";
import { createAuthMiddleware, requestContext } from "@easyweb/authentication";
import config from "./config";
const app = express();
app.use(cookieParser()); // required: the middleware reads the accessToken cookie
app.use(requestContext);
const authMiddleware = createAuthMiddleware(config.jwt);
// config.jwt must supply { secret, issuer, audience } matching auth-service's
app.get("/me", authMiddleware.authenticate, (req, res) => {
res.json(req.user); // { id, email, username, roles, permissions }
});
app.get("/feed", authMiddleware.optionalAuth, (req, res) => {
res.json({ personalised: Boolean(req.user) });
});
app.delete(
"/admin/:id",
authMiddleware.authenticate,
authMiddleware.authorize(["admin"]),
handler,
);Minting tokens (auth-service only):
import { createJwtSigner } from "@easyweb/authentication";
const signAccessToken = createJwtSigner(config.jwt);
// config.jwt additionally needs accessTokenTtlSecondsWhat it verifies — and what it does not
authenticate is stateless. It checks the signature, issuer, audience, expiry, and
that the payload is a well-formed access token. It performs no database lookup.
A logout or session revoke in auth-service stays invisible here until the access token
expires (JWT_ACCESS_TOKEN_TTL_SECONDS, currently 300). If a service needs immediate
revocation, it must keep its own DB-backed middleware — auth-service does exactly that.
req.user.permissions is always []. The access token carries no permissions claim;
auth-service resolves those from its own tables at request time. Guarding on this array
would deny every request.
Exports
| Export | Purpose |
|---|---|
| createAuthMiddleware(config) | { authenticate, optionalAuth, authorize } |
| requestContext | propagates / generates x-request-id |
| createJwtVerifier(config) | (token) => JwtAccessPayload, throws on invalid |
| createJwtSigner(config) | (claims) => string |
| extractToken(req) | Bearer header, then the accessToken cookie |
| isValidAccessPayload(decoded) | type guard: is this a well-formed access token? |
| effectiveRoles(roles) | the roles held, expanded through the implication map (2.1.0) |
| effectiveRoles(roles) | the roles held, expanded through the implication map (2.1.0) |
| JwtAccessPayload, JwtAccessClaims, JwtVerifyConfig, JwtSignConfig | types |
extractToken and isValidAccessPayload exist for services that must keep a DB-backed
middleware. They were previously private, so auth-service copied them — and the copies
drifted. If you need the same checks with different error messages, import these rather
than reimplementing them.
Importing the package also augments Express.Request with user, auth, and
requestId. Remove any local copy of that declare global block or TypeScript will
report duplicate members.
Token contract
JwtAccessPayload is the only shape the services agree on. Changing it breaks every
consumer — bump the major version.
License
MIT
