@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.
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/require-auth.middleware.d.ts +86 -0
- package/dist/middleware/require-auth.middleware.js +163 -0
- package/dist/middleware/require-human.middleware.d.ts +2 -0
- package/dist/middleware/require-human.middleware.js +18 -0
- package/dist/middleware/require-internal.middleware.js +11 -3
- package/dist/types/express.types.d.ts +8 -0
- package/dist/types/express.types.js +17 -0
- package/package.json +1 -1
package/dist/middleware/index.js
CHANGED
|
@@ -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,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
|
-
|
|
129
|
-
|
|
129
|
+
try {
|
|
130
|
+
(0, types_1.assertMachinePayload)(payload);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
130
133
|
res.status(401).json({
|
|
131
134
|
success: false,
|
|
132
|
-
error: {
|
|
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
|
}
|