@xtaskjs/security 1.0.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/README.md +115 -0
- package/babel.config.js +3 -0
- package/index.ts +1 -0
- package/jest.config.js +18 -0
- package/package.json +55 -0
- package/src/authentication.ts +27 -0
- package/src/authorization.ts +66 -0
- package/src/configuration.ts +90 -0
- package/src/decorators.ts +79 -0
- package/src/guards.ts +58 -0
- package/src/index.ts +10 -0
- package/src/lifecycle.ts +403 -0
- package/src/metadata.ts +136 -0
- package/src/passport-ambient.d.ts +22 -0
- package/src/strategies.ts +227 -0
- package/src/tokens.ts +16 -0
- package/src/types.ts +94 -0
- package/test/security.integration.test.ts +325 -0
- package/tsconfig.json +21 -0
- package/tsconfig.test.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @xtaskjs/security
|
|
2
|
+
|
|
3
|
+
Security integration package for xtaskjs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
```bash
|
|
7
|
+
npm install @xtaskjs/security passport passport-jwt reflect-metadata
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## What It Provides
|
|
11
|
+
- Passport-backed JWT and JWE authentication.
|
|
12
|
+
- Authorization decorators for controller routes.
|
|
13
|
+
- DI tokens for auth services, Passport, and the lifecycle manager.
|
|
14
|
+
- Lifecycle initialization integrated with `CreateApplication()` and `app.close()`.
|
|
15
|
+
|
|
16
|
+
## Register Strategies
|
|
17
|
+
```typescript
|
|
18
|
+
import { registerJwtStrategy, registerJweStrategy } from "@xtaskjs/security";
|
|
19
|
+
|
|
20
|
+
registerJwtStrategy({
|
|
21
|
+
name: "default",
|
|
22
|
+
default: true,
|
|
23
|
+
secretOrKey: process.env.JWT_SECRET,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
registerJweStrategy({
|
|
27
|
+
name: "encrypted",
|
|
28
|
+
decryptionKey: process.env.JWE_SECRET,
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Validate Callbacks
|
|
33
|
+
Use `validate` to enforce application-specific claims and load the current user from your DI container.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { registerJwtStrategy } from "@xtaskjs/security";
|
|
37
|
+
import { CreateApplication } from "@xtaskjs/core";
|
|
38
|
+
|
|
39
|
+
class UserDirectory {
|
|
40
|
+
async findActiveUser(id: string) {
|
|
41
|
+
return id === "alice" ? { id: "alice", roles: ["admin"] } : undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
registerJwtStrategy({
|
|
46
|
+
name: "default",
|
|
47
|
+
default: true,
|
|
48
|
+
secretOrKey: process.env.JWT_SECRET,
|
|
49
|
+
validate: async (payload, context) => {
|
|
50
|
+
if (payload.tenant !== "xtaskjs") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const userDirectory = context.container?.get(UserDirectory);
|
|
55
|
+
const user = await userDirectory?.findActiveUser(String(payload.sub || ""));
|
|
56
|
+
if (!user) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
sub: user.id,
|
|
62
|
+
roles: user.roles,
|
|
63
|
+
claims: payload,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await CreateApplication();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Secure Controllers
|
|
72
|
+
```typescript
|
|
73
|
+
import { Controller, Get } from "@xtaskjs/common";
|
|
74
|
+
import { Authenticated, Roles, AllowAnonymous } from "@xtaskjs/security";
|
|
75
|
+
|
|
76
|
+
@Controller("admin")
|
|
77
|
+
@Authenticated()
|
|
78
|
+
class AdminController {
|
|
79
|
+
@Get("/profile")
|
|
80
|
+
@Roles("admin")
|
|
81
|
+
profile(req: any) {
|
|
82
|
+
return {
|
|
83
|
+
user: req.user,
|
|
84
|
+
auth: req.auth,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Get("/health")
|
|
89
|
+
@AllowAnonymous()
|
|
90
|
+
health() {
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## DI Integration
|
|
97
|
+
```typescript
|
|
98
|
+
import { Service } from "@xtaskjs/core";
|
|
99
|
+
import {
|
|
100
|
+
InjectAuthenticationService,
|
|
101
|
+
InjectAuthorizationService,
|
|
102
|
+
SecurityAuthenticationService,
|
|
103
|
+
SecurityAuthorizationService,
|
|
104
|
+
} from "@xtaskjs/security";
|
|
105
|
+
|
|
106
|
+
@Service()
|
|
107
|
+
class SecurityAwareService {
|
|
108
|
+
constructor(
|
|
109
|
+
@InjectAuthenticationService()
|
|
110
|
+
private readonly authentication: SecurityAuthenticationService,
|
|
111
|
+
@InjectAuthorizationService()
|
|
112
|
+
private readonly authorization: SecurityAuthorizationService
|
|
113
|
+
) {}
|
|
114
|
+
}
|
|
115
|
+
```
|
package/babel.config.js
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src";
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @type {import("jest").Config} **/
|
|
2
|
+
module.exports = {
|
|
3
|
+
testEnvironment: "node",
|
|
4
|
+
transform: {
|
|
5
|
+
"^.+\\.tsx?$": [
|
|
6
|
+
"ts-jest",
|
|
7
|
+
{
|
|
8
|
+
tsconfig: "<rootDir>/tsconfig.test.json",
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
moduleNameMapper: {
|
|
13
|
+
"^@xtaskjs/common$": "<rootDir>/../common/src/index.ts",
|
|
14
|
+
"^@xtaskjs/common/(.*)$": "<rootDir>/../common/src/$1",
|
|
15
|
+
"^@xtaskjs/core$": "<rootDir>/../core/src/index.ts",
|
|
16
|
+
"^@xtaskjs/core/(.*)$": "<rootDir>/../core/src/$1"
|
|
17
|
+
},
|
|
18
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtaskjs/security",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "xtaskjs security integration package",
|
|
5
|
+
"author": "Javier Rodriguez Soler",
|
|
6
|
+
"homepage": "https://xtaskjs.com",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "rm -rf dist && tsc",
|
|
18
|
+
"test": "jest --passWithNoTests",
|
|
19
|
+
"test:coverage": "jest --coverage --runInBand",
|
|
20
|
+
"coverage:open": "xdg-open coverage/lcov-report/index.html"
|
|
21
|
+
},
|
|
22
|
+
"funding": {
|
|
23
|
+
"type": "opencollective",
|
|
24
|
+
"url": "https://opencollective.com/xtaskjs"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/xtaskjs/xtaskjs.git",
|
|
29
|
+
"directory": "packages/security"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@xtaskjs/common": "^1.0.13",
|
|
37
|
+
"@xtaskjs/core": "^1.0.13",
|
|
38
|
+
"passport": "^0.7.0",
|
|
39
|
+
"passport-jwt": "^4.0.1",
|
|
40
|
+
"passport-strategy": "^1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@jest/globals": "^30.1.2",
|
|
47
|
+
"@types/jest": "^30.0.0",
|
|
48
|
+
"jest": "^30.1.3",
|
|
49
|
+
"nodemon": "^3.1.10",
|
|
50
|
+
"ts-jest": "^29.4.4",
|
|
51
|
+
"ts-node": "^10.9.2",
|
|
52
|
+
"tsconfig-paths": "^4.2.0",
|
|
53
|
+
"typescript": "^5.9.2"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { RouteExecutionContext } from "@xtaskjs/common";
|
|
2
|
+
import { UnauthorizedError } from "@xtaskjs/core";
|
|
3
|
+
import { getSecurityLifecycleManager } from "./lifecycle";
|
|
4
|
+
import { SecurityAuthenticationResult } from "./types";
|
|
5
|
+
|
|
6
|
+
export class SecurityAuthenticationService {
|
|
7
|
+
async authenticate(
|
|
8
|
+
context: RouteExecutionContext,
|
|
9
|
+
strategyNames?: string | string[]
|
|
10
|
+
): Promise<SecurityAuthenticationResult> {
|
|
11
|
+
const result = await getSecurityLifecycleManager().authenticateContext(context, strategyNames);
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
throw new UnauthorizedError(result.challenge?.message || "Unauthorized", {
|
|
14
|
+
message: result.challenge?.message || "Unauthorized",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async authenticateRequest(
|
|
21
|
+
request: any,
|
|
22
|
+
response?: any,
|
|
23
|
+
strategyNames?: string | string[]
|
|
24
|
+
): Promise<SecurityAuthenticationResult> {
|
|
25
|
+
return getSecurityLifecycleManager().authenticateRequest(request, response, strategyNames);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { RouteAuthenticationContext, RouteExecutionContext } from "@xtaskjs/common";
|
|
2
|
+
import { SecurityRoleMatchingMode } from "./types";
|
|
3
|
+
|
|
4
|
+
const normalizeRoles = (input: unknown): string[] => {
|
|
5
|
+
if (Array.isArray(input)) {
|
|
6
|
+
return input
|
|
7
|
+
.map((value) => String(value || "").trim())
|
|
8
|
+
.filter((value) => value.length > 0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof input === "string") {
|
|
12
|
+
return input
|
|
13
|
+
.split(/[\s,]+/)
|
|
14
|
+
.map((value) => value.trim())
|
|
15
|
+
.filter((value) => value.length > 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return [];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class SecurityAuthorizationService {
|
|
22
|
+
getRoles(source: RouteExecutionContext | RouteAuthenticationContext | any): string[] {
|
|
23
|
+
if (!source) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(source.roles)) {
|
|
28
|
+
return normalizeRoles(source.roles);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (source.auth && Array.isArray(source.auth.roles)) {
|
|
32
|
+
return normalizeRoles(source.auth.roles);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(source.user?.roles) || typeof source.user?.roles === "string") {
|
|
36
|
+
return normalizeRoles(source.user.roles);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(source.claims?.roles) || typeof source.claims?.roles === "string") {
|
|
40
|
+
return normalizeRoles(source.claims.roles);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof source.claims?.scope === "string") {
|
|
44
|
+
return normalizeRoles(source.claims.scope);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isAuthorized(
|
|
51
|
+
source: RouteExecutionContext | RouteAuthenticationContext | any,
|
|
52
|
+
requiredRoles: string[],
|
|
53
|
+
mode: SecurityRoleMatchingMode = "any"
|
|
54
|
+
): boolean {
|
|
55
|
+
if (requiredRoles.length === 0) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const grantedRoles = new Set(this.getRoles(source));
|
|
60
|
+
if (mode === "all") {
|
|
61
|
+
return requiredRoles.every((role) => grantedRoles.has(role));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return requiredRoles.some((role) => grantedRoles.has(role));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JweSecurityStrategyOptions,
|
|
3
|
+
JwtSecurityStrategyOptions,
|
|
4
|
+
RegisteredJweSecurityStrategyOptions,
|
|
5
|
+
RegisteredJwtSecurityStrategyOptions,
|
|
6
|
+
RegisteredSecurityStrategyOptions,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_STRATEGY_NAME = "default";
|
|
10
|
+
const registeredStrategies = new Map<string, RegisteredSecurityStrategyOptions>();
|
|
11
|
+
|
|
12
|
+
const resolveStrategyName = (
|
|
13
|
+
kind: RegisteredSecurityStrategyOptions["kind"],
|
|
14
|
+
requestedName?: string
|
|
15
|
+
): string => {
|
|
16
|
+
if (typeof requestedName === "string" && requestedName.trim().length > 0) {
|
|
17
|
+
return requestedName.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!registeredStrategies.has(DEFAULT_STRATEGY_NAME)) {
|
|
21
|
+
return DEFAULT_STRATEGY_NAME;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return `${kind}-${registeredStrategies.size + 1}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const registerJwtStrategy = (
|
|
28
|
+
options: JwtSecurityStrategyOptions
|
|
29
|
+
): RegisteredJwtSecurityStrategyOptions => {
|
|
30
|
+
const name = resolveStrategyName("jwt", options.name);
|
|
31
|
+
const definition: RegisteredJwtSecurityStrategyOptions = {
|
|
32
|
+
...options,
|
|
33
|
+
kind: "jwt",
|
|
34
|
+
name,
|
|
35
|
+
};
|
|
36
|
+
registeredStrategies.set(name, definition);
|
|
37
|
+
return definition;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const registerJweStrategy = (
|
|
41
|
+
options: JweSecurityStrategyOptions
|
|
42
|
+
): RegisteredJweSecurityStrategyOptions => {
|
|
43
|
+
const name = resolveStrategyName("jwe", options.name);
|
|
44
|
+
const definition: RegisteredJweSecurityStrategyOptions = {
|
|
45
|
+
...options,
|
|
46
|
+
kind: "jwe",
|
|
47
|
+
name,
|
|
48
|
+
};
|
|
49
|
+
registeredStrategies.set(name, definition);
|
|
50
|
+
return definition;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const getRegisteredSecurityStrategies = (): RegisteredSecurityStrategyOptions[] => {
|
|
54
|
+
return Array.from(registeredStrategies.values());
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const clearRegisteredSecurityStrategies = (): void => {
|
|
58
|
+
registeredStrategies.clear();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getDefaultSecurityStrategyName = (): string | undefined => {
|
|
62
|
+
const explicitDefault = Array.from(registeredStrategies.values()).find(
|
|
63
|
+
(definition) => definition.default === true
|
|
64
|
+
);
|
|
65
|
+
if (explicitDefault) {
|
|
66
|
+
return explicitDefault.name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (registeredStrategies.size === 1) {
|
|
70
|
+
return Array.from(registeredStrategies.keys())[0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (registeredStrategies.has(DEFAULT_STRATEGY_NAME)) {
|
|
74
|
+
return DEFAULT_STRATEGY_NAME;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const JwtSecurityStrategy = (options: JwtSecurityStrategyOptions): ClassDecorator => {
|
|
81
|
+
return () => {
|
|
82
|
+
registerJwtStrategy(options);
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const JweSecurityStrategy = (options: JweSecurityStrategyOptions): ClassDecorator => {
|
|
87
|
+
return () => {
|
|
88
|
+
registerJweStrategy(options);
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { UseGuards } from "@xtaskjs/common";
|
|
2
|
+
import { authenticationGuard, authorizationGuard } from "./guards";
|
|
3
|
+
import {
|
|
4
|
+
normalizeStrategies,
|
|
5
|
+
setAllowAnonymousMetadata,
|
|
6
|
+
setAuthenticatedMetadata,
|
|
7
|
+
setRolesMetadata,
|
|
8
|
+
} from "./metadata";
|
|
9
|
+
import { AuthenticatedOptions, RolesOptions } from "./types";
|
|
10
|
+
|
|
11
|
+
const applyDecorator = (
|
|
12
|
+
decorator: MethodDecorator & ClassDecorator,
|
|
13
|
+
target: any,
|
|
14
|
+
propertyKey?: string | symbol,
|
|
15
|
+
descriptor?: PropertyDescriptor
|
|
16
|
+
): void => {
|
|
17
|
+
if (propertyKey === undefined) {
|
|
18
|
+
decorator(target);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
decorator(target, propertyKey, descriptor!);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const normalizeAuthenticatedOptions = (
|
|
26
|
+
value?: string | string[] | AuthenticatedOptions
|
|
27
|
+
): AuthenticatedOptions => {
|
|
28
|
+
if (!value) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
33
|
+
return { strategies: value };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Authenticated = (
|
|
40
|
+
value?: string | string[] | AuthenticatedOptions
|
|
41
|
+
): MethodDecorator & ClassDecorator => {
|
|
42
|
+
const options = normalizeAuthenticatedOptions(value);
|
|
43
|
+
const strategies = normalizeStrategies(options.strategies);
|
|
44
|
+
const guardDecorator = UseGuards(authenticationGuard);
|
|
45
|
+
|
|
46
|
+
return (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
|
|
47
|
+
setAuthenticatedMetadata(target, propertyKey, strategies);
|
|
48
|
+
applyDecorator(guardDecorator, target, propertyKey, descriptor);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Auth = Authenticated;
|
|
53
|
+
|
|
54
|
+
const normalizeRolesOptions = (value: RolesOptions | string, roles: string[]): RolesOptions => {
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
return { roles: [value, ...roles] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const Roles = (
|
|
63
|
+
value: RolesOptions | string,
|
|
64
|
+
...roles: string[]
|
|
65
|
+
): MethodDecorator & ClassDecorator => {
|
|
66
|
+
const options = normalizeRolesOptions(value, roles);
|
|
67
|
+
const guardDecorator = UseGuards(authorizationGuard);
|
|
68
|
+
|
|
69
|
+
return (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
|
|
70
|
+
setRolesMetadata(target, propertyKey, options);
|
|
71
|
+
applyDecorator(guardDecorator, target, propertyKey, descriptor);
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const AllowAnonymous = (): MethodDecorator & ClassDecorator => {
|
|
76
|
+
return (target: any, propertyKey?: string | symbol) => {
|
|
77
|
+
setAllowAnonymousMetadata(target, propertyKey);
|
|
78
|
+
};
|
|
79
|
+
};
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { GuardLike, RouteExecutionContext } from "@xtaskjs/common";
|
|
2
|
+
import { ForbiddenError } from "@xtaskjs/core";
|
|
3
|
+
import { SecurityAuthenticationService } from "./authentication";
|
|
4
|
+
import { SecurityAuthorizationService } from "./authorization";
|
|
5
|
+
import { resolveSecurityMetadata } from "./metadata";
|
|
6
|
+
|
|
7
|
+
const AUTHENTICATION_GUARD_STATE_KEY = "xtask:security:authentication-checked";
|
|
8
|
+
const AUTHORIZATION_GUARD_STATE_KEY = "xtask:security:authorization-checked";
|
|
9
|
+
|
|
10
|
+
const authenticationService = new SecurityAuthenticationService();
|
|
11
|
+
const authorizationService = new SecurityAuthorizationService();
|
|
12
|
+
|
|
13
|
+
export const authenticationGuard: GuardLike = {
|
|
14
|
+
async canActivate(context: RouteExecutionContext): Promise<boolean> {
|
|
15
|
+
if (context.state[AUTHENTICATION_GUARD_STATE_KEY]) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const metadata = resolveSecurityMetadata(context.controller?.constructor, context.handler);
|
|
20
|
+
if (metadata.allowAnonymous) {
|
|
21
|
+
context.state[AUTHENTICATION_GUARD_STATE_KEY] = true;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await authenticationService.authenticate(context, metadata.strategies);
|
|
26
|
+
context.state[AUTHENTICATION_GUARD_STATE_KEY] = true;
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const authorizationGuard: GuardLike = {
|
|
32
|
+
async canActivate(context: RouteExecutionContext): Promise<boolean> {
|
|
33
|
+
if (context.state[AUTHORIZATION_GUARD_STATE_KEY]) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const metadata = resolveSecurityMetadata(context.controller?.constructor, context.handler);
|
|
38
|
+
if (metadata.allowAnonymous || metadata.roles.length === 0) {
|
|
39
|
+
context.state[AUTHORIZATION_GUARD_STATE_KEY] = true;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!context.auth.isAuthenticated) {
|
|
44
|
+
await authenticationGuard.canActivate(context);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!authorizationService.isAuthorized(context.auth, metadata.roles, metadata.roleMode)) {
|
|
48
|
+
throw new ForbiddenError("Forbidden", {
|
|
49
|
+
message: "Forbidden",
|
|
50
|
+
requiredRoles: metadata.roles,
|
|
51
|
+
actualRoles: context.auth.roles,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
context.state[AUTHORIZATION_GUARD_STATE_KEY] = true;
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
|
|
3
|
+
export * from "./types";
|
|
4
|
+
export * from "./tokens";
|
|
5
|
+
export * from "./configuration";
|
|
6
|
+
export * from "./authentication";
|
|
7
|
+
export * from "./authorization";
|
|
8
|
+
export * from "./decorators";
|
|
9
|
+
export * from "./guards";
|
|
10
|
+
export * from "./lifecycle";
|