@easyweb/authentication 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,32 @@ Companion to [`@easyweb/events`](https://www.npmjs.com/package/@easyweb/events)
7
7
  messaging, and [`@easyweb/errors`](https://www.npmjs.com/package/@easyweb/errors) for
8
8
  the HTTP error taxonomy.
9
9
 
10
+ ## 2.1.0 — `SUPER_ADMIN` now satisfies `authorize(["ADMIN"])`
11
+
12
+ `authorize()` was an exact string match. Every admin route in the platform is written
13
+ `authorize(["ADMIN"])`, and auth-service's `admin-seed.ts` grants the bootstrap operator
14
+ `SUPER_ADMIN` by default — so the seeded account could not call a single admin route on
15
+ the platform.
16
+
17
+ `SUPER_ADMIN` now implies `ADMIN`. Nothing else implies anything: `ADMIN` deliberately
18
+ does **not** imply `USER`, because that edge would silently widen every future customer
19
+ route to staff.
20
+
21
+ **Additive, and widening only.** A service still on 2.0.0 behaves exactly as it did —
22
+ `SUPER_ADMIN` simply stays locked out there — so this needs no coordinated deploy. Since
23
+ every service declares `^2.0.0` but installs from a lockfile, pick it up with:
24
+
25
+ ```bash
26
+ npm i @easyweb/authentication@^2.1.0
27
+ ```
28
+
29
+ The new `effectiveRoles(roles)` export does the same expansion for a service with its own
30
+ guard (auth-service's DB-backed middleware, in particular).
31
+
32
+ The status is unchanged: `authorize()` still throws `NotAuthorizedError`, which is **401**
33
+ even for a signed-in caller with the wrong role. Every service's http suite pins that.
34
+ A caller needing to tell "not signed in" from "not permitted" must check the role itself.
35
+
10
36
  ## Upgrading to 2.0.0 — BREAKING
11
37
 
12
38
  The error classes moved to `@easyweb/errors`. Error handling is cross-cutting and has
@@ -101,6 +127,8 @@ would deny every request.
101
127
  | `createJwtSigner(config)` | `(claims) => string` |
102
128
  | `extractToken(req)` | Bearer header, then the `accessToken` cookie |
103
129
  | `isValidAccessPayload(decoded)` | type guard: is this a well-formed *access* token? |
130
+ | `effectiveRoles(roles)` | the roles held, expanded through the implication map (2.1.0) |
131
+ | `effectiveRoles(roles)` | the roles held, expanded through the implication map (2.1.0) |
104
132
  | `JwtAccessPayload`, `JwtAccessClaims`, `JwtVerifyConfig`, `JwtSignConfig` | types |
105
133
 
106
134
  `extractToken` and `isValidAccessPayload` exist for services that must keep a DB-backed
@@ -57,12 +57,25 @@ export declare function extractToken(req: Request): string | null;
57
57
  * needs the same guard but reports failure with its own message.
58
58
  */
59
59
  export declare function isValidAccessPayload(decoded: JwtAccessPayload | null | undefined): decoded is JwtAccessPayload;
60
+ /**
61
+ * The roles a caller holds, plus everything those roles stand in for.
62
+ *
63
+ * Exported for tests and for any service that needs the same expansion outside
64
+ * a middleware — auth-service's own DB-backed guard, in particular, which does
65
+ * not go through `createAuthMiddleware`.
66
+ */
67
+ export declare function effectiveRoles(roles: readonly string[]): Set<string>;
60
68
  export type AuthMiddleware = {
61
69
  /** Rejects with 401 unless the request carries a valid access token. */
62
70
  authenticate: (req: Request, res: Response, next: NextFunction) => void;
63
71
  /** Populates `req.user` when a valid token is present; never rejects. */
64
72
  optionalAuth: (req: Request, res: Response, next: NextFunction) => void;
65
- /** Role guard. Use after `authenticate`. */
73
+ /**
74
+ * Role guard. Use after `authenticate`.
75
+ *
76
+ * Matches against the caller's roles expanded through `ROLE_IMPLIES`, so
77
+ * `authorize(["ADMIN"])` admits `SUPER_ADMIN`.
78
+ */
66
79
  authorize: (allowedRoles: string[]) => (req: Request, res: Response, next: NextFunction) => void;
67
80
  };
68
81
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"authenticate.d.ts","sourceRoot":"","sources":["../../src/middleware/authenticate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEtE;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE;gBACL,EAAE,EAAE,MAAM,CAAC;gBACX,KAAK,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;gBACxB,KAAK,EAAE,MAAM,EAAE,CAAC;gBAChB,WAAW,EAAE,MAAM,EAAE,CAAC;aACvB,CAAC;YACF,IAAI,CAAC,EAAE;gBACL,MAAM,EAAE,MAAM,CAAC;gBACf,SAAS,CAAC,EAAE,MAAM,CAAC;aACpB,CAAC;SACH;KACF;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAC3C,OAAO,IAAI,gBAAgB,CAQ7B;AAqBD,MAAM,MAAM,cAAc,GAAG;IAC3B,wEAAwE;IACxE,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxE,yEAAyE;IACzE,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxE,4CAA4C;IAC5C,SAAS,EAAE,CACT,YAAY,EAAE,MAAM,EAAE,KACnB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;CAChE,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,CAsE5E"}
1
+ {"version":3,"file":"authenticate.d.ts","sourceRoot":"","sources":["../../src/middleware/authenticate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEtE;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE;gBACL,EAAE,EAAE,MAAM,CAAC;gBACX,KAAK,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;gBACxB,KAAK,EAAE,MAAM,EAAE,CAAC;gBAChB,WAAW,EAAE,MAAM,EAAE,CAAC;aACvB,CAAC;YACF,IAAI,CAAC,EAAE;gBACL,MAAM,EAAE,MAAM,CAAC;gBACf,SAAS,CAAC,EAAE,MAAM,CAAC;aACpB,CAAC;SACH;KACF;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAC3C,OAAO,IAAI,gBAAgB,CAQ7B;AAsBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAUpE;AAoCD,MAAM,MAAM,cAAc,GAAG;IAC3B,wEAAwE;IACxE,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxE,yEAAyE;IACzE,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxE;;;;;OAKG;IACH,SAAS,EAAE,CACT,YAAY,EAAE,MAAM,EAAE,KACnB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;CAChE,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,CA6E5E"}
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractToken = extractToken;
4
4
  exports.isValidAccessPayload = isValidAccessPayload;
5
+ exports.effectiveRoles = effectiveRoles;
5
6
  exports.createAuthMiddleware = createAuthMiddleware;
7
+ const logging_1 = require("@easyweb/logging");
6
8
  const verify_1 = require("../jwt/verify");
7
9
  const errors_1 = require("@easyweb/errors");
8
10
  /**
@@ -42,6 +44,41 @@ function isValidAccessPayload(decoded) {
42
44
  decoded.sessionId &&
43
45
  decoded.iat);
44
46
  }
47
+ /**
48
+ * Which roles a role stands in for.
49
+ *
50
+ * `authorize()` was an exact string match, and every admin route in the
51
+ * platform is written `authorize(["ADMIN"])` — so `SUPER_ADMIN`, which
52
+ * auth-service's `admin-seed.ts` grants the bootstrap operator by default,
53
+ * could not call a single one of them. The seeded account was locked out of
54
+ * the surface it exists to reach.
55
+ *
56
+ * Deliberately ONE entry. `ADMIN` does not imply `USER`: nothing calls
57
+ * `authorize(["USER"])` today, and adding that edge would silently widen every
58
+ * future customer route to staff — the kind of grant nobody re-reads.
59
+ *
60
+ * Widening only, which is what makes the 2.1.0 release safe to roll out one
61
+ * service at a time: a service still on 2.0.0 keeps behaving exactly as it did.
62
+ */
63
+ const ROLE_IMPLIES = {
64
+ SUPER_ADMIN: ["ADMIN"],
65
+ };
66
+ /**
67
+ * The roles a caller holds, plus everything those roles stand in for.
68
+ *
69
+ * Exported for tests and for any service that needs the same expansion outside
70
+ * a middleware — auth-service's own DB-backed guard, in particular, which does
71
+ * not go through `createAuthMiddleware`.
72
+ */
73
+ function effectiveRoles(roles) {
74
+ const effective = new Set(roles);
75
+ for (const role of roles) {
76
+ for (const implied of ROLE_IMPLIES[role] ?? []) {
77
+ effective.add(implied);
78
+ }
79
+ }
80
+ return effective;
81
+ }
45
82
  function attachClaims(req, decoded) {
46
83
  req.user = {
47
84
  id: decoded.sub,
@@ -58,6 +95,20 @@ function attachClaims(req, decoded) {
58
95
  userId: decoded.sub,
59
96
  sessionId: decoded.sessionId,
60
97
  };
98
+ /**
99
+ * Puts the caller on every log line for the rest of the request, including
100
+ * ones emitted by code that was never handed a `log` parameter.
101
+ *
102
+ * `requestContext` opened the scope before any token had been read, so this
103
+ * is a mutation of a scope already running rather than a new one — which is
104
+ * also what makes it visible to the access log, whose finish callback
105
+ * captured the store by reference several middlewares ago.
106
+ *
107
+ * Roles are deliberately not bound: they are an array that would be
108
+ * serialized onto every single line, and `authorize` failures already name
109
+ * them.
110
+ */
111
+ (0, logging_1.bindContext)({ userId: decoded.sub, sessionId: decoded.sessionId });
61
112
  }
62
113
  /**
63
114
  * Binds the JWT config once at startup and returns the middleware trio. The
@@ -102,10 +153,17 @@ function createAuthMiddleware(config) {
102
153
  }
103
154
  next();
104
155
  };
156
+ /**
157
+ * NOTE the status: `NotAuthorizedError` is 401, so a signed-in caller with
158
+ * the wrong role gets the same answer as an anonymous one. That is a quirk
159
+ * of this package rather than a decision, and every service's http tests pin
160
+ * it as-is — changing it here would turn eight suites red at once. A caller
161
+ * that needs to tell the two apart should check the role itself.
162
+ */
105
163
  const authorize = (allowedRoles) => {
106
164
  return (req, _res, next) => {
107
- const roles = req.user?.roles ?? [];
108
- if (!roles.some((r) => allowedRoles.includes(r))) {
165
+ const held = effectiveRoles(req.user?.roles ?? []);
166
+ if (!allowedRoles.some((r) => held.has(r))) {
109
167
  throw new errors_1.NotAuthorizedError("Insufficient role");
110
168
  }
111
169
  next();
@@ -1 +1 @@
1
- {"version":3,"file":"authenticate.js","sourceRoot":"","sources":["../../src/middleware/authenticate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"authenticate.js","sourceRoot":"","sources":["../../src/middleware/authenticate.ts"],"names":[],"mappings":"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"}
@@ -8,8 +8,12 @@ declare global {
8
8
  }
9
9
  /**
10
10
  * Picks up x-request-id from incoming headers (for inter-service forwarding),
11
- * or generates a new UUID when the request originates externally.
12
- * Always echoes the final value back in the response header.
11
+ * or generates a new UUID when the request originates externally. Always echoes
12
+ * the final value back in the response header.
13
+ *
14
+ * It also opens the AsyncLocalStorage scope every log line in the request reads
15
+ * from, which is why it must stay the FIRST middleware: anything mounted above
16
+ * it logs outside the scope and correlates with nothing.
13
17
  */
14
18
  export declare function requestContext(req: Request, res: Response, next: NextFunction): void;
15
19
  //# sourceMappingURL=request-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/middleware/request-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,SAAS,EAAE,MAAM,CAAC;SACnB;KACF;CACF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,QAYnB"}
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/middleware/request-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,SAAS,EAAE,MAAM,CAAC;SACnB;KACF;CACF;AAsBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,QAYnB"}
@@ -2,18 +2,42 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requestContext = requestContext;
4
4
  const crypto_1 = require("crypto");
5
+ const logging_1 = require("@easyweb/logging");
6
+ /**
7
+ * What we are willing to accept as somebody else's request id.
8
+ *
9
+ * The header is attacker-controlled. `k8s/base/ingress.yaml` publishes every
10
+ * service at `path: /`, so anything on the internet can set `x-request-id`, and
11
+ * this middleware used to take any non-empty string. That value then reaches
12
+ * every log line the request produces, the error envelope the customer is
13
+ * shown, and — once logs are aggregated — a field operators join traces on.
14
+ *
15
+ * Two things it buys. A newline in a log field splits one JSON record into two,
16
+ * so an attacker could forge log lines; and a caller pinning one fixed value
17
+ * collapses every one of their requests onto a single trace, which is the field
18
+ * being denied rather than abused.
19
+ *
20
+ * Wide enough for a UUID from a sibling service and for a short opaque token
21
+ * from the browser. Anything else is replaced rather than rejected — a bad
22
+ * header is not worth failing a request over.
23
+ */
24
+ const VALID_REQUEST_ID = /^[A-Za-z0-9_-]{1,64}$/;
5
25
  /**
6
26
  * Picks up x-request-id from incoming headers (for inter-service forwarding),
7
- * or generates a new UUID when the request originates externally.
8
- * Always echoes the final value back in the response header.
27
+ * or generates a new UUID when the request originates externally. Always echoes
28
+ * the final value back in the response header.
29
+ *
30
+ * It also opens the AsyncLocalStorage scope every log line in the request reads
31
+ * from, which is why it must stay the FIRST middleware: anything mounted above
32
+ * it logs outside the scope and correlates with nothing.
9
33
  */
10
34
  function requestContext(req, res, next) {
11
35
  const incoming = req.headers["x-request-id"];
12
- const requestId = typeof incoming === "string" && incoming.length > 0
36
+ const requestId = typeof incoming === "string" && VALID_REQUEST_ID.test(incoming)
13
37
  ? incoming
14
38
  : (0, crypto_1.randomUUID)();
15
39
  req.requestId = requestId;
16
40
  res.setHeader("x-request-id", requestId);
17
- next();
41
+ (0, logging_1.runWithContext)({ requestId }, () => next());
18
42
  }
19
43
  //# sourceMappingURL=request-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.js","sourceRoot":"","sources":["../../src/middleware/request-context.ts"],"names":[],"mappings":";;AAgBA,wCAeC;AA9BD,mCAAoC;AAUpC;;;;GAIG;AACH,SAAgB,cAAc,CAC5B,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7C,MAAM,SAAS,GACb,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QACjD,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,IAAA,mBAAU,GAAE,CAAC;IAEnB,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;IAC1B,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IAEzC,IAAI,EAAE,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"request-context.js","sourceRoot":"","sources":["../../src/middleware/request-context.ts"],"names":[],"mappings":";;AAyCA,wCAeC;AAvDD,mCAAoC;AACpC,8CAAkD;AAUlD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAEjD;;;;;;;;GAQG;AACH,SAAgB,cAAc,CAC5B,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7C,MAAM,SAAS,GACb,OAAO,QAAQ,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC7D,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,IAAA,mBAAU,GAAE,CAAC;IAEnB,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;IAC1B,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IAEzC,IAAA,wBAAc,EAAC,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAC9C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyweb/authentication",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Shared authentication primitives for Easyweb microservices: stateless JWT middleware, request context, and token verification helpers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,7 +19,10 @@
19
19
  "build": "npm run clean && tsc",
20
20
  "build:watch": "tsc --watch",
21
21
  "prepublishOnly": "npm run build",
22
- "clean": "rimraf dist"
22
+ "clean": "rimraf dist",
23
+ "test": "jest",
24
+ "typecheck": "tsc --noEmit",
25
+ "typecheck:test": "tsc -p tsconfig.test.json --noEmit"
23
26
  },
24
27
  "keywords": [
25
28
  "microservices",
@@ -37,14 +40,22 @@
37
40
  },
38
41
  "peerDependencies": {
39
42
  "@easyweb/errors": "^1.0.0",
43
+ "@easyweb/logging": "^1.0.0",
40
44
  "express": "^5.0.0",
41
45
  "jsonwebtoken": "^9.0.0"
42
46
  },
43
47
  "devDependencies": {
44
48
  "@easyweb/errors": "^1.0.0",
49
+ "@easyweb/logging": "^1.0.0",
50
+ "@swc/core": "^1.15.47",
51
+ "@swc/jest": "^0.2.39",
45
52
  "@types/express": "^5.0.6",
53
+ "@types/jest": "^30.0.0",
46
54
  "@types/jsonwebtoken": "^9.0.10",
47
55
  "@types/node": "^26.0.1",
56
+ "jest": "^30.4.2",
57
+ "jsonwebtoken": "^9.0.3",
58
+ "pino": "^10.3.1",
48
59
  "rimraf": "^6.1.3",
49
60
  "typescript": "^6.0.3"
50
61
  },