@xtaskjs/security 1.0.0 → 1.0.2

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.
@@ -0,0 +1,76 @@
1
+ import type { Container } from "@xtaskjs/core";
2
+ export type SecurityStrategyKind = "jwt" | "jwe";
3
+ export type SecurityRoleMatchingMode = "any" | "all";
4
+ export type SecurityTokenExtractor = (request: any) => string | null | undefined;
5
+ export type SecurityRoleExtractor = (payload: Record<string, any>, user: any) => string[] | undefined;
6
+ export interface SecurityValidationContext {
7
+ request: any;
8
+ response?: any;
9
+ token: string;
10
+ strategyName: string;
11
+ kind: SecurityStrategyKind;
12
+ container?: Container;
13
+ }
14
+ export type SecurityValidateFn = (payload: Record<string, any>, context: SecurityValidationContext) => any | false | Promise<any | false>;
15
+ interface BaseSecurityStrategyOptions {
16
+ name?: string;
17
+ default?: boolean;
18
+ jwtFromRequest?: SecurityTokenExtractor;
19
+ extractRoles?: SecurityRoleExtractor;
20
+ validate?: SecurityValidateFn;
21
+ }
22
+ export interface JwtSecurityStrategyOptions extends BaseSecurityStrategyOptions {
23
+ kind?: "jwt";
24
+ secretOrKey?: any;
25
+ secretOrKeyProvider?: any;
26
+ issuer?: string | string[];
27
+ audience?: string | string[];
28
+ algorithms?: string[];
29
+ ignoreExpiration?: boolean;
30
+ jsonWebTokenOptions?: Record<string, any>;
31
+ passReqToCallback?: boolean;
32
+ }
33
+ export interface JweSecurityStrategyOptions extends BaseSecurityStrategyOptions {
34
+ kind?: "jwe";
35
+ decryptionKey?: any;
36
+ resolveDecryptionKey?: () => any | Promise<any>;
37
+ issuer?: string | string[];
38
+ audience?: string | string[];
39
+ clockTolerance?: number | string;
40
+ }
41
+ export type SecurityStrategyOptions = JwtSecurityStrategyOptions | JweSecurityStrategyOptions;
42
+ export interface RegisteredJwtSecurityStrategyOptions extends JwtSecurityStrategyOptions {
43
+ kind: "jwt";
44
+ name: string;
45
+ }
46
+ export interface RegisteredJweSecurityStrategyOptions extends JweSecurityStrategyOptions {
47
+ kind: "jwe";
48
+ name: string;
49
+ }
50
+ export type RegisteredSecurityStrategyOptions = RegisteredJwtSecurityStrategyOptions | RegisteredJweSecurityStrategyOptions;
51
+ export interface ResolvedSecurityMetadata {
52
+ allowAnonymous: boolean;
53
+ strategies: string[];
54
+ roles: string[];
55
+ roleMode: SecurityRoleMatchingMode;
56
+ }
57
+ export interface AuthenticatedOptions {
58
+ strategies?: string | string[];
59
+ }
60
+ export interface RolesOptions {
61
+ roles: string[];
62
+ mode?: SecurityRoleMatchingMode;
63
+ strategies?: string | string[];
64
+ }
65
+ export interface SecurityAuthenticationResult {
66
+ success: boolean;
67
+ statusCode?: number;
68
+ challenge?: any;
69
+ strategy?: string;
70
+ token?: string;
71
+ user?: any;
72
+ claims?: Record<string, any>;
73
+ roles: string[];
74
+ info?: any;
75
+ }
76
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtaskjs/security",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "xtaskjs security integration package",
5
5
  "author": "Javier Rodriguez Soler",
6
6
  "homepage": "https://xtaskjs.com",
@@ -13,8 +13,14 @@
13
13
  "default": "./dist/index.js"
14
14
  }
15
15
  },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "CHANGELOG.md"
20
+ ],
16
21
  "scripts": {
17
- "build": "rm -rf dist && tsc",
22
+ "build": "npm run build --prefix ../core && rm -rf dist && tsc",
23
+ "prepack": "npm run build",
18
24
  "test": "jest --passWithNoTests",
19
25
  "test:coverage": "jest --coverage --runInBand",
20
26
  "coverage:open": "xdg-open coverage/lcov-report/index.html"
@@ -33,8 +39,8 @@
33
39
  },
34
40
  "license": "MIT",
35
41
  "dependencies": {
36
- "@xtaskjs/common": "^1.0.13",
37
- "@xtaskjs/core": "^1.0.13",
42
+ "@xtaskjs/common": "^1.0.16",
43
+ "@xtaskjs/core": "^1.0.16",
38
44
  "passport": "^0.7.0",
39
45
  "passport-jwt": "^4.0.1",
40
46
  "passport-strategy": "^1.0.0"
@@ -52,4 +58,4 @@
52
58
  "tsconfig-paths": "^4.2.0",
53
59
  "typescript": "^5.9.2"
54
60
  }
55
- }
61
+ }
package/babel.config.js DELETED
@@ -1,3 +0,0 @@
1
- module.exports = {
2
- presets: ["next/babel"],
3
- };
package/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./src";
package/jest.config.js DELETED
@@ -1,18 +0,0 @@
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
- };
@@ -1,27 +0,0 @@
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
- }
@@ -1,66 +0,0 @@
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
- }
@@ -1,90 +0,0 @@
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
- };
package/src/decorators.ts DELETED
@@ -1,79 +0,0 @@
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 DELETED
@@ -1,58 +0,0 @@
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
- };