@discover-cloud/shared 1.2.3 → 1.2.4
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.js
CHANGED
|
@@ -20,3 +20,4 @@ __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);
|
|
@@ -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 RequireAuthMiddleware {
|
|
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.RequireAuthMiddleware = 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 RequireAuthMiddleware {
|
|
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.RequireAuthMiddleware = RequireAuthMiddleware;
|