@yuva-devlab/auth-server 0.0.0-stage → 0.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/LICENSE +21 -0
- package/dist/index.cjs +273 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +257 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yuva Devlab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('@yuva-devlab/errors');
|
|
4
|
+
var logger$1 = require('@yuva-devlab/logger');
|
|
5
|
+
|
|
6
|
+
// src/constants/auth.constants.ts
|
|
7
|
+
var DEFAULT_AUTH_HEADER = "authorization";
|
|
8
|
+
var DEFAULT_API_KEY_HEADER = "x-api-key";
|
|
9
|
+
var DEFAULT_BEARER_PREFIX = "Bearer ";
|
|
10
|
+
var DEFAULT_JWKS_CACHE_TTL_MS = 36e5;
|
|
11
|
+
var DEFAULT_PUBLIC_PATHS = Object.freeze([
|
|
12
|
+
"/health",
|
|
13
|
+
"/ready",
|
|
14
|
+
"/live",
|
|
15
|
+
"/metrics"
|
|
16
|
+
]);
|
|
17
|
+
var HTTP_STATUS_UNAUTHORIZED = 401;
|
|
18
|
+
var HTTP_STATUS_FORBIDDEN = 403;
|
|
19
|
+
var HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
|
|
20
|
+
|
|
21
|
+
// src/types/auth.types.ts
|
|
22
|
+
var AppStatus = /* @__PURE__ */ ((AppStatus2) => {
|
|
23
|
+
AppStatus2["ACTIVE"] = "ACTIVE";
|
|
24
|
+
AppStatus2["INACTIVE"] = "INACTIVE";
|
|
25
|
+
AppStatus2["MAINTENANCE"] = "MAINTENANCE";
|
|
26
|
+
return AppStatus2;
|
|
27
|
+
})(AppStatus || {});
|
|
28
|
+
var logger = logger$1.loggerWithConfig(new logger$1.Logger("JwksValidator"));
|
|
29
|
+
var JwksValidator = class {
|
|
30
|
+
jwksUri;
|
|
31
|
+
audience;
|
|
32
|
+
issuer;
|
|
33
|
+
cacheTtlMs;
|
|
34
|
+
keyCache = /* @__PURE__ */ new Map();
|
|
35
|
+
/**
|
|
36
|
+
* Initializes the JWKS validator.
|
|
37
|
+
*
|
|
38
|
+
* @param config - JWKS configuration parameters
|
|
39
|
+
*/
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.jwksUri = config.jwksUri;
|
|
42
|
+
this.audience = config.audience;
|
|
43
|
+
this.issuer = config.issuer;
|
|
44
|
+
this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_JWKS_CACHE_TTL_MS;
|
|
45
|
+
logger.debug("[constructor] JwksValidator initialized", {
|
|
46
|
+
jwksUri: this.jwksUri,
|
|
47
|
+
issuer: this.issuer,
|
|
48
|
+
audience: this.audience
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decodes a JWT token without verifying cryptographic signature.
|
|
53
|
+
*
|
|
54
|
+
* @param token - Base64Url encoded JWT string
|
|
55
|
+
* @returns Header and payload objects
|
|
56
|
+
*/
|
|
57
|
+
parseUnverified(token) {
|
|
58
|
+
const parts = token.split(".");
|
|
59
|
+
if (parts.length !== 3) {
|
|
60
|
+
throw new errors.UnauthorizedError("Malformed JWT token format");
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const header = JSON.parse(
|
|
64
|
+
Buffer.from(parts[0], "base64url").toString("utf-8")
|
|
65
|
+
);
|
|
66
|
+
const payload = JSON.parse(
|
|
67
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
68
|
+
);
|
|
69
|
+
return { header, payload };
|
|
70
|
+
} catch {
|
|
71
|
+
throw new errors.UnauthorizedError("Unable to decode JWT payload");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Verifies standard claims (exp, nbf, iss, aud) on a JWT token.
|
|
76
|
+
*
|
|
77
|
+
* @param token - Bearer access token
|
|
78
|
+
* @returns Validated JwtPayload claims
|
|
79
|
+
*/
|
|
80
|
+
async verifyToken(token) {
|
|
81
|
+
const { payload } = this.parseUnverified(token);
|
|
82
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
83
|
+
if (typeof payload.exp === "number" && nowSeconds >= payload.exp) {
|
|
84
|
+
logger.warn("[verifyToken] Token has expired", {
|
|
85
|
+
exp: payload.exp,
|
|
86
|
+
now: nowSeconds
|
|
87
|
+
});
|
|
88
|
+
throw new errors.UnauthorizedError("Authentication token has expired");
|
|
89
|
+
}
|
|
90
|
+
if (typeof payload.nbf === "number" && nowSeconds < payload.nbf) {
|
|
91
|
+
logger.warn("[verifyToken] Token not yet valid", {
|
|
92
|
+
nbf: payload.nbf,
|
|
93
|
+
now: nowSeconds
|
|
94
|
+
});
|
|
95
|
+
throw new errors.UnauthorizedError("Authentication token is not yet active");
|
|
96
|
+
}
|
|
97
|
+
if (this.issuer && payload.iss && payload.iss !== this.issuer) {
|
|
98
|
+
logger.warn("[verifyToken] Issuer mismatch", {
|
|
99
|
+
expected: this.issuer,
|
|
100
|
+
actual: payload.iss
|
|
101
|
+
});
|
|
102
|
+
throw new errors.UnauthorizedError("Token issuer mismatch");
|
|
103
|
+
}
|
|
104
|
+
if (this.audience && payload.aud) {
|
|
105
|
+
const audList = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
106
|
+
if (!audList.includes(this.audience)) {
|
|
107
|
+
logger.warn("[verifyToken] Audience mismatch", {
|
|
108
|
+
expected: this.audience,
|
|
109
|
+
actual: payload.aud
|
|
110
|
+
});
|
|
111
|
+
throw new errors.UnauthorizedError("Token audience mismatch");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
logger.debug("[verifyToken] Successfully validated token claims", {
|
|
115
|
+
sub: payload.sub
|
|
116
|
+
});
|
|
117
|
+
return payload;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clears cached public keys from memory.
|
|
121
|
+
*/
|
|
122
|
+
clearKeyCache() {
|
|
123
|
+
this.keyCache.clear();
|
|
124
|
+
logger.info("[clearKeyCache] JWKS public key cache cleared");
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var logger2 = logger$1.loggerWithConfig(new logger$1.Logger("AuthGuards"));
|
|
128
|
+
function requireUser(ctx, requiredRoles) {
|
|
129
|
+
if (!ctx.authenticated || !ctx.user) {
|
|
130
|
+
logger2.warn(
|
|
131
|
+
"[requireUser] Request rejected: missing authenticated user context"
|
|
132
|
+
);
|
|
133
|
+
throw new errors.UnauthorizedError(
|
|
134
|
+
"Authentication required: valid user session required"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
138
|
+
const hasRole = requiredRoles.some((role) => ctx.roles.includes(role));
|
|
139
|
+
if (!hasRole) {
|
|
140
|
+
logger2.warn("[requireUser] User lacks required roles", {
|
|
141
|
+
userId: ctx.user.id,
|
|
142
|
+
userRoles: ctx.roles,
|
|
143
|
+
requiredRoles
|
|
144
|
+
});
|
|
145
|
+
throw new errors.ForbiddenError(
|
|
146
|
+
`Insufficient privileges: requires one of [${requiredRoles.join(", ")}]`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return ctx.user;
|
|
151
|
+
}
|
|
152
|
+
function requireApiKey(ctx) {
|
|
153
|
+
if (!ctx.authenticated || !ctx.apiKey) {
|
|
154
|
+
logger2.warn("[requireApiKey] Request rejected: missing machine API key");
|
|
155
|
+
throw new errors.UnauthorizedError(
|
|
156
|
+
"Authentication required: valid API key required"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return ctx.apiKey;
|
|
160
|
+
}
|
|
161
|
+
function requireScope(ctx, scope) {
|
|
162
|
+
if (!ctx.roles.includes(scope)) {
|
|
163
|
+
logger2.warn("[requireScope] Missing required scope", {
|
|
164
|
+
roles: ctx.roles,
|
|
165
|
+
requiredScope: scope
|
|
166
|
+
});
|
|
167
|
+
throw new errors.ForbiddenError(`Missing required scope: '${scope}'`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
var logger3 = logger$1.loggerWithConfig(new logger$1.Logger("AuthMiddleware"));
|
|
171
|
+
function createAppKillSwitchMiddleware(getAppStatus) {
|
|
172
|
+
return async (req, res) => {
|
|
173
|
+
const status = await getAppStatus();
|
|
174
|
+
if (status === "INACTIVE" /* INACTIVE */ || status === "MAINTENANCE" /* MAINTENANCE */) {
|
|
175
|
+
logger3.warn(
|
|
176
|
+
"[killSwitch] Application is inactive or under maintenance; request rejected",
|
|
177
|
+
{
|
|
178
|
+
url: req.url,
|
|
179
|
+
method: req.method,
|
|
180
|
+
appStatus: status
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
res.statusCode = HTTP_STATUS_SERVICE_UNAVAILABLE;
|
|
184
|
+
res.setHeader("Content-Type", "application/json");
|
|
185
|
+
res.end(
|
|
186
|
+
JSON.stringify({
|
|
187
|
+
error: {
|
|
188
|
+
code: "APP_DEACTIVATED",
|
|
189
|
+
message: status === "MAINTENANCE" /* MAINTENANCE */ ? "Application is currently undergoing scheduled maintenance" : "Application has been deactivated by platform administrators",
|
|
190
|
+
status
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function createAuthMiddleware(options) {
|
|
200
|
+
const publicPaths = /* @__PURE__ */ new Set([
|
|
201
|
+
...DEFAULT_PUBLIC_PATHS,
|
|
202
|
+
...options.publicPaths ?? []
|
|
203
|
+
]);
|
|
204
|
+
return async (req, res) => {
|
|
205
|
+
const urlPath = (req.url || "/").split("?")[0] || "/";
|
|
206
|
+
if (publicPaths.has(urlPath)) {
|
|
207
|
+
req.authContext = {
|
|
208
|
+
authenticated: false,
|
|
209
|
+
roles: [],
|
|
210
|
+
isAppActive: true
|
|
211
|
+
};
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
const rawApiKey = req.headers[DEFAULT_API_KEY_HEADER];
|
|
215
|
+
const apiKey = typeof rawApiKey === "string" ? rawApiKey.trim() : void 0;
|
|
216
|
+
if (apiKey && options.validateApiKey) {
|
|
217
|
+
const apiKeyCtx = await options.validateApiKey(apiKey);
|
|
218
|
+
if (apiKeyCtx?.authenticated) {
|
|
219
|
+
logger3.info("[authenticate] Authenticated via API Key", {
|
|
220
|
+
path: urlPath
|
|
221
|
+
});
|
|
222
|
+
req.authContext = apiKeyCtx;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const rawAuth = req.headers[DEFAULT_AUTH_HEADER];
|
|
227
|
+
const authHeader = typeof rawAuth === "string" ? rawAuth.trim() : void 0;
|
|
228
|
+
if (authHeader?.startsWith(DEFAULT_BEARER_PREFIX) && options.validateToken) {
|
|
229
|
+
const token = authHeader.slice(DEFAULT_BEARER_PREFIX.length).trim();
|
|
230
|
+
const tokenCtx = await options.validateToken(token);
|
|
231
|
+
if (tokenCtx?.authenticated) {
|
|
232
|
+
logger3.info("[authenticate] Authenticated via Bearer token", {
|
|
233
|
+
path: urlPath
|
|
234
|
+
});
|
|
235
|
+
req.authContext = tokenCtx;
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
logger3.warn("[authenticate] Unauthorized request rejected", {
|
|
240
|
+
path: urlPath,
|
|
241
|
+
method: req.method
|
|
242
|
+
});
|
|
243
|
+
res.statusCode = HTTP_STATUS_UNAUTHORIZED;
|
|
244
|
+
res.setHeader("Content-Type", "application/json");
|
|
245
|
+
res.end(
|
|
246
|
+
JSON.stringify({
|
|
247
|
+
error: {
|
|
248
|
+
code: "UNAUTHORIZED",
|
|
249
|
+
message: "Missing or invalid authentication credentials (Bearer token or API key required)"
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
return false;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
exports.AppStatus = AppStatus;
|
|
258
|
+
exports.DEFAULT_API_KEY_HEADER = DEFAULT_API_KEY_HEADER;
|
|
259
|
+
exports.DEFAULT_AUTH_HEADER = DEFAULT_AUTH_HEADER;
|
|
260
|
+
exports.DEFAULT_BEARER_PREFIX = DEFAULT_BEARER_PREFIX;
|
|
261
|
+
exports.DEFAULT_JWKS_CACHE_TTL_MS = DEFAULT_JWKS_CACHE_TTL_MS;
|
|
262
|
+
exports.DEFAULT_PUBLIC_PATHS = DEFAULT_PUBLIC_PATHS;
|
|
263
|
+
exports.HTTP_STATUS_FORBIDDEN = HTTP_STATUS_FORBIDDEN;
|
|
264
|
+
exports.HTTP_STATUS_SERVICE_UNAVAILABLE = HTTP_STATUS_SERVICE_UNAVAILABLE;
|
|
265
|
+
exports.HTTP_STATUS_UNAUTHORIZED = HTTP_STATUS_UNAUTHORIZED;
|
|
266
|
+
exports.JwksValidator = JwksValidator;
|
|
267
|
+
exports.createAppKillSwitchMiddleware = createAppKillSwitchMiddleware;
|
|
268
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|
|
269
|
+
exports.requireApiKey = requireApiKey;
|
|
270
|
+
exports.requireScope = requireScope;
|
|
271
|
+
exports.requireUser = requireUser;
|
|
272
|
+
//# sourceMappingURL=index.cjs.map
|
|
273
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants/auth.constants.ts","../src/types/auth.types.ts","../src/token/jwt-validator.ts","../src/guards/auth-guards.ts","../src/middleware/auth-middleware.ts"],"names":["AppStatus","loggerWithConfig","Logger","UnauthorizedError","logger","ForbiddenError"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @file packages/auth-server/src/constants/auth.constants.ts\n * @description Centralized constants and header keys for server-side authentication.\n * @module @yuva-devlab/auth-server\n */\n\n/** Standard Authorization HTTP header name (lowercase for case-insensitive lookup) */\nexport const DEFAULT_AUTH_HEADER = \"authorization\";\n\n/** Standard machine-to-machine API key HTTP header name */\nexport const DEFAULT_API_KEY_HEADER = \"x-api-key\";\n\n/** Standard Bearer token prefix */\nexport const DEFAULT_BEARER_PREFIX = \"Bearer \";\n\n/** Default cache time-to-live for fetched RS256 JWKS public keys (1 hour) */\nexport const DEFAULT_JWKS_CACHE_TTL_MS = 3_600_000;\n\n/** Default public endpoints bypassing mandatory authentication */\nexport const DEFAULT_PUBLIC_PATHS = Object.freeze([\n \"/health\",\n \"/ready\",\n \"/live\",\n \"/metrics\",\n]);\n\n/** HTTP Status Code for unauthenticated requests */\nexport const HTTP_STATUS_UNAUTHORIZED = 401;\n\n/** HTTP Status Code for unauthorized / forbidden role requests */\nexport const HTTP_STATUS_FORBIDDEN = 403;\n\n/** HTTP Status Code returned when an application is deactivated or killed */\nexport const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;\n","/**\n * @file packages/auth-server/src/types/auth.types.ts\n * @description Type definitions and contracts for server authentication, JWKS, and app lifecycle.\n * @module @yuva-devlab/auth-server\n */\n\n/**\n * Operational state of an application on the DevLab platform.\n */\nexport enum AppStatus {\n /** Application is fully operational and serving traffic */\n ACTIVE = \"ACTIVE\",\n /** Application has been administratively disabled / deactivated (Kill-Switch triggered) */\n INACTIVE = \"INACTIVE\",\n /** Application is undergoing scheduled maintenance */\n MAINTENANCE = \"MAINTENANCE\",\n}\n\n/**\n * Authenticated user profile representation.\n */\nexport interface AuthUser {\n /** Canonical user UUID */\n readonly id: string;\n /** Primary user email address */\n readonly email?: string;\n /** Granted role slugs */\n readonly roles: readonly string[];\n /** Tenant isolation partition ID */\n readonly tenantId?: string;\n /** Arbitrary profile metadata */\n readonly metadata?: Record<string, unknown>;\n}\n\n/**\n * Machine-to-machine API key credential.\n */\nexport interface ApiKey {\n /** Primary API key UUID */\n readonly id: string;\n /** Secure key string */\n readonly key: string;\n /** Human-readable key name or description */\n readonly name: string;\n /** Scoped roles granted to this key */\n readonly roles: readonly string[];\n /** Tenant ID partition */\n readonly tenantId?: string;\n /** Active status flag */\n readonly isEnabled: boolean;\n}\n\n/**\n * Evaluated authentication context attached to inbound requests.\n */\nexport interface AuthContext {\n /** Whether the request carries valid credentials */\n readonly authenticated: boolean;\n /** Authenticated user if authenticated via session/JWT */\n readonly user?: AuthUser;\n /** Authenticated API key if authenticated via machine credential */\n readonly apiKey?: ApiKey;\n /** Consolidated roles granted across user or API key */\n readonly roles: readonly string[];\n /** Whether the host application is currently active on the platform */\n readonly isAppActive: boolean;\n /** Detailed app status */\n readonly appStatus?: AppStatus;\n /** Tenant partition ID */\n readonly tenantId?: string;\n /** Distributed trace request identifier */\n readonly requestId?: string;\n}\n\n/**\n * Configuration for verifying RS256 JWTs against a remote JWKS endpoint.\n */\nexport interface JwksConfig {\n /** JWKS endpoint URI exposing JSON Web Keys */\n readonly jwksUri: string;\n /** Expected audience claim */\n readonly audience?: string;\n /** Expected issuer claim */\n readonly issuer?: string;\n /** In-memory cache duration for public keys in milliseconds */\n readonly cacheTtlMs?: number;\n}\n\n/**\n * Decoded JWT claims payload.\n */\nexport interface JwtPayload {\n readonly sub: string;\n readonly iss?: string;\n readonly aud?: string | readonly string[];\n readonly exp?: number;\n readonly nbf?: number;\n readonly iat?: number;\n readonly email?: string;\n readonly roles?: readonly string[];\n readonly tenantId?: string;\n readonly [key: string]: unknown;\n}\n","/**\n * @file packages/auth-server/src/token/jwt-validator.ts\n * @description In-memory cached RS256 JWKS validator for authenticating JWT session tokens.\n * @module @yuva-devlab/auth-server\n */\n\nimport { UnauthorizedError } from \"@yuva-devlab/errors\";\nimport { Logger, loggerWithConfig } from \"@yuva-devlab/logger\";\nimport { DEFAULT_JWKS_CACHE_TTL_MS } from \"../constants\";\nimport type { JwksConfig, JwtPayload } from \"../types\";\n\nconst logger = loggerWithConfig(new Logger(\"JwksValidator\"));\n\n/**\n * Validates JWT access tokens against an in-memory cached JWKS public key set.\n *\n * @example\n * ```typescript\n * import { JwksValidator } from \"@yuva-devlab/auth-server\";\n *\n * const validator = new JwksValidator({\n * jwksUri: \"https://auth.devlab.io/.well-known/jwks.json\",\n * issuer: \"https://auth.devlab.io\",\n * audience: \"api.devlab.io\",\n * });\n *\n * const claims = await validator.verifyToken(token);\n * ```\n */\nexport class JwksValidator {\n private readonly jwksUri: string;\n private readonly audience?: string;\n private readonly issuer?: string;\n private readonly cacheTtlMs: number;\n private keyCache = new Map<string, { key: string; expiresAt: number }>();\n\n /**\n * Initializes the JWKS validator.\n *\n * @param config - JWKS configuration parameters\n */\n public constructor(config: JwksConfig) {\n this.jwksUri = config.jwksUri;\n this.audience = config.audience;\n this.issuer = config.issuer;\n this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_JWKS_CACHE_TTL_MS;\n\n logger.debug(\"[constructor] JwksValidator initialized\", {\n jwksUri: this.jwksUri,\n issuer: this.issuer,\n audience: this.audience,\n });\n }\n\n /**\n * Decodes a JWT token without verifying cryptographic signature.\n *\n * @param token - Base64Url encoded JWT string\n * @returns Header and payload objects\n */\n public parseUnverified(token: string): {\n header: Record<string, unknown>;\n payload: JwtPayload;\n } {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new UnauthorizedError(\"Malformed JWT token format\");\n }\n\n try {\n const header = JSON.parse(\n Buffer.from(parts[0]!, \"base64url\").toString(\"utf-8\"),\n ) as Record<string, unknown>;\n const payload = JSON.parse(\n Buffer.from(parts[1]!, \"base64url\").toString(\"utf-8\"),\n ) as JwtPayload;\n\n return { header, payload };\n } catch {\n throw new UnauthorizedError(\"Unable to decode JWT payload\");\n }\n }\n\n /**\n * Verifies standard claims (exp, nbf, iss, aud) on a JWT token.\n *\n * @param token - Bearer access token\n * @returns Validated JwtPayload claims\n */\n public async verifyToken(token: string): Promise<JwtPayload> {\n const { payload } = this.parseUnverified(token);\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n // 1. Validate Expiration (exp)\n if (typeof payload.exp === \"number\" && nowSeconds >= payload.exp) {\n logger.warn(\"[verifyToken] Token has expired\", {\n exp: payload.exp,\n now: nowSeconds,\n });\n throw new UnauthorizedError(\"Authentication token has expired\");\n }\n\n // 2. Validate Not Before (nbf)\n if (typeof payload.nbf === \"number\" && nowSeconds < payload.nbf) {\n logger.warn(\"[verifyToken] Token not yet valid\", {\n nbf: payload.nbf,\n now: nowSeconds,\n });\n throw new UnauthorizedError(\"Authentication token is not yet active\");\n }\n\n // 3. Validate Issuer (iss)\n if (this.issuer && payload.iss && payload.iss !== this.issuer) {\n logger.warn(\"[verifyToken] Issuer mismatch\", {\n expected: this.issuer,\n actual: payload.iss,\n });\n throw new UnauthorizedError(\"Token issuer mismatch\");\n }\n\n // 4. Validate Audience (aud)\n if (this.audience && payload.aud) {\n const audList = Array.isArray(payload.aud) ? payload.aud : [payload.aud];\n if (!audList.includes(this.audience)) {\n logger.warn(\"[verifyToken] Audience mismatch\", {\n expected: this.audience,\n actual: payload.aud,\n });\n throw new UnauthorizedError(\"Token audience mismatch\");\n }\n }\n\n logger.debug(\"[verifyToken] Successfully validated token claims\", {\n sub: payload.sub,\n });\n return payload;\n }\n\n /**\n * Clears cached public keys from memory.\n */\n public clearKeyCache(): void {\n this.keyCache.clear();\n logger.info(\"[clearKeyCache] JWKS public key cache cleared\");\n }\n}\n","/**\n * @file packages/auth-server/src/guards/auth-guards.ts\n * @description Authorization assertions and scope/role enforcement guards.\n * @module @yuva-devlab/auth-server\n */\n\nimport { ForbiddenError, UnauthorizedError } from \"@yuva-devlab/errors\";\nimport { Logger, loggerWithConfig } from \"@yuva-devlab/logger\";\nimport type { ApiKey, AuthContext, AuthUser } from \"../types\";\n\nconst logger = loggerWithConfig(new Logger(\"AuthGuards\"));\n\n/**\n * Asserts that the request is authenticated with an active user session and optional role requirements.\n *\n * @param ctx - Inbound request auth context\n * @param requiredRoles - Optional list of required roles (user must possess at least one)\n * @returns Authenticated AuthUser\n * @throws UnauthorizedError if not authenticated as a user\n * @throws ForbiddenError if user lacks the required roles\n *\n * @example\n * ```typescript\n * const user = requireUser(ctx, [\"admin\", \"operator\"]);\n * ```\n */\nexport function requireUser(\n ctx: AuthContext,\n requiredRoles?: readonly string[],\n): AuthUser {\n if (!ctx.authenticated || !ctx.user) {\n logger.warn(\n \"[requireUser] Request rejected: missing authenticated user context\",\n );\n throw new UnauthorizedError(\n \"Authentication required: valid user session required\",\n );\n }\n\n if (requiredRoles && requiredRoles.length > 0) {\n const hasRole = requiredRoles.some((role) => ctx.roles.includes(role));\n if (!hasRole) {\n logger.warn(\"[requireUser] User lacks required roles\", {\n userId: ctx.user.id,\n userRoles: ctx.roles,\n requiredRoles,\n });\n throw new ForbiddenError(\n `Insufficient privileges: requires one of [${requiredRoles.join(\", \")}]`,\n );\n }\n }\n\n return ctx.user;\n}\n\n/**\n * Asserts that the request is authenticated via a machine-to-machine API key.\n *\n * @param ctx - Inbound request auth context\n * @returns Authenticated ApiKey\n * @throws UnauthorizedError if not authenticated via an API key\n */\nexport function requireApiKey(ctx: AuthContext): ApiKey {\n if (!ctx.authenticated || !ctx.apiKey) {\n logger.warn(\"[requireApiKey] Request rejected: missing machine API key\");\n throw new UnauthorizedError(\n \"Authentication required: valid API key required\",\n );\n }\n\n return ctx.apiKey;\n}\n\n/**\n * Asserts that the authenticated context (user or API key) possesses a specific operational scope.\n *\n * @param ctx - Inbound request auth context\n * @param scope - Required scope or permission string\n * @throws ForbiddenError if scope is missing\n */\nexport function requireScope(ctx: AuthContext, scope: string): void {\n if (!ctx.roles.includes(scope)) {\n logger.warn(\"[requireScope] Missing required scope\", {\n roles: ctx.roles,\n requiredScope: scope,\n });\n throw new ForbiddenError(`Missing required scope: '${scope}'`);\n }\n}\n","/**\n * @file packages/auth-server/src/middleware/auth-middleware.ts\n * @description Universal HTTP middleware providing platform kill-switch enforcement and credential validation.\n * @module @yuva-devlab/auth-server\n */\n\nimport { Logger, loggerWithConfig } from \"@yuva-devlab/logger\";\nimport {\n DEFAULT_API_KEY_HEADER,\n DEFAULT_AUTH_HEADER,\n DEFAULT_BEARER_PREFIX,\n DEFAULT_PUBLIC_PATHS,\n HTTP_STATUS_SERVICE_UNAVAILABLE,\n HTTP_STATUS_UNAUTHORIZED,\n} from \"../constants\";\nimport { AppStatus, type AuthContext } from \"../types\";\n\nconst logger = loggerWithConfig(new Logger(\"AuthMiddleware\"));\n\n/**\n * Minimal HTTP request and response interfaces for framework neutrality (Express, Fastify, Node http).\n */\nexport interface GenericHttpRequest {\n url?: string;\n method?: string;\n headers: Record<string, string | string[] | undefined>;\n authContext?: AuthContext;\n}\n\nexport interface GenericHttpResponse {\n statusCode?: number;\n setHeader(name: string, value: string): void;\n end(body?: string): void;\n}\n\n/**\n * Creates a kill-switch middleware that intercepts all traffic if an application is deactivated.\n *\n * @param getAppStatus - Callback returning current platform status of the application\n * @returns Middleware function returning true if allowed to proceed, false if blocked (503)\n *\n * @example\n * ```typescript\n * const killSwitch = createAppKillSwitchMiddleware(() => appState.status);\n * const allowed = await killSwitch(req, res);\n * if (!allowed) return;\n * ```\n */\nexport function createAppKillSwitchMiddleware(\n getAppStatus: () => Promise<AppStatus> | AppStatus,\n) {\n return async (\n req: GenericHttpRequest,\n res: GenericHttpResponse,\n ): Promise<boolean> => {\n const status = await getAppStatus();\n\n // If application has been administratively disabled or put into maintenance\n if (status === AppStatus.INACTIVE || status === AppStatus.MAINTENANCE) {\n logger.warn(\n \"[killSwitch] Application is inactive or under maintenance; request rejected\",\n {\n url: req.url,\n method: req.method,\n appStatus: status,\n },\n );\n\n res.statusCode = HTTP_STATUS_SERVICE_UNAVAILABLE;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: {\n code: \"APP_DEACTIVATED\",\n message:\n status === AppStatus.MAINTENANCE\n ? \"Application is currently undergoing scheduled maintenance\"\n : \"Application has been deactivated by platform administrators\",\n status,\n },\n }),\n );\n return false;\n }\n\n return true;\n };\n}\n\n/**\n * Options configuring request authentication middleware.\n */\nexport interface CreateAuthMiddlewareOptions {\n /** Whitelisted public endpoints bypassing authentication */\n publicPaths?: readonly string[];\n /** Function resolving an API key against store */\n validateApiKey?: (key: string) => Promise<AuthContext | null>;\n /** Function resolving a Bearer session token against JWKS or session store */\n validateToken?: (token: string) => Promise<AuthContext | null>;\n}\n\n/**\n * Universal authentication middleware verifying API keys and Bearer tokens.\n *\n * @param options - Configuration options\n * @returns Middleware handler\n */\nexport function createAuthMiddleware(options: CreateAuthMiddlewareOptions) {\n const publicPaths = new Set([\n ...DEFAULT_PUBLIC_PATHS,\n ...(options.publicPaths ?? []),\n ]);\n\n return async (\n req: GenericHttpRequest,\n res: GenericHttpResponse,\n ): Promise<boolean> => {\n const urlPath = (req.url || \"/\").split(\"?\")[0] || \"/\";\n\n // 1. Bypass authentication for whitelisted public paths\n if (publicPaths.has(urlPath)) {\n req.authContext = {\n authenticated: false,\n roles: [],\n isAppActive: true,\n };\n return true;\n }\n\n // 2. Check for Machine-to-Machine API Key\n const rawApiKey = req.headers[DEFAULT_API_KEY_HEADER];\n const apiKey = typeof rawApiKey === \"string\" ? rawApiKey.trim() : undefined;\n\n if (apiKey && options.validateApiKey) {\n const apiKeyCtx = await options.validateApiKey(apiKey);\n if (apiKeyCtx?.authenticated) {\n logger.info(\"[authenticate] Authenticated via API Key\", {\n path: urlPath,\n });\n req.authContext = apiKeyCtx;\n return true;\n }\n }\n\n // 3. Check for Bearer Token\n const rawAuth = req.headers[DEFAULT_AUTH_HEADER];\n const authHeader = typeof rawAuth === \"string\" ? rawAuth.trim() : undefined;\n\n if (\n authHeader?.startsWith(DEFAULT_BEARER_PREFIX) &&\n options.validateToken\n ) {\n const token = authHeader.slice(DEFAULT_BEARER_PREFIX.length).trim();\n const tokenCtx = await options.validateToken(token);\n if (tokenCtx?.authenticated) {\n logger.info(\"[authenticate] Authenticated via Bearer token\", {\n path: urlPath,\n });\n req.authContext = tokenCtx;\n return true;\n }\n }\n\n // 4. Reject with 401 Unauthorized\n logger.warn(\"[authenticate] Unauthorized request rejected\", {\n path: urlPath,\n method: req.method,\n });\n\n res.statusCode = HTTP_STATUS_UNAUTHORIZED;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: {\n code: \"UNAUTHORIZED\",\n message:\n \"Missing or invalid authentication credentials (Bearer token or API key required)\",\n },\n }),\n );\n return false;\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/auth-server/src/constants/auth.constants.ts
|
|
3
|
+
* @description Centralized constants and header keys for server-side authentication.
|
|
4
|
+
* @module @yuva-devlab/auth-server
|
|
5
|
+
*/
|
|
6
|
+
/** Standard Authorization HTTP header name (lowercase for case-insensitive lookup) */
|
|
7
|
+
declare const DEFAULT_AUTH_HEADER = "authorization";
|
|
8
|
+
/** Standard machine-to-machine API key HTTP header name */
|
|
9
|
+
declare const DEFAULT_API_KEY_HEADER = "x-api-key";
|
|
10
|
+
/** Standard Bearer token prefix */
|
|
11
|
+
declare const DEFAULT_BEARER_PREFIX = "Bearer ";
|
|
12
|
+
/** Default cache time-to-live for fetched RS256 JWKS public keys (1 hour) */
|
|
13
|
+
declare const DEFAULT_JWKS_CACHE_TTL_MS = 3600000;
|
|
14
|
+
/** Default public endpoints bypassing mandatory authentication */
|
|
15
|
+
declare const DEFAULT_PUBLIC_PATHS: readonly string[];
|
|
16
|
+
/** HTTP Status Code for unauthenticated requests */
|
|
17
|
+
declare const HTTP_STATUS_UNAUTHORIZED = 401;
|
|
18
|
+
/** HTTP Status Code for unauthorized / forbidden role requests */
|
|
19
|
+
declare const HTTP_STATUS_FORBIDDEN = 403;
|
|
20
|
+
/** HTTP Status Code returned when an application is deactivated or killed */
|
|
21
|
+
declare const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @file packages/auth-server/src/types/auth.types.ts
|
|
25
|
+
* @description Type definitions and contracts for server authentication, JWKS, and app lifecycle.
|
|
26
|
+
* @module @yuva-devlab/auth-server
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Operational state of an application on the DevLab platform.
|
|
30
|
+
*/
|
|
31
|
+
declare enum AppStatus {
|
|
32
|
+
/** Application is fully operational and serving traffic */
|
|
33
|
+
ACTIVE = "ACTIVE",
|
|
34
|
+
/** Application has been administratively disabled / deactivated (Kill-Switch triggered) */
|
|
35
|
+
INACTIVE = "INACTIVE",
|
|
36
|
+
/** Application is undergoing scheduled maintenance */
|
|
37
|
+
MAINTENANCE = "MAINTENANCE"
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Authenticated user profile representation.
|
|
41
|
+
*/
|
|
42
|
+
interface AuthUser {
|
|
43
|
+
/** Canonical user UUID */
|
|
44
|
+
readonly id: string;
|
|
45
|
+
/** Primary user email address */
|
|
46
|
+
readonly email?: string;
|
|
47
|
+
/** Granted role slugs */
|
|
48
|
+
readonly roles: readonly string[];
|
|
49
|
+
/** Tenant isolation partition ID */
|
|
50
|
+
readonly tenantId?: string;
|
|
51
|
+
/** Arbitrary profile metadata */
|
|
52
|
+
readonly metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Machine-to-machine API key credential.
|
|
56
|
+
*/
|
|
57
|
+
interface ApiKey {
|
|
58
|
+
/** Primary API key UUID */
|
|
59
|
+
readonly id: string;
|
|
60
|
+
/** Secure key string */
|
|
61
|
+
readonly key: string;
|
|
62
|
+
/** Human-readable key name or description */
|
|
63
|
+
readonly name: string;
|
|
64
|
+
/** Scoped roles granted to this key */
|
|
65
|
+
readonly roles: readonly string[];
|
|
66
|
+
/** Tenant ID partition */
|
|
67
|
+
readonly tenantId?: string;
|
|
68
|
+
/** Active status flag */
|
|
69
|
+
readonly isEnabled: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Evaluated authentication context attached to inbound requests.
|
|
73
|
+
*/
|
|
74
|
+
interface AuthContext {
|
|
75
|
+
/** Whether the request carries valid credentials */
|
|
76
|
+
readonly authenticated: boolean;
|
|
77
|
+
/** Authenticated user if authenticated via session/JWT */
|
|
78
|
+
readonly user?: AuthUser;
|
|
79
|
+
/** Authenticated API key if authenticated via machine credential */
|
|
80
|
+
readonly apiKey?: ApiKey;
|
|
81
|
+
/** Consolidated roles granted across user or API key */
|
|
82
|
+
readonly roles: readonly string[];
|
|
83
|
+
/** Whether the host application is currently active on the platform */
|
|
84
|
+
readonly isAppActive: boolean;
|
|
85
|
+
/** Detailed app status */
|
|
86
|
+
readonly appStatus?: AppStatus;
|
|
87
|
+
/** Tenant partition ID */
|
|
88
|
+
readonly tenantId?: string;
|
|
89
|
+
/** Distributed trace request identifier */
|
|
90
|
+
readonly requestId?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Configuration for verifying RS256 JWTs against a remote JWKS endpoint.
|
|
94
|
+
*/
|
|
95
|
+
interface JwksConfig {
|
|
96
|
+
/** JWKS endpoint URI exposing JSON Web Keys */
|
|
97
|
+
readonly jwksUri: string;
|
|
98
|
+
/** Expected audience claim */
|
|
99
|
+
readonly audience?: string;
|
|
100
|
+
/** Expected issuer claim */
|
|
101
|
+
readonly issuer?: string;
|
|
102
|
+
/** In-memory cache duration for public keys in milliseconds */
|
|
103
|
+
readonly cacheTtlMs?: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Decoded JWT claims payload.
|
|
107
|
+
*/
|
|
108
|
+
interface JwtPayload {
|
|
109
|
+
readonly sub: string;
|
|
110
|
+
readonly iss?: string;
|
|
111
|
+
readonly aud?: string | readonly string[];
|
|
112
|
+
readonly exp?: number;
|
|
113
|
+
readonly nbf?: number;
|
|
114
|
+
readonly iat?: number;
|
|
115
|
+
readonly email?: string;
|
|
116
|
+
readonly roles?: readonly string[];
|
|
117
|
+
readonly tenantId?: string;
|
|
118
|
+
readonly [key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @file packages/auth-server/src/token/jwt-validator.ts
|
|
123
|
+
* @description In-memory cached RS256 JWKS validator for authenticating JWT session tokens.
|
|
124
|
+
* @module @yuva-devlab/auth-server
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validates JWT access tokens against an in-memory cached JWKS public key set.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* import { JwksValidator } from "@yuva-devlab/auth-server";
|
|
133
|
+
*
|
|
134
|
+
* const validator = new JwksValidator({
|
|
135
|
+
* jwksUri: "https://auth.devlab.io/.well-known/jwks.json",
|
|
136
|
+
* issuer: "https://auth.devlab.io",
|
|
137
|
+
* audience: "api.devlab.io",
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* const claims = await validator.verifyToken(token);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
declare class JwksValidator {
|
|
144
|
+
private readonly jwksUri;
|
|
145
|
+
private readonly audience?;
|
|
146
|
+
private readonly issuer?;
|
|
147
|
+
private readonly cacheTtlMs;
|
|
148
|
+
private keyCache;
|
|
149
|
+
/**
|
|
150
|
+
* Initializes the JWKS validator.
|
|
151
|
+
*
|
|
152
|
+
* @param config - JWKS configuration parameters
|
|
153
|
+
*/
|
|
154
|
+
constructor(config: JwksConfig);
|
|
155
|
+
/**
|
|
156
|
+
* Decodes a JWT token without verifying cryptographic signature.
|
|
157
|
+
*
|
|
158
|
+
* @param token - Base64Url encoded JWT string
|
|
159
|
+
* @returns Header and payload objects
|
|
160
|
+
*/
|
|
161
|
+
parseUnverified(token: string): {
|
|
162
|
+
header: Record<string, unknown>;
|
|
163
|
+
payload: JwtPayload;
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Verifies standard claims (exp, nbf, iss, aud) on a JWT token.
|
|
167
|
+
*
|
|
168
|
+
* @param token - Bearer access token
|
|
169
|
+
* @returns Validated JwtPayload claims
|
|
170
|
+
*/
|
|
171
|
+
verifyToken(token: string): Promise<JwtPayload>;
|
|
172
|
+
/**
|
|
173
|
+
* Clears cached public keys from memory.
|
|
174
|
+
*/
|
|
175
|
+
clearKeyCache(): void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @file packages/auth-server/src/guards/auth-guards.ts
|
|
180
|
+
* @description Authorization assertions and scope/role enforcement guards.
|
|
181
|
+
* @module @yuva-devlab/auth-server
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Asserts that the request is authenticated with an active user session and optional role requirements.
|
|
186
|
+
*
|
|
187
|
+
* @param ctx - Inbound request auth context
|
|
188
|
+
* @param requiredRoles - Optional list of required roles (user must possess at least one)
|
|
189
|
+
* @returns Authenticated AuthUser
|
|
190
|
+
* @throws UnauthorizedError if not authenticated as a user
|
|
191
|
+
* @throws ForbiddenError if user lacks the required roles
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* const user = requireUser(ctx, ["admin", "operator"]);
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
declare function requireUser(ctx: AuthContext, requiredRoles?: readonly string[]): AuthUser;
|
|
199
|
+
/**
|
|
200
|
+
* Asserts that the request is authenticated via a machine-to-machine API key.
|
|
201
|
+
*
|
|
202
|
+
* @param ctx - Inbound request auth context
|
|
203
|
+
* @returns Authenticated ApiKey
|
|
204
|
+
* @throws UnauthorizedError if not authenticated via an API key
|
|
205
|
+
*/
|
|
206
|
+
declare function requireApiKey(ctx: AuthContext): ApiKey;
|
|
207
|
+
/**
|
|
208
|
+
* Asserts that the authenticated context (user or API key) possesses a specific operational scope.
|
|
209
|
+
*
|
|
210
|
+
* @param ctx - Inbound request auth context
|
|
211
|
+
* @param scope - Required scope or permission string
|
|
212
|
+
* @throws ForbiddenError if scope is missing
|
|
213
|
+
*/
|
|
214
|
+
declare function requireScope(ctx: AuthContext, scope: string): void;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @file packages/auth-server/src/middleware/auth-middleware.ts
|
|
218
|
+
* @description Universal HTTP middleware providing platform kill-switch enforcement and credential validation.
|
|
219
|
+
* @module @yuva-devlab/auth-server
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Minimal HTTP request and response interfaces for framework neutrality (Express, Fastify, Node http).
|
|
224
|
+
*/
|
|
225
|
+
interface GenericHttpRequest {
|
|
226
|
+
url?: string;
|
|
227
|
+
method?: string;
|
|
228
|
+
headers: Record<string, string | string[] | undefined>;
|
|
229
|
+
authContext?: AuthContext;
|
|
230
|
+
}
|
|
231
|
+
interface GenericHttpResponse {
|
|
232
|
+
statusCode?: number;
|
|
233
|
+
setHeader(name: string, value: string): void;
|
|
234
|
+
end(body?: string): void;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Creates a kill-switch middleware that intercepts all traffic if an application is deactivated.
|
|
238
|
+
*
|
|
239
|
+
* @param getAppStatus - Callback returning current platform status of the application
|
|
240
|
+
* @returns Middleware function returning true if allowed to proceed, false if blocked (503)
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const killSwitch = createAppKillSwitchMiddleware(() => appState.status);
|
|
245
|
+
* const allowed = await killSwitch(req, res);
|
|
246
|
+
* if (!allowed) return;
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
declare function createAppKillSwitchMiddleware(getAppStatus: () => Promise<AppStatus> | AppStatus): (req: GenericHttpRequest, res: GenericHttpResponse) => Promise<boolean>;
|
|
250
|
+
/**
|
|
251
|
+
* Options configuring request authentication middleware.
|
|
252
|
+
*/
|
|
253
|
+
interface CreateAuthMiddlewareOptions {
|
|
254
|
+
/** Whitelisted public endpoints bypassing authentication */
|
|
255
|
+
publicPaths?: readonly string[];
|
|
256
|
+
/** Function resolving an API key against store */
|
|
257
|
+
validateApiKey?: (key: string) => Promise<AuthContext | null>;
|
|
258
|
+
/** Function resolving a Bearer session token against JWKS or session store */
|
|
259
|
+
validateToken?: (token: string) => Promise<AuthContext | null>;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Universal authentication middleware verifying API keys and Bearer tokens.
|
|
263
|
+
*
|
|
264
|
+
* @param options - Configuration options
|
|
265
|
+
* @returns Middleware handler
|
|
266
|
+
*/
|
|
267
|
+
declare function createAuthMiddleware(options: CreateAuthMiddlewareOptions): (req: GenericHttpRequest, res: GenericHttpResponse) => Promise<boolean>;
|
|
268
|
+
|
|
269
|
+
export { type ApiKey, AppStatus, type AuthContext, type AuthUser, type CreateAuthMiddlewareOptions, DEFAULT_API_KEY_HEADER, DEFAULT_AUTH_HEADER, DEFAULT_BEARER_PREFIX, DEFAULT_JWKS_CACHE_TTL_MS, DEFAULT_PUBLIC_PATHS, type GenericHttpRequest, type GenericHttpResponse, HTTP_STATUS_FORBIDDEN, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_UNAUTHORIZED, type JwksConfig, JwksValidator, type JwtPayload, createAppKillSwitchMiddleware, createAuthMiddleware, requireApiKey, requireScope, requireUser };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/auth-server/src/constants/auth.constants.ts
|
|
3
|
+
* @description Centralized constants and header keys for server-side authentication.
|
|
4
|
+
* @module @yuva-devlab/auth-server
|
|
5
|
+
*/
|
|
6
|
+
/** Standard Authorization HTTP header name (lowercase for case-insensitive lookup) */
|
|
7
|
+
declare const DEFAULT_AUTH_HEADER = "authorization";
|
|
8
|
+
/** Standard machine-to-machine API key HTTP header name */
|
|
9
|
+
declare const DEFAULT_API_KEY_HEADER = "x-api-key";
|
|
10
|
+
/** Standard Bearer token prefix */
|
|
11
|
+
declare const DEFAULT_BEARER_PREFIX = "Bearer ";
|
|
12
|
+
/** Default cache time-to-live for fetched RS256 JWKS public keys (1 hour) */
|
|
13
|
+
declare const DEFAULT_JWKS_CACHE_TTL_MS = 3600000;
|
|
14
|
+
/** Default public endpoints bypassing mandatory authentication */
|
|
15
|
+
declare const DEFAULT_PUBLIC_PATHS: readonly string[];
|
|
16
|
+
/** HTTP Status Code for unauthenticated requests */
|
|
17
|
+
declare const HTTP_STATUS_UNAUTHORIZED = 401;
|
|
18
|
+
/** HTTP Status Code for unauthorized / forbidden role requests */
|
|
19
|
+
declare const HTTP_STATUS_FORBIDDEN = 403;
|
|
20
|
+
/** HTTP Status Code returned when an application is deactivated or killed */
|
|
21
|
+
declare const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @file packages/auth-server/src/types/auth.types.ts
|
|
25
|
+
* @description Type definitions and contracts for server authentication, JWKS, and app lifecycle.
|
|
26
|
+
* @module @yuva-devlab/auth-server
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Operational state of an application on the DevLab platform.
|
|
30
|
+
*/
|
|
31
|
+
declare enum AppStatus {
|
|
32
|
+
/** Application is fully operational and serving traffic */
|
|
33
|
+
ACTIVE = "ACTIVE",
|
|
34
|
+
/** Application has been administratively disabled / deactivated (Kill-Switch triggered) */
|
|
35
|
+
INACTIVE = "INACTIVE",
|
|
36
|
+
/** Application is undergoing scheduled maintenance */
|
|
37
|
+
MAINTENANCE = "MAINTENANCE"
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Authenticated user profile representation.
|
|
41
|
+
*/
|
|
42
|
+
interface AuthUser {
|
|
43
|
+
/** Canonical user UUID */
|
|
44
|
+
readonly id: string;
|
|
45
|
+
/** Primary user email address */
|
|
46
|
+
readonly email?: string;
|
|
47
|
+
/** Granted role slugs */
|
|
48
|
+
readonly roles: readonly string[];
|
|
49
|
+
/** Tenant isolation partition ID */
|
|
50
|
+
readonly tenantId?: string;
|
|
51
|
+
/** Arbitrary profile metadata */
|
|
52
|
+
readonly metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Machine-to-machine API key credential.
|
|
56
|
+
*/
|
|
57
|
+
interface ApiKey {
|
|
58
|
+
/** Primary API key UUID */
|
|
59
|
+
readonly id: string;
|
|
60
|
+
/** Secure key string */
|
|
61
|
+
readonly key: string;
|
|
62
|
+
/** Human-readable key name or description */
|
|
63
|
+
readonly name: string;
|
|
64
|
+
/** Scoped roles granted to this key */
|
|
65
|
+
readonly roles: readonly string[];
|
|
66
|
+
/** Tenant ID partition */
|
|
67
|
+
readonly tenantId?: string;
|
|
68
|
+
/** Active status flag */
|
|
69
|
+
readonly isEnabled: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Evaluated authentication context attached to inbound requests.
|
|
73
|
+
*/
|
|
74
|
+
interface AuthContext {
|
|
75
|
+
/** Whether the request carries valid credentials */
|
|
76
|
+
readonly authenticated: boolean;
|
|
77
|
+
/** Authenticated user if authenticated via session/JWT */
|
|
78
|
+
readonly user?: AuthUser;
|
|
79
|
+
/** Authenticated API key if authenticated via machine credential */
|
|
80
|
+
readonly apiKey?: ApiKey;
|
|
81
|
+
/** Consolidated roles granted across user or API key */
|
|
82
|
+
readonly roles: readonly string[];
|
|
83
|
+
/** Whether the host application is currently active on the platform */
|
|
84
|
+
readonly isAppActive: boolean;
|
|
85
|
+
/** Detailed app status */
|
|
86
|
+
readonly appStatus?: AppStatus;
|
|
87
|
+
/** Tenant partition ID */
|
|
88
|
+
readonly tenantId?: string;
|
|
89
|
+
/** Distributed trace request identifier */
|
|
90
|
+
readonly requestId?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Configuration for verifying RS256 JWTs against a remote JWKS endpoint.
|
|
94
|
+
*/
|
|
95
|
+
interface JwksConfig {
|
|
96
|
+
/** JWKS endpoint URI exposing JSON Web Keys */
|
|
97
|
+
readonly jwksUri: string;
|
|
98
|
+
/** Expected audience claim */
|
|
99
|
+
readonly audience?: string;
|
|
100
|
+
/** Expected issuer claim */
|
|
101
|
+
readonly issuer?: string;
|
|
102
|
+
/** In-memory cache duration for public keys in milliseconds */
|
|
103
|
+
readonly cacheTtlMs?: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Decoded JWT claims payload.
|
|
107
|
+
*/
|
|
108
|
+
interface JwtPayload {
|
|
109
|
+
readonly sub: string;
|
|
110
|
+
readonly iss?: string;
|
|
111
|
+
readonly aud?: string | readonly string[];
|
|
112
|
+
readonly exp?: number;
|
|
113
|
+
readonly nbf?: number;
|
|
114
|
+
readonly iat?: number;
|
|
115
|
+
readonly email?: string;
|
|
116
|
+
readonly roles?: readonly string[];
|
|
117
|
+
readonly tenantId?: string;
|
|
118
|
+
readonly [key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @file packages/auth-server/src/token/jwt-validator.ts
|
|
123
|
+
* @description In-memory cached RS256 JWKS validator for authenticating JWT session tokens.
|
|
124
|
+
* @module @yuva-devlab/auth-server
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validates JWT access tokens against an in-memory cached JWKS public key set.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* import { JwksValidator } from "@yuva-devlab/auth-server";
|
|
133
|
+
*
|
|
134
|
+
* const validator = new JwksValidator({
|
|
135
|
+
* jwksUri: "https://auth.devlab.io/.well-known/jwks.json",
|
|
136
|
+
* issuer: "https://auth.devlab.io",
|
|
137
|
+
* audience: "api.devlab.io",
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* const claims = await validator.verifyToken(token);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
declare class JwksValidator {
|
|
144
|
+
private readonly jwksUri;
|
|
145
|
+
private readonly audience?;
|
|
146
|
+
private readonly issuer?;
|
|
147
|
+
private readonly cacheTtlMs;
|
|
148
|
+
private keyCache;
|
|
149
|
+
/**
|
|
150
|
+
* Initializes the JWKS validator.
|
|
151
|
+
*
|
|
152
|
+
* @param config - JWKS configuration parameters
|
|
153
|
+
*/
|
|
154
|
+
constructor(config: JwksConfig);
|
|
155
|
+
/**
|
|
156
|
+
* Decodes a JWT token without verifying cryptographic signature.
|
|
157
|
+
*
|
|
158
|
+
* @param token - Base64Url encoded JWT string
|
|
159
|
+
* @returns Header and payload objects
|
|
160
|
+
*/
|
|
161
|
+
parseUnverified(token: string): {
|
|
162
|
+
header: Record<string, unknown>;
|
|
163
|
+
payload: JwtPayload;
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Verifies standard claims (exp, nbf, iss, aud) on a JWT token.
|
|
167
|
+
*
|
|
168
|
+
* @param token - Bearer access token
|
|
169
|
+
* @returns Validated JwtPayload claims
|
|
170
|
+
*/
|
|
171
|
+
verifyToken(token: string): Promise<JwtPayload>;
|
|
172
|
+
/**
|
|
173
|
+
* Clears cached public keys from memory.
|
|
174
|
+
*/
|
|
175
|
+
clearKeyCache(): void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @file packages/auth-server/src/guards/auth-guards.ts
|
|
180
|
+
* @description Authorization assertions and scope/role enforcement guards.
|
|
181
|
+
* @module @yuva-devlab/auth-server
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Asserts that the request is authenticated with an active user session and optional role requirements.
|
|
186
|
+
*
|
|
187
|
+
* @param ctx - Inbound request auth context
|
|
188
|
+
* @param requiredRoles - Optional list of required roles (user must possess at least one)
|
|
189
|
+
* @returns Authenticated AuthUser
|
|
190
|
+
* @throws UnauthorizedError if not authenticated as a user
|
|
191
|
+
* @throws ForbiddenError if user lacks the required roles
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* const user = requireUser(ctx, ["admin", "operator"]);
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
declare function requireUser(ctx: AuthContext, requiredRoles?: readonly string[]): AuthUser;
|
|
199
|
+
/**
|
|
200
|
+
* Asserts that the request is authenticated via a machine-to-machine API key.
|
|
201
|
+
*
|
|
202
|
+
* @param ctx - Inbound request auth context
|
|
203
|
+
* @returns Authenticated ApiKey
|
|
204
|
+
* @throws UnauthorizedError if not authenticated via an API key
|
|
205
|
+
*/
|
|
206
|
+
declare function requireApiKey(ctx: AuthContext): ApiKey;
|
|
207
|
+
/**
|
|
208
|
+
* Asserts that the authenticated context (user or API key) possesses a specific operational scope.
|
|
209
|
+
*
|
|
210
|
+
* @param ctx - Inbound request auth context
|
|
211
|
+
* @param scope - Required scope or permission string
|
|
212
|
+
* @throws ForbiddenError if scope is missing
|
|
213
|
+
*/
|
|
214
|
+
declare function requireScope(ctx: AuthContext, scope: string): void;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @file packages/auth-server/src/middleware/auth-middleware.ts
|
|
218
|
+
* @description Universal HTTP middleware providing platform kill-switch enforcement and credential validation.
|
|
219
|
+
* @module @yuva-devlab/auth-server
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Minimal HTTP request and response interfaces for framework neutrality (Express, Fastify, Node http).
|
|
224
|
+
*/
|
|
225
|
+
interface GenericHttpRequest {
|
|
226
|
+
url?: string;
|
|
227
|
+
method?: string;
|
|
228
|
+
headers: Record<string, string | string[] | undefined>;
|
|
229
|
+
authContext?: AuthContext;
|
|
230
|
+
}
|
|
231
|
+
interface GenericHttpResponse {
|
|
232
|
+
statusCode?: number;
|
|
233
|
+
setHeader(name: string, value: string): void;
|
|
234
|
+
end(body?: string): void;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Creates a kill-switch middleware that intercepts all traffic if an application is deactivated.
|
|
238
|
+
*
|
|
239
|
+
* @param getAppStatus - Callback returning current platform status of the application
|
|
240
|
+
* @returns Middleware function returning true if allowed to proceed, false if blocked (503)
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const killSwitch = createAppKillSwitchMiddleware(() => appState.status);
|
|
245
|
+
* const allowed = await killSwitch(req, res);
|
|
246
|
+
* if (!allowed) return;
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
declare function createAppKillSwitchMiddleware(getAppStatus: () => Promise<AppStatus> | AppStatus): (req: GenericHttpRequest, res: GenericHttpResponse) => Promise<boolean>;
|
|
250
|
+
/**
|
|
251
|
+
* Options configuring request authentication middleware.
|
|
252
|
+
*/
|
|
253
|
+
interface CreateAuthMiddlewareOptions {
|
|
254
|
+
/** Whitelisted public endpoints bypassing authentication */
|
|
255
|
+
publicPaths?: readonly string[];
|
|
256
|
+
/** Function resolving an API key against store */
|
|
257
|
+
validateApiKey?: (key: string) => Promise<AuthContext | null>;
|
|
258
|
+
/** Function resolving a Bearer session token against JWKS or session store */
|
|
259
|
+
validateToken?: (token: string) => Promise<AuthContext | null>;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Universal authentication middleware verifying API keys and Bearer tokens.
|
|
263
|
+
*
|
|
264
|
+
* @param options - Configuration options
|
|
265
|
+
* @returns Middleware handler
|
|
266
|
+
*/
|
|
267
|
+
declare function createAuthMiddleware(options: CreateAuthMiddlewareOptions): (req: GenericHttpRequest, res: GenericHttpResponse) => Promise<boolean>;
|
|
268
|
+
|
|
269
|
+
export { type ApiKey, AppStatus, type AuthContext, type AuthUser, type CreateAuthMiddlewareOptions, DEFAULT_API_KEY_HEADER, DEFAULT_AUTH_HEADER, DEFAULT_BEARER_PREFIX, DEFAULT_JWKS_CACHE_TTL_MS, DEFAULT_PUBLIC_PATHS, type GenericHttpRequest, type GenericHttpResponse, HTTP_STATUS_FORBIDDEN, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_UNAUTHORIZED, type JwksConfig, JwksValidator, type JwtPayload, createAppKillSwitchMiddleware, createAuthMiddleware, requireApiKey, requireScope, requireUser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { UnauthorizedError, ForbiddenError } from '@yuva-devlab/errors';
|
|
2
|
+
import { loggerWithConfig, Logger } from '@yuva-devlab/logger';
|
|
3
|
+
|
|
4
|
+
// src/constants/auth.constants.ts
|
|
5
|
+
var DEFAULT_AUTH_HEADER = "authorization";
|
|
6
|
+
var DEFAULT_API_KEY_HEADER = "x-api-key";
|
|
7
|
+
var DEFAULT_BEARER_PREFIX = "Bearer ";
|
|
8
|
+
var DEFAULT_JWKS_CACHE_TTL_MS = 36e5;
|
|
9
|
+
var DEFAULT_PUBLIC_PATHS = Object.freeze([
|
|
10
|
+
"/health",
|
|
11
|
+
"/ready",
|
|
12
|
+
"/live",
|
|
13
|
+
"/metrics"
|
|
14
|
+
]);
|
|
15
|
+
var HTTP_STATUS_UNAUTHORIZED = 401;
|
|
16
|
+
var HTTP_STATUS_FORBIDDEN = 403;
|
|
17
|
+
var HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
|
|
18
|
+
|
|
19
|
+
// src/types/auth.types.ts
|
|
20
|
+
var AppStatus = /* @__PURE__ */ ((AppStatus2) => {
|
|
21
|
+
AppStatus2["ACTIVE"] = "ACTIVE";
|
|
22
|
+
AppStatus2["INACTIVE"] = "INACTIVE";
|
|
23
|
+
AppStatus2["MAINTENANCE"] = "MAINTENANCE";
|
|
24
|
+
return AppStatus2;
|
|
25
|
+
})(AppStatus || {});
|
|
26
|
+
var logger = loggerWithConfig(new Logger("JwksValidator"));
|
|
27
|
+
var JwksValidator = class {
|
|
28
|
+
jwksUri;
|
|
29
|
+
audience;
|
|
30
|
+
issuer;
|
|
31
|
+
cacheTtlMs;
|
|
32
|
+
keyCache = /* @__PURE__ */ new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Initializes the JWKS validator.
|
|
35
|
+
*
|
|
36
|
+
* @param config - JWKS configuration parameters
|
|
37
|
+
*/
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.jwksUri = config.jwksUri;
|
|
40
|
+
this.audience = config.audience;
|
|
41
|
+
this.issuer = config.issuer;
|
|
42
|
+
this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_JWKS_CACHE_TTL_MS;
|
|
43
|
+
logger.debug("[constructor] JwksValidator initialized", {
|
|
44
|
+
jwksUri: this.jwksUri,
|
|
45
|
+
issuer: this.issuer,
|
|
46
|
+
audience: this.audience
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Decodes a JWT token without verifying cryptographic signature.
|
|
51
|
+
*
|
|
52
|
+
* @param token - Base64Url encoded JWT string
|
|
53
|
+
* @returns Header and payload objects
|
|
54
|
+
*/
|
|
55
|
+
parseUnverified(token) {
|
|
56
|
+
const parts = token.split(".");
|
|
57
|
+
if (parts.length !== 3) {
|
|
58
|
+
throw new UnauthorizedError("Malformed JWT token format");
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const header = JSON.parse(
|
|
62
|
+
Buffer.from(parts[0], "base64url").toString("utf-8")
|
|
63
|
+
);
|
|
64
|
+
const payload = JSON.parse(
|
|
65
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
66
|
+
);
|
|
67
|
+
return { header, payload };
|
|
68
|
+
} catch {
|
|
69
|
+
throw new UnauthorizedError("Unable to decode JWT payload");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Verifies standard claims (exp, nbf, iss, aud) on a JWT token.
|
|
74
|
+
*
|
|
75
|
+
* @param token - Bearer access token
|
|
76
|
+
* @returns Validated JwtPayload claims
|
|
77
|
+
*/
|
|
78
|
+
async verifyToken(token) {
|
|
79
|
+
const { payload } = this.parseUnverified(token);
|
|
80
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
81
|
+
if (typeof payload.exp === "number" && nowSeconds >= payload.exp) {
|
|
82
|
+
logger.warn("[verifyToken] Token has expired", {
|
|
83
|
+
exp: payload.exp,
|
|
84
|
+
now: nowSeconds
|
|
85
|
+
});
|
|
86
|
+
throw new UnauthorizedError("Authentication token has expired");
|
|
87
|
+
}
|
|
88
|
+
if (typeof payload.nbf === "number" && nowSeconds < payload.nbf) {
|
|
89
|
+
logger.warn("[verifyToken] Token not yet valid", {
|
|
90
|
+
nbf: payload.nbf,
|
|
91
|
+
now: nowSeconds
|
|
92
|
+
});
|
|
93
|
+
throw new UnauthorizedError("Authentication token is not yet active");
|
|
94
|
+
}
|
|
95
|
+
if (this.issuer && payload.iss && payload.iss !== this.issuer) {
|
|
96
|
+
logger.warn("[verifyToken] Issuer mismatch", {
|
|
97
|
+
expected: this.issuer,
|
|
98
|
+
actual: payload.iss
|
|
99
|
+
});
|
|
100
|
+
throw new UnauthorizedError("Token issuer mismatch");
|
|
101
|
+
}
|
|
102
|
+
if (this.audience && payload.aud) {
|
|
103
|
+
const audList = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
104
|
+
if (!audList.includes(this.audience)) {
|
|
105
|
+
logger.warn("[verifyToken] Audience mismatch", {
|
|
106
|
+
expected: this.audience,
|
|
107
|
+
actual: payload.aud
|
|
108
|
+
});
|
|
109
|
+
throw new UnauthorizedError("Token audience mismatch");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
logger.debug("[verifyToken] Successfully validated token claims", {
|
|
113
|
+
sub: payload.sub
|
|
114
|
+
});
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Clears cached public keys from memory.
|
|
119
|
+
*/
|
|
120
|
+
clearKeyCache() {
|
|
121
|
+
this.keyCache.clear();
|
|
122
|
+
logger.info("[clearKeyCache] JWKS public key cache cleared");
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var logger2 = loggerWithConfig(new Logger("AuthGuards"));
|
|
126
|
+
function requireUser(ctx, requiredRoles) {
|
|
127
|
+
if (!ctx.authenticated || !ctx.user) {
|
|
128
|
+
logger2.warn(
|
|
129
|
+
"[requireUser] Request rejected: missing authenticated user context"
|
|
130
|
+
);
|
|
131
|
+
throw new UnauthorizedError(
|
|
132
|
+
"Authentication required: valid user session required"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
136
|
+
const hasRole = requiredRoles.some((role) => ctx.roles.includes(role));
|
|
137
|
+
if (!hasRole) {
|
|
138
|
+
logger2.warn("[requireUser] User lacks required roles", {
|
|
139
|
+
userId: ctx.user.id,
|
|
140
|
+
userRoles: ctx.roles,
|
|
141
|
+
requiredRoles
|
|
142
|
+
});
|
|
143
|
+
throw new ForbiddenError(
|
|
144
|
+
`Insufficient privileges: requires one of [${requiredRoles.join(", ")}]`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return ctx.user;
|
|
149
|
+
}
|
|
150
|
+
function requireApiKey(ctx) {
|
|
151
|
+
if (!ctx.authenticated || !ctx.apiKey) {
|
|
152
|
+
logger2.warn("[requireApiKey] Request rejected: missing machine API key");
|
|
153
|
+
throw new UnauthorizedError(
|
|
154
|
+
"Authentication required: valid API key required"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return ctx.apiKey;
|
|
158
|
+
}
|
|
159
|
+
function requireScope(ctx, scope) {
|
|
160
|
+
if (!ctx.roles.includes(scope)) {
|
|
161
|
+
logger2.warn("[requireScope] Missing required scope", {
|
|
162
|
+
roles: ctx.roles,
|
|
163
|
+
requiredScope: scope
|
|
164
|
+
});
|
|
165
|
+
throw new ForbiddenError(`Missing required scope: '${scope}'`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var logger3 = loggerWithConfig(new Logger("AuthMiddleware"));
|
|
169
|
+
function createAppKillSwitchMiddleware(getAppStatus) {
|
|
170
|
+
return async (req, res) => {
|
|
171
|
+
const status = await getAppStatus();
|
|
172
|
+
if (status === "INACTIVE" /* INACTIVE */ || status === "MAINTENANCE" /* MAINTENANCE */) {
|
|
173
|
+
logger3.warn(
|
|
174
|
+
"[killSwitch] Application is inactive or under maintenance; request rejected",
|
|
175
|
+
{
|
|
176
|
+
url: req.url,
|
|
177
|
+
method: req.method,
|
|
178
|
+
appStatus: status
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
res.statusCode = HTTP_STATUS_SERVICE_UNAVAILABLE;
|
|
182
|
+
res.setHeader("Content-Type", "application/json");
|
|
183
|
+
res.end(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
error: {
|
|
186
|
+
code: "APP_DEACTIVATED",
|
|
187
|
+
message: status === "MAINTENANCE" /* MAINTENANCE */ ? "Application is currently undergoing scheduled maintenance" : "Application has been deactivated by platform administrators",
|
|
188
|
+
status
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function createAuthMiddleware(options) {
|
|
198
|
+
const publicPaths = /* @__PURE__ */ new Set([
|
|
199
|
+
...DEFAULT_PUBLIC_PATHS,
|
|
200
|
+
...options.publicPaths ?? []
|
|
201
|
+
]);
|
|
202
|
+
return async (req, res) => {
|
|
203
|
+
const urlPath = (req.url || "/").split("?")[0] || "/";
|
|
204
|
+
if (publicPaths.has(urlPath)) {
|
|
205
|
+
req.authContext = {
|
|
206
|
+
authenticated: false,
|
|
207
|
+
roles: [],
|
|
208
|
+
isAppActive: true
|
|
209
|
+
};
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
const rawApiKey = req.headers[DEFAULT_API_KEY_HEADER];
|
|
213
|
+
const apiKey = typeof rawApiKey === "string" ? rawApiKey.trim() : void 0;
|
|
214
|
+
if (apiKey && options.validateApiKey) {
|
|
215
|
+
const apiKeyCtx = await options.validateApiKey(apiKey);
|
|
216
|
+
if (apiKeyCtx?.authenticated) {
|
|
217
|
+
logger3.info("[authenticate] Authenticated via API Key", {
|
|
218
|
+
path: urlPath
|
|
219
|
+
});
|
|
220
|
+
req.authContext = apiKeyCtx;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const rawAuth = req.headers[DEFAULT_AUTH_HEADER];
|
|
225
|
+
const authHeader = typeof rawAuth === "string" ? rawAuth.trim() : void 0;
|
|
226
|
+
if (authHeader?.startsWith(DEFAULT_BEARER_PREFIX) && options.validateToken) {
|
|
227
|
+
const token = authHeader.slice(DEFAULT_BEARER_PREFIX.length).trim();
|
|
228
|
+
const tokenCtx = await options.validateToken(token);
|
|
229
|
+
if (tokenCtx?.authenticated) {
|
|
230
|
+
logger3.info("[authenticate] Authenticated via Bearer token", {
|
|
231
|
+
path: urlPath
|
|
232
|
+
});
|
|
233
|
+
req.authContext = tokenCtx;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
logger3.warn("[authenticate] Unauthorized request rejected", {
|
|
238
|
+
path: urlPath,
|
|
239
|
+
method: req.method
|
|
240
|
+
});
|
|
241
|
+
res.statusCode = HTTP_STATUS_UNAUTHORIZED;
|
|
242
|
+
res.setHeader("Content-Type", "application/json");
|
|
243
|
+
res.end(
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
error: {
|
|
246
|
+
code: "UNAUTHORIZED",
|
|
247
|
+
message: "Missing or invalid authentication credentials (Bearer token or API key required)"
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
return false;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export { AppStatus, DEFAULT_API_KEY_HEADER, DEFAULT_AUTH_HEADER, DEFAULT_BEARER_PREFIX, DEFAULT_JWKS_CACHE_TTL_MS, DEFAULT_PUBLIC_PATHS, HTTP_STATUS_FORBIDDEN, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_UNAUTHORIZED, JwksValidator, createAppKillSwitchMiddleware, createAuthMiddleware, requireApiKey, requireScope, requireUser };
|
|
256
|
+
//# sourceMappingURL=index.js.map
|
|
257
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants/auth.constants.ts","../src/types/auth.types.ts","../src/token/jwt-validator.ts","../src/guards/auth-guards.ts","../src/middleware/auth-middleware.ts"],"names":["AppStatus","logger","loggerWithConfig","Logger","UnauthorizedError"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @file packages/auth-server/src/constants/auth.constants.ts\n * @description Centralized constants and header keys for server-side authentication.\n * @module @yuva-devlab/auth-server\n */\n\n/** Standard Authorization HTTP header name (lowercase for case-insensitive lookup) */\nexport const DEFAULT_AUTH_HEADER = \"authorization\";\n\n/** Standard machine-to-machine API key HTTP header name */\nexport const DEFAULT_API_KEY_HEADER = \"x-api-key\";\n\n/** Standard Bearer token prefix */\nexport const DEFAULT_BEARER_PREFIX = \"Bearer \";\n\n/** Default cache time-to-live for fetched RS256 JWKS public keys (1 hour) */\nexport const DEFAULT_JWKS_CACHE_TTL_MS = 3_600_000;\n\n/** Default public endpoints bypassing mandatory authentication */\nexport const DEFAULT_PUBLIC_PATHS = Object.freeze([\n \"/health\",\n \"/ready\",\n \"/live\",\n \"/metrics\",\n]);\n\n/** HTTP Status Code for unauthenticated requests */\nexport const HTTP_STATUS_UNAUTHORIZED = 401;\n\n/** HTTP Status Code for unauthorized / forbidden role requests */\nexport const HTTP_STATUS_FORBIDDEN = 403;\n\n/** HTTP Status Code returned when an application is deactivated or killed */\nexport const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;\n","/**\n * @file packages/auth-server/src/types/auth.types.ts\n * @description Type definitions and contracts for server authentication, JWKS, and app lifecycle.\n * @module @yuva-devlab/auth-server\n */\n\n/**\n * Operational state of an application on the DevLab platform.\n */\nexport enum AppStatus {\n /** Application is fully operational and serving traffic */\n ACTIVE = \"ACTIVE\",\n /** Application has been administratively disabled / deactivated (Kill-Switch triggered) */\n INACTIVE = \"INACTIVE\",\n /** Application is undergoing scheduled maintenance */\n MAINTENANCE = \"MAINTENANCE\",\n}\n\n/**\n * Authenticated user profile representation.\n */\nexport interface AuthUser {\n /** Canonical user UUID */\n readonly id: string;\n /** Primary user email address */\n readonly email?: string;\n /** Granted role slugs */\n readonly roles: readonly string[];\n /** Tenant isolation partition ID */\n readonly tenantId?: string;\n /** Arbitrary profile metadata */\n readonly metadata?: Record<string, unknown>;\n}\n\n/**\n * Machine-to-machine API key credential.\n */\nexport interface ApiKey {\n /** Primary API key UUID */\n readonly id: string;\n /** Secure key string */\n readonly key: string;\n /** Human-readable key name or description */\n readonly name: string;\n /** Scoped roles granted to this key */\n readonly roles: readonly string[];\n /** Tenant ID partition */\n readonly tenantId?: string;\n /** Active status flag */\n readonly isEnabled: boolean;\n}\n\n/**\n * Evaluated authentication context attached to inbound requests.\n */\nexport interface AuthContext {\n /** Whether the request carries valid credentials */\n readonly authenticated: boolean;\n /** Authenticated user if authenticated via session/JWT */\n readonly user?: AuthUser;\n /** Authenticated API key if authenticated via machine credential */\n readonly apiKey?: ApiKey;\n /** Consolidated roles granted across user or API key */\n readonly roles: readonly string[];\n /** Whether the host application is currently active on the platform */\n readonly isAppActive: boolean;\n /** Detailed app status */\n readonly appStatus?: AppStatus;\n /** Tenant partition ID */\n readonly tenantId?: string;\n /** Distributed trace request identifier */\n readonly requestId?: string;\n}\n\n/**\n * Configuration for verifying RS256 JWTs against a remote JWKS endpoint.\n */\nexport interface JwksConfig {\n /** JWKS endpoint URI exposing JSON Web Keys */\n readonly jwksUri: string;\n /** Expected audience claim */\n readonly audience?: string;\n /** Expected issuer claim */\n readonly issuer?: string;\n /** In-memory cache duration for public keys in milliseconds */\n readonly cacheTtlMs?: number;\n}\n\n/**\n * Decoded JWT claims payload.\n */\nexport interface JwtPayload {\n readonly sub: string;\n readonly iss?: string;\n readonly aud?: string | readonly string[];\n readonly exp?: number;\n readonly nbf?: number;\n readonly iat?: number;\n readonly email?: string;\n readonly roles?: readonly string[];\n readonly tenantId?: string;\n readonly [key: string]: unknown;\n}\n","/**\n * @file packages/auth-server/src/token/jwt-validator.ts\n * @description In-memory cached RS256 JWKS validator for authenticating JWT session tokens.\n * @module @yuva-devlab/auth-server\n */\n\nimport { UnauthorizedError } from \"@yuva-devlab/errors\";\nimport { Logger, loggerWithConfig } from \"@yuva-devlab/logger\";\nimport { DEFAULT_JWKS_CACHE_TTL_MS } from \"../constants\";\nimport type { JwksConfig, JwtPayload } from \"../types\";\n\nconst logger = loggerWithConfig(new Logger(\"JwksValidator\"));\n\n/**\n * Validates JWT access tokens against an in-memory cached JWKS public key set.\n *\n * @example\n * ```typescript\n * import { JwksValidator } from \"@yuva-devlab/auth-server\";\n *\n * const validator = new JwksValidator({\n * jwksUri: \"https://auth.devlab.io/.well-known/jwks.json\",\n * issuer: \"https://auth.devlab.io\",\n * audience: \"api.devlab.io\",\n * });\n *\n * const claims = await validator.verifyToken(token);\n * ```\n */\nexport class JwksValidator {\n private readonly jwksUri: string;\n private readonly audience?: string;\n private readonly issuer?: string;\n private readonly cacheTtlMs: number;\n private keyCache = new Map<string, { key: string; expiresAt: number }>();\n\n /**\n * Initializes the JWKS validator.\n *\n * @param config - JWKS configuration parameters\n */\n public constructor(config: JwksConfig) {\n this.jwksUri = config.jwksUri;\n this.audience = config.audience;\n this.issuer = config.issuer;\n this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_JWKS_CACHE_TTL_MS;\n\n logger.debug(\"[constructor] JwksValidator initialized\", {\n jwksUri: this.jwksUri,\n issuer: this.issuer,\n audience: this.audience,\n });\n }\n\n /**\n * Decodes a JWT token without verifying cryptographic signature.\n *\n * @param token - Base64Url encoded JWT string\n * @returns Header and payload objects\n */\n public parseUnverified(token: string): {\n header: Record<string, unknown>;\n payload: JwtPayload;\n } {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new UnauthorizedError(\"Malformed JWT token format\");\n }\n\n try {\n const header = JSON.parse(\n Buffer.from(parts[0]!, \"base64url\").toString(\"utf-8\"),\n ) as Record<string, unknown>;\n const payload = JSON.parse(\n Buffer.from(parts[1]!, \"base64url\").toString(\"utf-8\"),\n ) as JwtPayload;\n\n return { header, payload };\n } catch {\n throw new UnauthorizedError(\"Unable to decode JWT payload\");\n }\n }\n\n /**\n * Verifies standard claims (exp, nbf, iss, aud) on a JWT token.\n *\n * @param token - Bearer access token\n * @returns Validated JwtPayload claims\n */\n public async verifyToken(token: string): Promise<JwtPayload> {\n const { payload } = this.parseUnverified(token);\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n // 1. Validate Expiration (exp)\n if (typeof payload.exp === \"number\" && nowSeconds >= payload.exp) {\n logger.warn(\"[verifyToken] Token has expired\", {\n exp: payload.exp,\n now: nowSeconds,\n });\n throw new UnauthorizedError(\"Authentication token has expired\");\n }\n\n // 2. Validate Not Before (nbf)\n if (typeof payload.nbf === \"number\" && nowSeconds < payload.nbf) {\n logger.warn(\"[verifyToken] Token not yet valid\", {\n nbf: payload.nbf,\n now: nowSeconds,\n });\n throw new UnauthorizedError(\"Authentication token is not yet active\");\n }\n\n // 3. Validate Issuer (iss)\n if (this.issuer && payload.iss && payload.iss !== this.issuer) {\n logger.warn(\"[verifyToken] Issuer mismatch\", {\n expected: this.issuer,\n actual: payload.iss,\n });\n throw new UnauthorizedError(\"Token issuer mismatch\");\n }\n\n // 4. Validate Audience (aud)\n if (this.audience && payload.aud) {\n const audList = Array.isArray(payload.aud) ? payload.aud : [payload.aud];\n if (!audList.includes(this.audience)) {\n logger.warn(\"[verifyToken] Audience mismatch\", {\n expected: this.audience,\n actual: payload.aud,\n });\n throw new UnauthorizedError(\"Token audience mismatch\");\n }\n }\n\n logger.debug(\"[verifyToken] Successfully validated token claims\", {\n sub: payload.sub,\n });\n return payload;\n }\n\n /**\n * Clears cached public keys from memory.\n */\n public clearKeyCache(): void {\n this.keyCache.clear();\n logger.info(\"[clearKeyCache] JWKS public key cache cleared\");\n }\n}\n","/**\n * @file packages/auth-server/src/guards/auth-guards.ts\n * @description Authorization assertions and scope/role enforcement guards.\n * @module @yuva-devlab/auth-server\n */\n\nimport { ForbiddenError, UnauthorizedError } from \"@yuva-devlab/errors\";\nimport { Logger, loggerWithConfig } from \"@yuva-devlab/logger\";\nimport type { ApiKey, AuthContext, AuthUser } from \"../types\";\n\nconst logger = loggerWithConfig(new Logger(\"AuthGuards\"));\n\n/**\n * Asserts that the request is authenticated with an active user session and optional role requirements.\n *\n * @param ctx - Inbound request auth context\n * @param requiredRoles - Optional list of required roles (user must possess at least one)\n * @returns Authenticated AuthUser\n * @throws UnauthorizedError if not authenticated as a user\n * @throws ForbiddenError if user lacks the required roles\n *\n * @example\n * ```typescript\n * const user = requireUser(ctx, [\"admin\", \"operator\"]);\n * ```\n */\nexport function requireUser(\n ctx: AuthContext,\n requiredRoles?: readonly string[],\n): AuthUser {\n if (!ctx.authenticated || !ctx.user) {\n logger.warn(\n \"[requireUser] Request rejected: missing authenticated user context\",\n );\n throw new UnauthorizedError(\n \"Authentication required: valid user session required\",\n );\n }\n\n if (requiredRoles && requiredRoles.length > 0) {\n const hasRole = requiredRoles.some((role) => ctx.roles.includes(role));\n if (!hasRole) {\n logger.warn(\"[requireUser] User lacks required roles\", {\n userId: ctx.user.id,\n userRoles: ctx.roles,\n requiredRoles,\n });\n throw new ForbiddenError(\n `Insufficient privileges: requires one of [${requiredRoles.join(\", \")}]`,\n );\n }\n }\n\n return ctx.user;\n}\n\n/**\n * Asserts that the request is authenticated via a machine-to-machine API key.\n *\n * @param ctx - Inbound request auth context\n * @returns Authenticated ApiKey\n * @throws UnauthorizedError if not authenticated via an API key\n */\nexport function requireApiKey(ctx: AuthContext): ApiKey {\n if (!ctx.authenticated || !ctx.apiKey) {\n logger.warn(\"[requireApiKey] Request rejected: missing machine API key\");\n throw new UnauthorizedError(\n \"Authentication required: valid API key required\",\n );\n }\n\n return ctx.apiKey;\n}\n\n/**\n * Asserts that the authenticated context (user or API key) possesses a specific operational scope.\n *\n * @param ctx - Inbound request auth context\n * @param scope - Required scope or permission string\n * @throws ForbiddenError if scope is missing\n */\nexport function requireScope(ctx: AuthContext, scope: string): void {\n if (!ctx.roles.includes(scope)) {\n logger.warn(\"[requireScope] Missing required scope\", {\n roles: ctx.roles,\n requiredScope: scope,\n });\n throw new ForbiddenError(`Missing required scope: '${scope}'`);\n }\n}\n","/**\n * @file packages/auth-server/src/middleware/auth-middleware.ts\n * @description Universal HTTP middleware providing platform kill-switch enforcement and credential validation.\n * @module @yuva-devlab/auth-server\n */\n\nimport { Logger, loggerWithConfig } from \"@yuva-devlab/logger\";\nimport {\n DEFAULT_API_KEY_HEADER,\n DEFAULT_AUTH_HEADER,\n DEFAULT_BEARER_PREFIX,\n DEFAULT_PUBLIC_PATHS,\n HTTP_STATUS_SERVICE_UNAVAILABLE,\n HTTP_STATUS_UNAUTHORIZED,\n} from \"../constants\";\nimport { AppStatus, type AuthContext } from \"../types\";\n\nconst logger = loggerWithConfig(new Logger(\"AuthMiddleware\"));\n\n/**\n * Minimal HTTP request and response interfaces for framework neutrality (Express, Fastify, Node http).\n */\nexport interface GenericHttpRequest {\n url?: string;\n method?: string;\n headers: Record<string, string | string[] | undefined>;\n authContext?: AuthContext;\n}\n\nexport interface GenericHttpResponse {\n statusCode?: number;\n setHeader(name: string, value: string): void;\n end(body?: string): void;\n}\n\n/**\n * Creates a kill-switch middleware that intercepts all traffic if an application is deactivated.\n *\n * @param getAppStatus - Callback returning current platform status of the application\n * @returns Middleware function returning true if allowed to proceed, false if blocked (503)\n *\n * @example\n * ```typescript\n * const killSwitch = createAppKillSwitchMiddleware(() => appState.status);\n * const allowed = await killSwitch(req, res);\n * if (!allowed) return;\n * ```\n */\nexport function createAppKillSwitchMiddleware(\n getAppStatus: () => Promise<AppStatus> | AppStatus,\n) {\n return async (\n req: GenericHttpRequest,\n res: GenericHttpResponse,\n ): Promise<boolean> => {\n const status = await getAppStatus();\n\n // If application has been administratively disabled or put into maintenance\n if (status === AppStatus.INACTIVE || status === AppStatus.MAINTENANCE) {\n logger.warn(\n \"[killSwitch] Application is inactive or under maintenance; request rejected\",\n {\n url: req.url,\n method: req.method,\n appStatus: status,\n },\n );\n\n res.statusCode = HTTP_STATUS_SERVICE_UNAVAILABLE;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: {\n code: \"APP_DEACTIVATED\",\n message:\n status === AppStatus.MAINTENANCE\n ? \"Application is currently undergoing scheduled maintenance\"\n : \"Application has been deactivated by platform administrators\",\n status,\n },\n }),\n );\n return false;\n }\n\n return true;\n };\n}\n\n/**\n * Options configuring request authentication middleware.\n */\nexport interface CreateAuthMiddlewareOptions {\n /** Whitelisted public endpoints bypassing authentication */\n publicPaths?: readonly string[];\n /** Function resolving an API key against store */\n validateApiKey?: (key: string) => Promise<AuthContext | null>;\n /** Function resolving a Bearer session token against JWKS or session store */\n validateToken?: (token: string) => Promise<AuthContext | null>;\n}\n\n/**\n * Universal authentication middleware verifying API keys and Bearer tokens.\n *\n * @param options - Configuration options\n * @returns Middleware handler\n */\nexport function createAuthMiddleware(options: CreateAuthMiddlewareOptions) {\n const publicPaths = new Set([\n ...DEFAULT_PUBLIC_PATHS,\n ...(options.publicPaths ?? []),\n ]);\n\n return async (\n req: GenericHttpRequest,\n res: GenericHttpResponse,\n ): Promise<boolean> => {\n const urlPath = (req.url || \"/\").split(\"?\")[0] || \"/\";\n\n // 1. Bypass authentication for whitelisted public paths\n if (publicPaths.has(urlPath)) {\n req.authContext = {\n authenticated: false,\n roles: [],\n isAppActive: true,\n };\n return true;\n }\n\n // 2. Check for Machine-to-Machine API Key\n const rawApiKey = req.headers[DEFAULT_API_KEY_HEADER];\n const apiKey = typeof rawApiKey === \"string\" ? rawApiKey.trim() : undefined;\n\n if (apiKey && options.validateApiKey) {\n const apiKeyCtx = await options.validateApiKey(apiKey);\n if (apiKeyCtx?.authenticated) {\n logger.info(\"[authenticate] Authenticated via API Key\", {\n path: urlPath,\n });\n req.authContext = apiKeyCtx;\n return true;\n }\n }\n\n // 3. Check for Bearer Token\n const rawAuth = req.headers[DEFAULT_AUTH_HEADER];\n const authHeader = typeof rawAuth === \"string\" ? rawAuth.trim() : undefined;\n\n if (\n authHeader?.startsWith(DEFAULT_BEARER_PREFIX) &&\n options.validateToken\n ) {\n const token = authHeader.slice(DEFAULT_BEARER_PREFIX.length).trim();\n const tokenCtx = await options.validateToken(token);\n if (tokenCtx?.authenticated) {\n logger.info(\"[authenticate] Authenticated via Bearer token\", {\n path: urlPath,\n });\n req.authContext = tokenCtx;\n return true;\n }\n }\n\n // 4. Reject with 401 Unauthorized\n logger.warn(\"[authenticate] Unauthorized request rejected\", {\n path: urlPath,\n method: req.method,\n });\n\n res.statusCode = HTTP_STATUS_UNAUTHORIZED;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: {\n code: \"UNAUTHORIZED\",\n message:\n \"Missing or invalid authentication credentials (Bearer token or API key required)\",\n },\n }),\n );\n return false;\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yuva-devlab/auth-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Server-side authentication guards, cached JWKS validator, and kill-switch middleware for DevLab",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": {
|
|
12
|
+
"import": "./dist/index.d.ts",
|
|
13
|
+
"require": "./dist/index.d.cts"
|
|
14
|
+
},
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"zod": "^3.25.76",
|
|
29
|
+
"@yuva-devlab/errors": "0.2.0",
|
|
30
|
+
"@yuva-devlab/logger": "1.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^24.10.1",
|
|
34
|
+
"eslint": "^9.39.1",
|
|
35
|
+
"prettier": "^3.9.8",
|
|
36
|
+
"tsup": "^8.5.1",
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"@yuva-devlab/eslint-config": "0.1.1",
|
|
39
|
+
"@yuva-devlab/prettier-config": "0.1.1",
|
|
40
|
+
"@yuva-devlab/typescript-config": "0.1.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"dev": "tsup --watch",
|
|
45
|
+
"lint": "eslint src",
|
|
46
|
+
"lint:fix": "eslint src --fix",
|
|
47
|
+
"format": "prettier --write .",
|
|
48
|
+
"format:check": "prettier --check .",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"clean": "rm -rf dist node_modules .turbo"
|
|
51
|
+
}
|
|
6
52
|
}
|
package/README.md
DELETED