@discover-cloud/shared 1.2.3 → 1.2.5

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.
@@ -4,3 +4,5 @@ export * from "./request-id.middleware";
4
4
  export * from "./authorize.middleware";
5
5
  export * from "./validated-merge.middleware";
6
6
  export * from "./require-internal.middleware";
7
+ export * from "./require-auth.middleware";
8
+ export * from "./require-human.middleware";
@@ -20,3 +20,5 @@ __exportStar(require("./request-id.middleware"), exports);
20
20
  __exportStar(require("./authorize.middleware"), exports);
21
21
  __exportStar(require("./validated-merge.middleware"), exports);
22
22
  __exportStar(require("./require-internal.middleware"), exports);
23
+ __exportStar(require("./require-auth.middleware"), exports);
24
+ __exportStar(require("./require-human.middleware"), exports);
@@ -0,0 +1,86 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { AccountRole } from "../enums";
3
+ import { GlobalPermission } from "../enums";
4
+ import { ILogger } from "../utils";
5
+ import { InternalJwtVerifier } from "../jwt/internal-jwt-verifier";
6
+ /**
7
+ * REQUIRE AUTH MIDDLEWARE (@discover-cloud/shared)
8
+ * ─────────────────────────────────────────────────────────────────────────
9
+ * Verifies the internal JWT, resolves permissions from the Redis cache,
10
+ * and attaches the typed AccessContext to req.accessContext.
11
+ *
12
+ * Moved to shared so every downstream service (auth, user, cloud, insights)
13
+ * uses the same verified implementation rather than maintaining their own copy.
14
+ *
15
+ * Token extraction:
16
+ * Reads a Bearer token from the Authorization header.
17
+ * Does not support cookie-based auth — that is the API gateway's responsibility.
18
+ *
19
+ * On success:
20
+ * req.internalAuth → raw verified JWT payload (use for jti, caller, requestId)
21
+ * req.accessContext → clean typed context (use in controllers and services)
22
+ *
23
+ * On failure:
24
+ * Returns a structured error response and does NOT call next().
25
+ * next(err) is only called for genuinely unexpected errors (not auth failures).
26
+ *
27
+ * Inline guards (requirePermission, requireRole):
28
+ * Convenience methods for single-route guards. For router-level permission
29
+ * enforcement, prefer the standalone factories in permission.middleware.ts —
30
+ * they don't require a reference to this class instance.
31
+ *
32
+ * Logging:
33
+ * Unexpected errors are logged at "error" level with requestId for correlation.
34
+ * Auth failures (expired, invalid token) are intentionally not logged at error
35
+ * level — they are client errors, not server faults.
36
+ *
37
+ * Usage in each service's app.ts:
38
+ * import { RequireAuthMiddleware } from "@discover-cloud/shared";
39
+ *
40
+ * const requireAuth = new RequireAuthMiddleware(jwtVerifier, logger);
41
+ * router.use(requireAuth.handle);
42
+ */
43
+ export declare class requireAuth {
44
+ private readonly verifier;
45
+ private readonly logger;
46
+ constructor(verifier: InternalJwtVerifier, logger?: ILogger);
47
+ /**
48
+ * Core authentication middleware.
49
+ * Apply via: router.use(requireAuth.handle) or router.get("/", requireAuth.handle, controller.x)
50
+ */
51
+ handle: (req: Request, res: Response, next: NextFunction) => Promise<void>;
52
+ /**
53
+ * Inline permission guard — use when this class instance is available at the call site.
54
+ * For router-level guards, prefer requireGlobalPermission() from permission.middleware.ts.
55
+ *
56
+ * Usage: router.get("/", requireAuth.handle, requireAuth.requirePermission(P.READ_ACCOUNTS), controller.list)
57
+ */
58
+ requirePermission: (permission: GlobalPermission) => (req: Request, res: Response, next: NextFunction) => void;
59
+ /**
60
+ * Inline role guard — use when this class instance is available at the call site.
61
+ *
62
+ * Prefer permission-based guards over role-based guards where possible.
63
+ * Role checks are appropriate for coarse access gates (e.g. "only admins enter this router").
64
+ */
65
+ requireRole: (role: AccountRole) => (req: Request, res: Response, next: NextFunction) => void;
66
+ /**
67
+ * Extracts the Bearer token from the Authorization header.
68
+ * Returns null (not an error) if the header is absent or malformed —
69
+ * callers decide how to handle the missing token.
70
+ */
71
+ private extractBearer;
72
+ /**
73
+ * Maps jose verification errors to appropriate HTTP responses.
74
+ *
75
+ * Error categories:
76
+ * JWTExpired → 401 TOKEN_EXPIRED (client should refresh)
77
+ * JWTClaimValidationFailed,
78
+ * JWSSignatureVerificationFailed,
79
+ * JWSInvalid, JWTInvalid → 401 INVALID_TOKEN (client should re-login)
80
+ * Anything else → 503 SERVICE_UNAVAILABLE (unexpected — log at error)
81
+ *
82
+ * Security note: generic "invalid token" messages are intentional.
83
+ * Never expose which specific claim failed — that aids token forgery attempts.
84
+ */
85
+ private handleJwtError;
86
+ }
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireAuth = void 0;
4
+ const jose_1 = require("jose");
5
+ const utils_1 = require("../utils");
6
+ const internal_jwt_verifier_1 = require("../jwt/internal-jwt-verifier");
7
+ const utils_2 = require("../utils");
8
+ /**
9
+ * REQUIRE AUTH MIDDLEWARE (@discover-cloud/shared)
10
+ * ─────────────────────────────────────────────────────────────────────────
11
+ * Verifies the internal JWT, resolves permissions from the Redis cache,
12
+ * and attaches the typed AccessContext to req.accessContext.
13
+ *
14
+ * Moved to shared so every downstream service (auth, user, cloud, insights)
15
+ * uses the same verified implementation rather than maintaining their own copy.
16
+ *
17
+ * Token extraction:
18
+ * Reads a Bearer token from the Authorization header.
19
+ * Does not support cookie-based auth — that is the API gateway's responsibility.
20
+ *
21
+ * On success:
22
+ * req.internalAuth → raw verified JWT payload (use for jti, caller, requestId)
23
+ * req.accessContext → clean typed context (use in controllers and services)
24
+ *
25
+ * On failure:
26
+ * Returns a structured error response and does NOT call next().
27
+ * next(err) is only called for genuinely unexpected errors (not auth failures).
28
+ *
29
+ * Inline guards (requirePermission, requireRole):
30
+ * Convenience methods for single-route guards. For router-level permission
31
+ * enforcement, prefer the standalone factories in permission.middleware.ts —
32
+ * they don't require a reference to this class instance.
33
+ *
34
+ * Logging:
35
+ * Unexpected errors are logged at "error" level with requestId for correlation.
36
+ * Auth failures (expired, invalid token) are intentionally not logged at error
37
+ * level — they are client errors, not server faults.
38
+ *
39
+ * Usage in each service's app.ts:
40
+ * import { RequireAuthMiddleware } from "@discover-cloud/shared";
41
+ *
42
+ * const requireAuth = new RequireAuthMiddleware(jwtVerifier, logger);
43
+ * router.use(requireAuth.handle);
44
+ */
45
+ class requireAuth {
46
+ constructor(verifier, logger = utils_1.noopLogger) {
47
+ this.verifier = verifier;
48
+ this.logger = logger;
49
+ /**
50
+ * Core authentication middleware.
51
+ * Apply via: router.use(requireAuth.handle) or router.get("/", requireAuth.handle, controller.x)
52
+ */
53
+ this.handle = async (req, res, next) => {
54
+ const token = this.extractBearer(req);
55
+ if (!token) {
56
+ (0, utils_2.failure)(res, req, "Missing Authorization header", "UNAUTHORIZED", 401);
57
+ return;
58
+ }
59
+ try {
60
+ const { payload, context } = await this.verifier.buildAccessContext(token);
61
+ req.internalAuth = payload;
62
+ req.accessContext = context;
63
+ this.logger.debug({
64
+ requestId: req.id,
65
+ // log accountId for humans, serviceId for machines — never log both
66
+ identity: context.kind === "human" ? context.accountId : context.serviceId,
67
+ kind: context.kind,
68
+ caller: payload.caller,
69
+ }, "Request authenticated");
70
+ next();
71
+ }
72
+ catch (err) {
73
+ this.handleJwtError(err, req, res);
74
+ }
75
+ };
76
+ /**
77
+ * Inline permission guard — use when this class instance is available at the call site.
78
+ * For router-level guards, prefer requireGlobalPermission() from permission.middleware.ts.
79
+ *
80
+ * Usage: router.get("/", requireAuth.handle, requireAuth.requirePermission(P.READ_ACCOUNTS), controller.list)
81
+ */
82
+ this.requirePermission = (permission) => (req, res, next) => {
83
+ const ctx = req.accessContext;
84
+ if (!ctx) {
85
+ // Should not happen after handle() — indicates middleware ordering bug
86
+ (0, utils_2.failure)(res, req, "No access context — ensure requireAuth runs first", "UNAUTHORIZED", 401);
87
+ return;
88
+ }
89
+ if (!internal_jwt_verifier_1.InternalJwtVerifier.hasPermission(ctx, permission)) {
90
+ (0, utils_2.failure)(res, req, `Missing required permission: ${permission}`, "FORBIDDEN", 403);
91
+ return;
92
+ }
93
+ next();
94
+ };
95
+ /**
96
+ * Inline role guard — use when this class instance is available at the call site.
97
+ *
98
+ * Prefer permission-based guards over role-based guards where possible.
99
+ * Role checks are appropriate for coarse access gates (e.g. "only admins enter this router").
100
+ */
101
+ this.requireRole = (role) => (req, res, next) => {
102
+ const ctx = req.accessContext;
103
+ if (!ctx) {
104
+ (0, utils_2.failure)(res, req, "No access context — ensure requireAuth runs first", "UNAUTHORIZED", 401);
105
+ return;
106
+ }
107
+ if (!internal_jwt_verifier_1.InternalJwtVerifier.hasRole(ctx, role)) {
108
+ (0, utils_2.failure)(res, req, `Missing required role: ${role}`, "FORBIDDEN", 403);
109
+ return;
110
+ }
111
+ next();
112
+ };
113
+ }
114
+ /* ────────────────────────────────────────────────────────────────────
115
+ PRIVATE HELPERS
116
+ ──────────────────────────────────────────────────────────────────── */
117
+ /**
118
+ * Extracts the Bearer token from the Authorization header.
119
+ * Returns null (not an error) if the header is absent or malformed —
120
+ * callers decide how to handle the missing token.
121
+ */
122
+ extractBearer(req) {
123
+ const header = req.headers.authorization;
124
+ if (!header) {
125
+ return null;
126
+ }
127
+ const [scheme, token] = header.trim().split(/\s+/);
128
+ if (scheme !== "Bearer" || !token) {
129
+ return null;
130
+ }
131
+ return token;
132
+ }
133
+ /**
134
+ * Maps jose verification errors to appropriate HTTP responses.
135
+ *
136
+ * Error categories:
137
+ * JWTExpired → 401 TOKEN_EXPIRED (client should refresh)
138
+ * JWTClaimValidationFailed,
139
+ * JWSSignatureVerificationFailed,
140
+ * JWSInvalid, JWTInvalid → 401 INVALID_TOKEN (client should re-login)
141
+ * Anything else → 503 SERVICE_UNAVAILABLE (unexpected — log at error)
142
+ *
143
+ * Security note: generic "invalid token" messages are intentional.
144
+ * Never expose which specific claim failed — that aids token forgery attempts.
145
+ */
146
+ handleJwtError(err, req, res) {
147
+ if (err instanceof jose_1.errors.JWTExpired) {
148
+ (0, utils_2.failure)(res, req, "Your session has expired. Please log in again.", "TOKEN_EXPIRED", 401);
149
+ return;
150
+ }
151
+ if (err instanceof jose_1.errors.JWTClaimValidationFailed ||
152
+ err instanceof jose_1.errors.JWSSignatureVerificationFailed ||
153
+ err instanceof jose_1.errors.JWSInvalid ||
154
+ err instanceof jose_1.errors.JWTInvalid) {
155
+ (0, utils_2.failure)(res, req, "Token is invalid or has been tampered with.", "INVALID_TOKEN", 401);
156
+ return;
157
+ }
158
+ // Unexpected error — log for investigation, return a safe 503
159
+ this.logger.error({ err, requestId: req.id }, "RequireAuthMiddleware: unexpected error during JWT verification");
160
+ (0, utils_2.failure)(res, req, "Authentication service is temporarily unavailable.", "SERVICE_UNAVAILABLE", 503);
161
+ }
162
+ }
163
+ exports.requireAuth = requireAuth;
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ export declare const requireHuman: (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireHuman = void 0;
4
+ const utils_1 = require("../utils");
5
+ const types_1 = require("../types");
6
+ const requireHuman = (req, res, next) => {
7
+ const ctx = req.accessContext;
8
+ if (!ctx) {
9
+ (0, utils_1.failure)(res, req, "Unauthorized: No access context", "UNAUTHORIZED", 401);
10
+ return;
11
+ }
12
+ if (!(0, types_1.isHumanContext)(ctx)) {
13
+ (0, utils_1.failure)(res, req, "Human token required", "FORBIDDEN", 403);
14
+ return;
15
+ }
16
+ next();
17
+ };
18
+ exports.requireHuman = requireHuman;
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.requireInternal = requireInternal;
37
37
  const jose = __importStar(require("jose"));
38
+ const types_1 = require("../types");
38
39
  const logger_utils_1 = require("../utils/logger.utils");
39
40
  /**
40
41
  * REQUIRE INTERNAL MIDDLEWARE (@discover-cloud/shared)
@@ -125,14 +126,20 @@ function requireInternal(options = {}) {
125
126
  });
126
127
  return;
127
128
  }
128
- const serviceId = payload["serviceId"];
129
- if (typeof serviceId !== "string") {
129
+ try {
130
+ (0, types_1.assertMachinePayload)(payload);
131
+ }
132
+ catch {
130
133
  res.status(401).json({
131
134
  success: false,
132
- error: { code: "INVALID_TOKEN", message: "Token missing serviceId" },
135
+ error: {
136
+ code: "INVALID_TOKEN",
137
+ message: "Invalid machine token payload",
138
+ },
133
139
  });
134
140
  return;
135
141
  }
142
+ const serviceId = payload.serviceId;
136
143
  // Allowlist — tightest possible scope per endpoint
137
144
  if (allowedServices && !allowedServices.includes(serviceId)) {
138
145
  logger.warn({ requestId: req.id, serviceId }, "[requireInternal] serviceId not in allowlist");
@@ -143,6 +150,7 @@ function requireInternal(options = {}) {
143
150
  return;
144
151
  }
145
152
  const context = { kind: "machine", serviceId };
153
+ req.internalAuth = payload;
146
154
  req.accessContext = context;
147
155
  logger.debug({ requestId: req.id, serviceId, jti: payload.jti }, "[requireInternal] Machine token verified");
148
156
  next();
@@ -96,6 +96,14 @@ export interface MachineAccessContext {
96
96
  export type AccessContext = HumanAccessContext | MachineAccessContext;
97
97
  export declare function isHumanPayload(payload: InternalJwtPayload): payload is HumanInternalJwtPayload;
98
98
  export declare function isMachinePayload(payload: InternalJwtPayload): payload is MachineInternalJwtPayload;
99
+ /**
100
+ * Runtime assertion for jose.jwtVerify() results.
101
+ *
102
+ * jose returns a generic JWTPayload because it cannot know
103
+ * our custom claims. After verification + this assertion,
104
+ * TypeScript safely treats it as MachineInternalJwtPayload.
105
+ */
106
+ export declare function assertMachinePayload(payload: JWTPayload): asserts payload is MachineInternalJwtPayload;
99
107
  export declare function isHumanContext(ctx: AccessContext): ctx is HumanAccessContext;
100
108
  export declare function isMachineContext(ctx: AccessContext): ctx is MachineAccessContext;
101
109
  export interface VerifiedAccessPayload {
@@ -20,6 +20,7 @@
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.isHumanPayload = isHumanPayload;
22
22
  exports.isMachinePayload = isMachinePayload;
23
+ exports.assertMachinePayload = assertMachinePayload;
23
24
  exports.isHumanContext = isHumanContext;
24
25
  exports.isMachineContext = isMachineContext;
25
26
  /* ====================================================================
@@ -35,6 +36,22 @@ function isHumanPayload(payload) {
35
36
  function isMachinePayload(payload) {
36
37
  return payload.isMachine === true;
37
38
  }
39
+ /**
40
+ * Runtime assertion for jose.jwtVerify() results.
41
+ *
42
+ * jose returns a generic JWTPayload because it cannot know
43
+ * our custom claims. After verification + this assertion,
44
+ * TypeScript safely treats it as MachineInternalJwtPayload.
45
+ */
46
+ function assertMachinePayload(payload) {
47
+ if (payload["typ"] !== "internal" ||
48
+ payload["isMachine"] !== true ||
49
+ typeof payload["serviceId"] !== "string" ||
50
+ typeof payload["jti"] !== "string" ||
51
+ typeof payload["caller"] !== "string") {
52
+ throw new Error("Invalid machine JWT payload");
53
+ }
54
+ }
38
55
  function isHumanContext(ctx) {
39
56
  return ctx.kind === "human";
40
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discover-cloud/shared",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",