@xtaskjs/security 1.0.0 → 1.0.3
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/CHANGELOG.md +24 -0
- package/dist/authentication.d.ts +6 -0
- package/dist/authentication.js +20 -0
- package/dist/authorization.d.ts +6 -0
- package/dist/authorization.js +51 -0
- package/dist/configuration.d.ts +8 -0
- package/dist/configuration.js +70 -0
- package/dist/decorators.d.ts +5 -0
- package/dist/decorators.js +54 -0
- package/dist/guards.d.ts +3 -0
- package/dist/guards.js +50 -0
- package/{src/index.ts → dist/index.d.ts} +1 -2
- package/dist/index.js +25 -0
- package/dist/lifecycle.d.ts +25 -0
- package/dist/lifecycle.js +314 -0
- package/dist/metadata.d.ts +6 -0
- package/dist/metadata.js +103 -0
- package/dist/strategies.d.ts +16 -0
- package/dist/strategies.js +173 -0
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.js +19 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +2 -0
- package/package.json +11 -5
- package/babel.config.js +0 -3
- package/index.ts +0 -1
- package/jest.config.js +0 -18
- package/src/authentication.ts +0 -27
- package/src/authorization.ts +0 -66
- package/src/configuration.ts +0 -90
- package/src/decorators.ts +0 -79
- package/src/guards.ts +0 -58
- package/src/lifecycle.ts +0 -403
- package/src/metadata.ts +0 -136
- package/src/passport-ambient.d.ts +0 -22
- package/src/strategies.ts +0 -227
- package/src/tokens.ts +0 -16
- package/src/types.ts +0 -94
- package/test/security.integration.test.ts +0 -325
- package/tsconfig.json +0 -21
- package/tsconfig.test.json +0 -17
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.InjectSecurityLifecycleManager = exports.InjectPassport = exports.InjectAuthorizationService = exports.InjectAuthenticationService = exports.resetSecurityIntegration = exports.getSecurityLifecycleManager = exports.shutdownSecurityIntegration = exports.initializeSecurityIntegration = exports.SecurityLifecycleManager = void 0;
|
|
7
|
+
const core_1 = require("@xtaskjs/core");
|
|
8
|
+
const passport_1 = __importDefault(require("passport"));
|
|
9
|
+
const passport_jwt_1 = require("passport-jwt");
|
|
10
|
+
const configuration_1 = require("./configuration");
|
|
11
|
+
const tokens_1 = require("./tokens");
|
|
12
|
+
const authorization_1 = require("./authorization");
|
|
13
|
+
const authentication_1 = require("./authentication");
|
|
14
|
+
const strategies_1 = require("./strategies");
|
|
15
|
+
const createPassportInstance = () => {
|
|
16
|
+
const PassportConstructor = passport_1.default.Passport || passport_1.default.Authenticator;
|
|
17
|
+
return new PassportConstructor();
|
|
18
|
+
};
|
|
19
|
+
const createSafeExtractor = (extractor) => {
|
|
20
|
+
return (request) => {
|
|
21
|
+
try {
|
|
22
|
+
return extractor(request);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const toRecord = (value) => {
|
|
30
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
};
|
|
35
|
+
const uniqueRoles = (roles) => {
|
|
36
|
+
return Array.from(new Set(roles.filter((value) => value.length > 0)));
|
|
37
|
+
};
|
|
38
|
+
const resolveClaims = (user, info) => {
|
|
39
|
+
if (info?.claims) {
|
|
40
|
+
return toRecord(info.claims);
|
|
41
|
+
}
|
|
42
|
+
if (user?.claims) {
|
|
43
|
+
return toRecord(user.claims);
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
};
|
|
47
|
+
const resolveRoles = (definition, claims, user) => {
|
|
48
|
+
const extracted = definition.extractRoles?.(claims, user);
|
|
49
|
+
if (Array.isArray(extracted)) {
|
|
50
|
+
return uniqueRoles(extracted.map((value) => String(value).trim()));
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(user?.roles)) {
|
|
53
|
+
return uniqueRoles(user.roles.map((value) => String(value).trim()));
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(claims.roles)) {
|
|
56
|
+
return uniqueRoles(claims.roles.map((value) => String(value).trim()));
|
|
57
|
+
}
|
|
58
|
+
if (typeof claims.scope === "string") {
|
|
59
|
+
return uniqueRoles(claims.scope
|
|
60
|
+
.split(/\s+/)
|
|
61
|
+
.map((value) => value.trim())
|
|
62
|
+
.filter((value) => value.length > 0));
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
};
|
|
66
|
+
class SecurityLifecycleManager {
|
|
67
|
+
constructor() {
|
|
68
|
+
this.passportInstance = createPassportInstance();
|
|
69
|
+
this.strategyDefinitions = new Map();
|
|
70
|
+
}
|
|
71
|
+
async initialize(container) {
|
|
72
|
+
this.passportInstance = createPassportInstance();
|
|
73
|
+
this.strategyDefinitions.clear();
|
|
74
|
+
this.container = container;
|
|
75
|
+
for (const definition of (0, configuration_1.getRegisteredSecurityStrategies)()) {
|
|
76
|
+
this.strategyDefinitions.set(definition.name, definition);
|
|
77
|
+
if (definition.kind === "jwt") {
|
|
78
|
+
this.registerJwtStrategy(definition);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
this.passportInstance.use(definition.name, new strategies_1.JweStrategy(definition, () => ({ container: this.container })));
|
|
82
|
+
}
|
|
83
|
+
this.registerContainerBindings(container);
|
|
84
|
+
}
|
|
85
|
+
async destroy() {
|
|
86
|
+
this.passportInstance = createPassportInstance();
|
|
87
|
+
this.strategyDefinitions.clear();
|
|
88
|
+
this.container = undefined;
|
|
89
|
+
}
|
|
90
|
+
getPassport() {
|
|
91
|
+
return this.passportInstance;
|
|
92
|
+
}
|
|
93
|
+
async authenticateContext(context, strategyNames) {
|
|
94
|
+
const result = await this.authenticateRequest(context.request, context.response, strategyNames);
|
|
95
|
+
if (!result.success) {
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
context.auth = {
|
|
99
|
+
isAuthenticated: true,
|
|
100
|
+
strategy: result.strategy,
|
|
101
|
+
token: result.token,
|
|
102
|
+
user: result.user,
|
|
103
|
+
claims: result.claims,
|
|
104
|
+
roles: [...result.roles],
|
|
105
|
+
};
|
|
106
|
+
if (context.request && typeof context.request === "object") {
|
|
107
|
+
context.request.user = result.user;
|
|
108
|
+
context.request.auth = context.auth;
|
|
109
|
+
context.request.authInfo = result.info;
|
|
110
|
+
}
|
|
111
|
+
if (context.response && typeof context.response === "object") {
|
|
112
|
+
context.response.locals = context.response.locals || {};
|
|
113
|
+
context.response.locals.auth = context.auth;
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
async authenticateRequest(request, _response, strategyNames) {
|
|
118
|
+
const selectedStrategies = this.resolveStrategyNames(strategyNames);
|
|
119
|
+
let lastFailure = {
|
|
120
|
+
statusCode: 401,
|
|
121
|
+
challenge: { message: "Unauthorized" },
|
|
122
|
+
};
|
|
123
|
+
for (const strategyName of selectedStrategies) {
|
|
124
|
+
const definition = this.strategyDefinitions.get(strategyName);
|
|
125
|
+
if (!definition) {
|
|
126
|
+
throw new Error(`Security strategy '${strategyName}' is not registered`);
|
|
127
|
+
}
|
|
128
|
+
const extractor = definition.jwtFromRequest || strategies_1.defaultBearerTokenExtractor;
|
|
129
|
+
const token = extractor(request) || undefined;
|
|
130
|
+
const outcome = await this.runPassportStrategy(strategyName, request);
|
|
131
|
+
if (outcome.type === "success") {
|
|
132
|
+
const claims = resolveClaims(outcome.user, outcome.info);
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
strategy: strategyName,
|
|
136
|
+
token,
|
|
137
|
+
user: outcome.user,
|
|
138
|
+
claims,
|
|
139
|
+
roles: resolveRoles(definition, claims, outcome.user),
|
|
140
|
+
info: outcome.info,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (outcome.type === "fail") {
|
|
144
|
+
lastFailure = { statusCode: outcome.statusCode, challenge: outcome.challenge };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
statusCode: lastFailure.statusCode || 401,
|
|
150
|
+
challenge: lastFailure.challenge || { message: "Unauthorized" },
|
|
151
|
+
roles: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
resolveStrategyNames(strategyNames) {
|
|
155
|
+
if (Array.isArray(strategyNames) && strategyNames.length > 0) {
|
|
156
|
+
return strategyNames;
|
|
157
|
+
}
|
|
158
|
+
if (typeof strategyNames === "string" && strategyNames.trim().length > 0) {
|
|
159
|
+
return [strategyNames.trim()];
|
|
160
|
+
}
|
|
161
|
+
const defaultStrategy = (0, configuration_1.getDefaultSecurityStrategyName)();
|
|
162
|
+
if (defaultStrategy) {
|
|
163
|
+
return [defaultStrategy];
|
|
164
|
+
}
|
|
165
|
+
throw new Error("No security strategy registered. Call registerJwtStrategy() or registerJweStrategy().");
|
|
166
|
+
}
|
|
167
|
+
registerJwtStrategy(definition) {
|
|
168
|
+
const extractor = createSafeExtractor(definition.jwtFromRequest || passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken());
|
|
169
|
+
const options = {
|
|
170
|
+
jwtFromRequest: extractor,
|
|
171
|
+
secretOrKey: definition.secretOrKey,
|
|
172
|
+
secretOrKeyProvider: definition.secretOrKeyProvider,
|
|
173
|
+
issuer: definition.issuer,
|
|
174
|
+
audience: definition.audience,
|
|
175
|
+
algorithms: definition.algorithms,
|
|
176
|
+
ignoreExpiration: definition.ignoreExpiration,
|
|
177
|
+
jsonWebTokenOptions: definition.jsonWebTokenOptions,
|
|
178
|
+
passReqToCallback: definition.passReqToCallback === true,
|
|
179
|
+
};
|
|
180
|
+
const verify = async (...args) => {
|
|
181
|
+
const done = args[args.length - 1];
|
|
182
|
+
const request = definition.passReqToCallback ? args[0] : undefined;
|
|
183
|
+
const payload = toRecord(definition.passReqToCallback ? args[1] : args[0]);
|
|
184
|
+
try {
|
|
185
|
+
const token = extractor(request) || "";
|
|
186
|
+
const user = await (0, strategies_1.resolveValidatedUser)(definition, payload, {
|
|
187
|
+
container: this.container,
|
|
188
|
+
request,
|
|
189
|
+
token,
|
|
190
|
+
strategyName: definition.name,
|
|
191
|
+
kind: definition.kind,
|
|
192
|
+
});
|
|
193
|
+
if (!user) {
|
|
194
|
+
done(null, false, { message: "Unauthorized", claims: payload });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
done(null, user, {
|
|
198
|
+
claims: payload,
|
|
199
|
+
strategy: definition.name,
|
|
200
|
+
kind: definition.kind,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
done(error);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
this.passportInstance.use(definition.name, new passport_jwt_1.Strategy(options, verify));
|
|
208
|
+
}
|
|
209
|
+
runPassportStrategy(strategyName, request) {
|
|
210
|
+
const strategy = this.passportInstance._strategy(strategyName);
|
|
211
|
+
if (!strategy) {
|
|
212
|
+
throw new Error(`Passport strategy '${strategyName}' is not registered`);
|
|
213
|
+
}
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const delegate = Object.create(strategy);
|
|
216
|
+
Object.assign(delegate, strategy);
|
|
217
|
+
delegate.success = (user, info) => resolve({ type: "success", user, info });
|
|
218
|
+
delegate.fail = (challenge, statusCode) => resolve({ type: "fail", challenge, statusCode });
|
|
219
|
+
delegate.pass = () => resolve({ type: "pass" });
|
|
220
|
+
delegate.error = (error) => reject(error);
|
|
221
|
+
delegate.redirect = (_url, statusCode) => resolve({
|
|
222
|
+
type: "fail",
|
|
223
|
+
challenge: { message: "Redirect responses are not supported by xtaskjs security guards" },
|
|
224
|
+
statusCode: statusCode || 401,
|
|
225
|
+
});
|
|
226
|
+
delegate.authenticate(request, {});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
registerContainerBindings(container) {
|
|
230
|
+
if (!container) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const anyContainer = container;
|
|
234
|
+
if (typeof anyContainer.registerWithName === "function") {
|
|
235
|
+
anyContainer.registerWithName(authentication_1.SecurityAuthenticationService, { scope: "singleton" }, (0, tokens_1.getAuthenticationServiceToken)());
|
|
236
|
+
anyContainer.registerWithName(authorization_1.SecurityAuthorizationService, { scope: "singleton" }, (0, tokens_1.getAuthorizationServiceToken)());
|
|
237
|
+
}
|
|
238
|
+
if (typeof anyContainer.registerNamedInstance === "function") {
|
|
239
|
+
anyContainer.registerNamedInstance((0, tokens_1.getPassportToken)(), this.passportInstance);
|
|
240
|
+
anyContainer.registerNamedInstance((0, tokens_1.getSecurityLifecycleToken)(), this);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.SecurityLifecycleManager = SecurityLifecycleManager;
|
|
245
|
+
const lifecycleManager = new SecurityLifecycleManager();
|
|
246
|
+
const initializeSecurityIntegration = async (container) => {
|
|
247
|
+
await lifecycleManager.initialize(container);
|
|
248
|
+
};
|
|
249
|
+
exports.initializeSecurityIntegration = initializeSecurityIntegration;
|
|
250
|
+
const shutdownSecurityIntegration = async () => {
|
|
251
|
+
await lifecycleManager.destroy();
|
|
252
|
+
};
|
|
253
|
+
exports.shutdownSecurityIntegration = shutdownSecurityIntegration;
|
|
254
|
+
const getSecurityLifecycleManager = () => {
|
|
255
|
+
return lifecycleManager;
|
|
256
|
+
};
|
|
257
|
+
exports.getSecurityLifecycleManager = getSecurityLifecycleManager;
|
|
258
|
+
const resetSecurityIntegration = async () => {
|
|
259
|
+
await (0, exports.shutdownSecurityIntegration)();
|
|
260
|
+
(0, configuration_1.clearRegisteredSecurityStrategies)();
|
|
261
|
+
};
|
|
262
|
+
exports.resetSecurityIntegration = resetSecurityIntegration;
|
|
263
|
+
const InjectAuthenticationService = () => {
|
|
264
|
+
return (target, propertyKey, parameterIndex) => {
|
|
265
|
+
const token = (0, tokens_1.getAuthenticationServiceToken)();
|
|
266
|
+
if (typeof parameterIndex === "number") {
|
|
267
|
+
(0, core_1.Qualifier)(token)(target, propertyKey, parameterIndex);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (propertyKey !== undefined) {
|
|
271
|
+
(0, core_1.AutoWired)({ qualifier: token })(target, propertyKey);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
exports.InjectAuthenticationService = InjectAuthenticationService;
|
|
276
|
+
const InjectAuthorizationService = () => {
|
|
277
|
+
return (target, propertyKey, parameterIndex) => {
|
|
278
|
+
const token = (0, tokens_1.getAuthorizationServiceToken)();
|
|
279
|
+
if (typeof parameterIndex === "number") {
|
|
280
|
+
(0, core_1.Qualifier)(token)(target, propertyKey, parameterIndex);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (propertyKey !== undefined) {
|
|
284
|
+
(0, core_1.AutoWired)({ qualifier: token })(target, propertyKey);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
exports.InjectAuthorizationService = InjectAuthorizationService;
|
|
289
|
+
const InjectPassport = () => {
|
|
290
|
+
return (target, propertyKey, parameterIndex) => {
|
|
291
|
+
const token = (0, tokens_1.getPassportToken)();
|
|
292
|
+
if (typeof parameterIndex === "number") {
|
|
293
|
+
(0, core_1.Qualifier)(token)(target, propertyKey, parameterIndex);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (propertyKey !== undefined) {
|
|
297
|
+
(0, core_1.AutoWired)({ qualifier: token })(target, propertyKey);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
exports.InjectPassport = InjectPassport;
|
|
302
|
+
const InjectSecurityLifecycleManager = () => {
|
|
303
|
+
return (target, propertyKey, parameterIndex) => {
|
|
304
|
+
const token = (0, tokens_1.getSecurityLifecycleToken)();
|
|
305
|
+
if (typeof parameterIndex === "number") {
|
|
306
|
+
(0, core_1.Qualifier)(token)(target, propertyKey, parameterIndex);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (propertyKey !== undefined) {
|
|
310
|
+
(0, core_1.AutoWired)({ qualifier: token })(target, propertyKey);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
exports.InjectSecurityLifecycleManager = InjectSecurityLifecycleManager;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ResolvedSecurityMetadata, RolesOptions } from "./types";
|
|
2
|
+
export declare const normalizeStrategies: (strategies?: string | string[]) => string[];
|
|
3
|
+
export declare const setAuthenticatedMetadata: (target: any, propertyKey: PropertyKey | undefined, strategies?: string | string[]) => void;
|
|
4
|
+
export declare const setRolesMetadata: (target: any, propertyKey: PropertyKey | undefined, options: RolesOptions) => void;
|
|
5
|
+
export declare const setAllowAnonymousMetadata: (target: any, propertyKey?: PropertyKey) => void;
|
|
6
|
+
export declare const resolveSecurityMetadata: (controllerType: any, handler?: PropertyKey) => ResolvedSecurityMetadata;
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveSecurityMetadata = exports.setAllowAnonymousMetadata = exports.setRolesMetadata = exports.setAuthenticatedMetadata = exports.normalizeStrategies = void 0;
|
|
4
|
+
const CLASS_SECURITY_METADATA_KEY = Symbol("xtask:security:class");
|
|
5
|
+
const METHOD_SECURITY_METADATA_KEY = Symbol("xtask:security:method");
|
|
6
|
+
const emptyMetadata = () => ({
|
|
7
|
+
allowAnonymous: false,
|
|
8
|
+
strategies: [],
|
|
9
|
+
roles: [],
|
|
10
|
+
roleMode: "any",
|
|
11
|
+
});
|
|
12
|
+
const uniqueStrings = (values) => {
|
|
13
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
14
|
+
};
|
|
15
|
+
const getClassMetadata = (target) => {
|
|
16
|
+
return Reflect.getMetadata(CLASS_SECURITY_METADATA_KEY, target) || emptyMetadata();
|
|
17
|
+
};
|
|
18
|
+
const setClassMetadata = (target, metadata) => {
|
|
19
|
+
Reflect.defineMetadata(CLASS_SECURITY_METADATA_KEY, metadata, target);
|
|
20
|
+
};
|
|
21
|
+
const getMethodMetadataMap = (target) => {
|
|
22
|
+
const stored = Reflect.getMetadata(METHOD_SECURITY_METADATA_KEY, target);
|
|
23
|
+
return stored || new Map();
|
|
24
|
+
};
|
|
25
|
+
const setMethodMetadataMap = (target, metadataMap) => {
|
|
26
|
+
Reflect.defineMetadata(METHOD_SECURITY_METADATA_KEY, metadataMap, target);
|
|
27
|
+
};
|
|
28
|
+
const getOrCreateMethodMetadata = (target, propertyKey) => {
|
|
29
|
+
const metadataMap = getMethodMetadataMap(target.constructor);
|
|
30
|
+
const current = metadataMap.get(propertyKey) || emptyMetadata();
|
|
31
|
+
metadataMap.set(propertyKey, current);
|
|
32
|
+
setMethodMetadataMap(target.constructor, metadataMap);
|
|
33
|
+
return current;
|
|
34
|
+
};
|
|
35
|
+
const normalizeStrategies = (strategies) => {
|
|
36
|
+
if (!strategies) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return uniqueStrings(Array.isArray(strategies) ? strategies : [strategies]);
|
|
40
|
+
};
|
|
41
|
+
exports.normalizeStrategies = normalizeStrategies;
|
|
42
|
+
const setAuthenticatedMetadata = (target, propertyKey, strategies) => {
|
|
43
|
+
const normalizedStrategies = (0, exports.normalizeStrategies)(strategies);
|
|
44
|
+
if (propertyKey === undefined) {
|
|
45
|
+
const current = getClassMetadata(target);
|
|
46
|
+
current.allowAnonymous = false;
|
|
47
|
+
current.strategies = uniqueStrings([...(current.strategies || []), ...normalizedStrategies]);
|
|
48
|
+
setClassMetadata(target, current);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const current = getOrCreateMethodMetadata(target, propertyKey);
|
|
52
|
+
current.allowAnonymous = false;
|
|
53
|
+
current.strategies = uniqueStrings([...(current.strategies || []), ...normalizedStrategies]);
|
|
54
|
+
};
|
|
55
|
+
exports.setAuthenticatedMetadata = setAuthenticatedMetadata;
|
|
56
|
+
const setRolesMetadata = (target, propertyKey, options) => {
|
|
57
|
+
const roles = uniqueStrings(options.roles || []);
|
|
58
|
+
const strategies = (0, exports.normalizeStrategies)(options.strategies);
|
|
59
|
+
const roleMode = options.mode || "any";
|
|
60
|
+
if (propertyKey === undefined) {
|
|
61
|
+
const current = getClassMetadata(target);
|
|
62
|
+
current.roles = uniqueStrings([...(current.roles || []), ...roles]);
|
|
63
|
+
current.strategies = uniqueStrings([...(current.strategies || []), ...strategies]);
|
|
64
|
+
current.roleMode = roleMode;
|
|
65
|
+
setClassMetadata(target, current);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const current = getOrCreateMethodMetadata(target, propertyKey);
|
|
69
|
+
current.roles = uniqueStrings([...(current.roles || []), ...roles]);
|
|
70
|
+
current.strategies = uniqueStrings([...(current.strategies || []), ...strategies]);
|
|
71
|
+
current.roleMode = roleMode;
|
|
72
|
+
};
|
|
73
|
+
exports.setRolesMetadata = setRolesMetadata;
|
|
74
|
+
const setAllowAnonymousMetadata = (target, propertyKey) => {
|
|
75
|
+
if (propertyKey === undefined) {
|
|
76
|
+
const current = getClassMetadata(target);
|
|
77
|
+
current.allowAnonymous = true;
|
|
78
|
+
setClassMetadata(target, current);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const current = getOrCreateMethodMetadata(target, propertyKey);
|
|
82
|
+
current.allowAnonymous = true;
|
|
83
|
+
};
|
|
84
|
+
exports.setAllowAnonymousMetadata = setAllowAnonymousMetadata;
|
|
85
|
+
const resolveSecurityMetadata = (controllerType, handler) => {
|
|
86
|
+
const classMetadata = controllerType ? getClassMetadata(controllerType) : emptyMetadata();
|
|
87
|
+
const methodMetadata = controllerType && handler !== undefined
|
|
88
|
+
? getMethodMetadataMap(controllerType).get(handler) || emptyMetadata()
|
|
89
|
+
: emptyMetadata();
|
|
90
|
+
const allowAnonymous = Boolean(methodMetadata.allowAnonymous ?? classMetadata.allowAnonymous ?? false);
|
|
91
|
+
const strategies = uniqueStrings([
|
|
92
|
+
...(classMetadata.strategies || []),
|
|
93
|
+
...(methodMetadata.strategies || []),
|
|
94
|
+
]);
|
|
95
|
+
const roles = uniqueStrings([...(classMetadata.roles || []), ...(methodMetadata.roles || [])]);
|
|
96
|
+
return {
|
|
97
|
+
allowAnonymous,
|
|
98
|
+
strategies,
|
|
99
|
+
roles,
|
|
100
|
+
roleMode: methodMetadata.roleMode || classMetadata.roleMode || "any",
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
exports.resolveSecurityMetadata = resolveSecurityMetadata;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Strategy as PassportStrategyBase } from "passport-strategy";
|
|
2
|
+
import { RegisteredJweSecurityStrategyOptions, RegisteredSecurityStrategyOptions, SecurityValidationContext } from "./types";
|
|
3
|
+
export declare const defaultBearerTokenExtractor: (request: any) => string | null;
|
|
4
|
+
export declare const resolveValidatedUser: (definition: RegisteredSecurityStrategyOptions, payload: Record<string, any>, context: SecurityValidationContext) => Promise<any | false>;
|
|
5
|
+
export declare class JweStrategy extends PassportStrategyBase {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
success: (user: any, info?: any) => void;
|
|
8
|
+
fail: (challenge?: any, status?: number) => void;
|
|
9
|
+
error: (error: Error) => void;
|
|
10
|
+
pass: () => void;
|
|
11
|
+
private readonly definition;
|
|
12
|
+
private readonly resolveValidationContext?;
|
|
13
|
+
constructor(definition: RegisteredJweSecurityStrategyOptions, resolveValidationContext?: () => Partial<SecurityValidationContext>);
|
|
14
|
+
authenticate(request: any): void;
|
|
15
|
+
private run;
|
|
16
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JweStrategy = exports.resolveValidatedUser = exports.defaultBearerTokenExtractor = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const passport_strategy_1 = require("passport-strategy");
|
|
6
|
+
const defaultBearerTokenExtractor = (request) => {
|
|
7
|
+
const headerValue = request?.headers?.authorization ||
|
|
8
|
+
request?.headers?.Authorization ||
|
|
9
|
+
request?.authorization;
|
|
10
|
+
if (typeof headerValue !== "string") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const match = headerValue.match(/^Bearer\s+(.+)$/i);
|
|
14
|
+
return match?.[1] || null;
|
|
15
|
+
};
|
|
16
|
+
exports.defaultBearerTokenExtractor = defaultBearerTokenExtractor;
|
|
17
|
+
const decodeBase64Url = (value) => {
|
|
18
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
19
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
|
|
20
|
+
return Buffer.from(`${normalized}${padding}`, "base64");
|
|
21
|
+
};
|
|
22
|
+
const decodeJsonSegment = (value) => {
|
|
23
|
+
return ensureRecord(JSON.parse(decodeBase64Url(value).toString("utf-8")));
|
|
24
|
+
};
|
|
25
|
+
const normalizeDecryptionKey = (value) => {
|
|
26
|
+
if (Buffer.isBuffer(value)) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (value instanceof Uint8Array) {
|
|
30
|
+
return Buffer.from(value);
|
|
31
|
+
}
|
|
32
|
+
return Buffer.from(String(value), "utf-8");
|
|
33
|
+
};
|
|
34
|
+
const toUnixTime = () => Math.floor(Date.now() / 1000);
|
|
35
|
+
const toClockToleranceSeconds = (value) => {
|
|
36
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
const parsed = Number(value);
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
};
|
|
45
|
+
const normalizeAudience = (value) => {
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.map((item) => String(item));
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
return [value];
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
};
|
|
54
|
+
const validateJweClaims = (payload, definition) => {
|
|
55
|
+
const now = toUnixTime();
|
|
56
|
+
const clockTolerance = toClockToleranceSeconds(definition.clockTolerance);
|
|
57
|
+
if (typeof payload.exp === "number" && now - clockTolerance >= payload.exp) {
|
|
58
|
+
throw new Error("Token expired");
|
|
59
|
+
}
|
|
60
|
+
if (typeof payload.nbf === "number" && now + clockTolerance < payload.nbf) {
|
|
61
|
+
throw new Error("Token is not active yet");
|
|
62
|
+
}
|
|
63
|
+
if (definition.issuer) {
|
|
64
|
+
const expectedIssuers = Array.isArray(definition.issuer)
|
|
65
|
+
? definition.issuer
|
|
66
|
+
: [definition.issuer];
|
|
67
|
+
if (!expectedIssuers.includes(String(payload.iss || ""))) {
|
|
68
|
+
throw new Error("Token issuer is not allowed");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (definition.audience) {
|
|
72
|
+
const expectedAudiences = Array.isArray(definition.audience)
|
|
73
|
+
? definition.audience
|
|
74
|
+
: [definition.audience];
|
|
75
|
+
const actualAudiences = normalizeAudience(payload.aud);
|
|
76
|
+
const hasAudience = expectedAudiences.some((audience) => actualAudiences.includes(audience));
|
|
77
|
+
if (!hasAudience) {
|
|
78
|
+
throw new Error("Token audience is not allowed");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const ensureRecord = (value) => {
|
|
83
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
return {};
|
|
87
|
+
};
|
|
88
|
+
const resolveValidatedUser = async (definition, payload, context) => {
|
|
89
|
+
if (!definition.validate) {
|
|
90
|
+
return payload;
|
|
91
|
+
}
|
|
92
|
+
return definition.validate(payload, context);
|
|
93
|
+
};
|
|
94
|
+
exports.resolveValidatedUser = resolveValidatedUser;
|
|
95
|
+
const toJweKey = async (definition) => {
|
|
96
|
+
const key = typeof definition.resolveDecryptionKey === "function"
|
|
97
|
+
? await definition.resolveDecryptionKey()
|
|
98
|
+
: definition.decryptionKey;
|
|
99
|
+
if (typeof key === "string") {
|
|
100
|
+
return new TextEncoder().encode(key);
|
|
101
|
+
}
|
|
102
|
+
return key;
|
|
103
|
+
};
|
|
104
|
+
class JweStrategy extends passport_strategy_1.Strategy {
|
|
105
|
+
constructor(definition, resolveValidationContext) {
|
|
106
|
+
super();
|
|
107
|
+
this.definition = definition;
|
|
108
|
+
this.name = definition.name;
|
|
109
|
+
this.resolveValidationContext = resolveValidationContext;
|
|
110
|
+
}
|
|
111
|
+
authenticate(request) {
|
|
112
|
+
void this.run(request);
|
|
113
|
+
}
|
|
114
|
+
async run(request) {
|
|
115
|
+
const extractor = this.definition.jwtFromRequest || exports.defaultBearerTokenExtractor;
|
|
116
|
+
const token = extractor(request);
|
|
117
|
+
if (!token) {
|
|
118
|
+
this.fail({ message: "Missing bearer token" }, 401);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const [protectedHeader, encryptedKey, iv, ciphertext, tag] = token.split(".");
|
|
123
|
+
if (!protectedHeader || encryptedKey === undefined || !iv || !ciphertext || !tag) {
|
|
124
|
+
throw new Error("Malformed JWE token");
|
|
125
|
+
}
|
|
126
|
+
const header = decodeJsonSegment(protectedHeader);
|
|
127
|
+
if (header.alg !== "dir") {
|
|
128
|
+
throw new Error(`Unsupported JWE alg '${String(header.alg || "")}'`);
|
|
129
|
+
}
|
|
130
|
+
if (header.enc !== "A256GCM") {
|
|
131
|
+
throw new Error(`Unsupported JWE enc '${String(header.enc || "")}'`);
|
|
132
|
+
}
|
|
133
|
+
if (encryptedKey.length > 0) {
|
|
134
|
+
throw new Error("Compact JWE with direct encryption must not include an encrypted key segment");
|
|
135
|
+
}
|
|
136
|
+
const key = normalizeDecryptionKey(await toJweKey(this.definition));
|
|
137
|
+
if (key.length !== 32) {
|
|
138
|
+
throw new Error("JWE decryption key must be 32 bytes for dir/A256GCM tokens");
|
|
139
|
+
}
|
|
140
|
+
const decipher = (0, crypto_1.createDecipheriv)("aes-256-gcm", key, decodeBase64Url(iv));
|
|
141
|
+
decipher.setAAD(Buffer.from(protectedHeader, "utf-8"));
|
|
142
|
+
decipher.setAuthTag(decodeBase64Url(tag));
|
|
143
|
+
const plaintext = Buffer.concat([
|
|
144
|
+
decipher.update(decodeBase64Url(ciphertext)),
|
|
145
|
+
decipher.final(),
|
|
146
|
+
]);
|
|
147
|
+
const payload = ensureRecord(JSON.parse(plaintext.toString("utf-8")));
|
|
148
|
+
validateJweClaims(payload, this.definition);
|
|
149
|
+
const user = await (0, exports.resolveValidatedUser)(this.definition, payload, {
|
|
150
|
+
...this.resolveValidationContext?.(),
|
|
151
|
+
request,
|
|
152
|
+
token,
|
|
153
|
+
strategyName: this.definition.name,
|
|
154
|
+
kind: this.definition.kind,
|
|
155
|
+
});
|
|
156
|
+
if (!user) {
|
|
157
|
+
this.fail({ message: "Unauthorized", claims: payload }, 401);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.success(user, {
|
|
161
|
+
claims: payload,
|
|
162
|
+
protectedHeader: header,
|
|
163
|
+
strategy: this.definition.name,
|
|
164
|
+
kind: this.definition.kind,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const message = typeof error?.message === "string" ? error.message : "Unauthorized";
|
|
169
|
+
this.fail({ message }, 401);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.JweStrategy = JweStrategy;
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const SECURITY_PASSPORT_TOKEN = "xtask:security:passport";
|
|
2
|
+
export declare const SECURITY_LIFECYCLE_TOKEN = "xtask:security:lifecycle";
|
|
3
|
+
export declare const SECURITY_AUTHENTICATION_SERVICE_TOKEN = "xtask:security:authentication-service";
|
|
4
|
+
export declare const SECURITY_AUTHORIZATION_SERVICE_TOKEN = "xtask:security:authorization-service";
|
|
5
|
+
export declare const getPassportToken: () => string;
|
|
6
|
+
export declare const getSecurityLifecycleToken: () => string;
|
|
7
|
+
export declare const getAuthenticationServiceToken: () => string;
|
|
8
|
+
export declare const getAuthorizationServiceToken: () => string;
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAuthorizationServiceToken = exports.getAuthenticationServiceToken = exports.getSecurityLifecycleToken = exports.getPassportToken = exports.SECURITY_AUTHORIZATION_SERVICE_TOKEN = exports.SECURITY_AUTHENTICATION_SERVICE_TOKEN = exports.SECURITY_LIFECYCLE_TOKEN = exports.SECURITY_PASSPORT_TOKEN = void 0;
|
|
4
|
+
exports.SECURITY_PASSPORT_TOKEN = "xtask:security:passport";
|
|
5
|
+
exports.SECURITY_LIFECYCLE_TOKEN = "xtask:security:lifecycle";
|
|
6
|
+
exports.SECURITY_AUTHENTICATION_SERVICE_TOKEN = "xtask:security:authentication-service";
|
|
7
|
+
exports.SECURITY_AUTHORIZATION_SERVICE_TOKEN = "xtask:security:authorization-service";
|
|
8
|
+
const getPassportToken = () => exports.SECURITY_PASSPORT_TOKEN;
|
|
9
|
+
exports.getPassportToken = getPassportToken;
|
|
10
|
+
const getSecurityLifecycleToken = () => exports.SECURITY_LIFECYCLE_TOKEN;
|
|
11
|
+
exports.getSecurityLifecycleToken = getSecurityLifecycleToken;
|
|
12
|
+
const getAuthenticationServiceToken = () => {
|
|
13
|
+
return exports.SECURITY_AUTHENTICATION_SERVICE_TOKEN;
|
|
14
|
+
};
|
|
15
|
+
exports.getAuthenticationServiceToken = getAuthenticationServiceToken;
|
|
16
|
+
const getAuthorizationServiceToken = () => {
|
|
17
|
+
return exports.SECURITY_AUTHORIZATION_SERVICE_TOKEN;
|
|
18
|
+
};
|
|
19
|
+
exports.getAuthorizationServiceToken = getAuthorizationServiceToken;
|