@nathapp/nestjs-csrf 3.3.0 → 4.0.1
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/package.json +10 -10
- package/.eslintrc.js +0 -8
- package/.nax/features/nestjs-csrf/.nax-acceptance.test.ts +0 -785
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -15
- package/lib/constant/csrf.constants.ts +0 -3
- package/lib/constant/index.ts +0 -1
- package/lib/decorator/index.ts +0 -1
- package/lib/decorator/skip-csrf.decorator.ts +0 -6
- package/lib/guard/csrf.guard.ts +0 -98
- package/lib/guard/index.ts +0 -1
- package/lib/index.ts +0 -12
- package/lib/interface/csrf-options.interface.ts +0 -31
- package/lib/interface/index.ts +0 -7
- package/lib/module/csrf.module.ts +0 -74
- package/lib/module/index.ts +0 -1
- package/lib/service/csrf-token.service.ts +0 -34
- package/lib/service/index.ts +0 -2
- package/lib/service/resolve-options.ts +0 -47
- package/test/csrf-token.service.behavior.spec.ts +0 -110
- package/test/csrf-token.service.spec.ts +0 -18
- package/test/csrf.constants.spec.ts +0 -11
- package/test/csrf.module.spec.ts +0 -267
- package/test/guard/csrf.guard.spec.ts +0 -347
- package/test/jest-nax.json +0 -27
- package/test/jest-test.json +0 -28
- package/test/resolve-csrf-options.spec.ts +0 -128
- package/test/skip-csrf.decorator.spec.ts +0 -77
- package/tsconfig.json +0 -9
package/.turbo/turbo-build.log
DELETED
package/CHANGELOG.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# @nathapp/nestjs-csrf
|
|
2
|
-
|
|
3
|
-
## 3.3.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- a27440f: 3.3.0 milestone release. The `@nathapp/nestjs-*` packages now version and publish in lockstep at a unified version.
|
|
8
|
-
|
|
9
|
-
- **nestjs-csrf**: first public release — stateless double-submit-cookie CSRF guard (zero-dep HMAC, Express + Fastify, Bearer coexist skip, `@SkipCsrf()`, auto-binds `APP_GUARD`).
|
|
10
|
-
- **nestjs-health**: first public release — terminus aggregator over cache/prisma/queue with k8s liveness/readiness probes via a dynamic controller factory.
|
|
11
|
-
- **nestjs-idempotency**: first public release — `@Idempotent()` interceptor (zero-dep sha256 fingerprint, Express + Fastify, 409-in-flight, 2xx replay, fingerprint mismatch 422, fail-closed on store error).
|
|
12
|
-
- **nestjs-observability**: first public release — Prometheus RED metrics + OpenTelemetry tracing joined to the logging ALS; `initTracing()` pre-bootstrap hook.
|
|
13
|
-
- **nestjs-outbox**: first public release — transactional outbox relay (at-least-once via `ITransactionManager`, lease-claim + backoff, dead-letter).
|
|
14
|
-
- **nestjs-storage**: migrated from aws-sdk v2 to v3 (zero runtime dep via `import type` + lazy `loadPackage`); NFSStorage stub replaced with an S3-style HTTP file-service client.
|
|
15
|
-
- **nestjs-cache**: `delByPattern` now routes through Redis SCAN instead of an O(n) key sweep, with glob-detection and multi-tier iterator support.
|
package/lib/constant/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { CSRF_MODULE_OPTIONS, SKIP_CSRF_METADATA_KEY } from './csrf.constants';
|
package/lib/decorator/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { SkipCsrf } from './skip-csrf.decorator';
|
package/lib/guard/csrf.guard.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CanActivate,
|
|
3
|
-
ExecutionContext,
|
|
4
|
-
Inject,
|
|
5
|
-
Injectable,
|
|
6
|
-
Logger,
|
|
7
|
-
} from '@nestjs/common';
|
|
8
|
-
import { Reflector } from '@nestjs/core';
|
|
9
|
-
import { ForbiddenAppException } from '@nathapp/nestjs-common';
|
|
10
|
-
import { CSRF_MODULE_OPTIONS, SKIP_CSRF_METADATA_KEY } from '../constant/csrf.constants';
|
|
11
|
-
import { CsrfOptions, ResolvedCsrfOptions } from '../interface/csrf-options.interface';
|
|
12
|
-
import { CsrfTokenService } from '../service/csrf-token.service';
|
|
13
|
-
import { resolveCsrfOptions } from '../service/resolve-options';
|
|
14
|
-
|
|
15
|
-
@Injectable()
|
|
16
|
-
export class DefaultCsrfGuard implements CanActivate {
|
|
17
|
-
private readonly logger = new Logger(DefaultCsrfGuard.name);
|
|
18
|
-
private readonly options: ResolvedCsrfOptions;
|
|
19
|
-
|
|
20
|
-
constructor(
|
|
21
|
-
@Inject(CSRF_MODULE_OPTIONS) rawOptions: CsrfOptions,
|
|
22
|
-
private readonly tokens: CsrfTokenService,
|
|
23
|
-
private readonly reflector: Reflector,
|
|
24
|
-
) {
|
|
25
|
-
this.options = resolveCsrfOptions(rawOptions);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
canActivate(context: ExecutionContext): boolean {
|
|
29
|
-
if (this.options.enabled === false) return true;
|
|
30
|
-
|
|
31
|
-
const http = context.switchToHttp();
|
|
32
|
-
const req = http.getRequest<{
|
|
33
|
-
method?: string;
|
|
34
|
-
cookies?: Record<string, string>;
|
|
35
|
-
headers?: Record<string, string | string[] | undefined>;
|
|
36
|
-
raw?: unknown;
|
|
37
|
-
}>();
|
|
38
|
-
const res = http.getResponse<{
|
|
39
|
-
cookie?: (name: string, value: string, opts?: unknown) => void;
|
|
40
|
-
setCookie?: (name: string, value: string, opts?: unknown) => void;
|
|
41
|
-
}>();
|
|
42
|
-
|
|
43
|
-
if (this.options.ignoreMethods.includes((req.method ?? '').toUpperCase())) {
|
|
44
|
-
const existingCookie = req.cookies?.[this.options.cookie.name];
|
|
45
|
-
if (!existingCookie) {
|
|
46
|
-
this.issue(res);
|
|
47
|
-
}
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const reflectionTargets: Function[] = [context.getHandler(), context.getClass()];
|
|
52
|
-
const validTargets = reflectionTargets.filter(
|
|
53
|
-
(t): t is Function => t != null,
|
|
54
|
-
);
|
|
55
|
-
if (
|
|
56
|
-
validTargets.length > 0 &&
|
|
57
|
-
this.reflector.getAllAndOverride(SKIP_CSRF_METADATA_KEY, validTargets)
|
|
58
|
-
) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
this.options.skipWhenBearer &&
|
|
64
|
-
typeof req.headers?.['authorization'] === 'string' &&
|
|
65
|
-
/^Bearer\s/i.test(req.headers['authorization'] as string)
|
|
66
|
-
) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const cookie = req.cookies?.[this.options.cookie.name];
|
|
71
|
-
const headerValue = req.headers?.[this.options.headerName];
|
|
72
|
-
const header = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
73
|
-
|
|
74
|
-
if (!this.tokens.verify(this.options.secret, cookie, header)) {
|
|
75
|
-
throw new ForbiddenAppException({ reason: 'csrf_token_invalid' });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private issue(res: {
|
|
82
|
-
cookie?: (name: string, value: string, opts?: unknown) => void;
|
|
83
|
-
setCookie?: (name: string, value: string, opts?: unknown) => void;
|
|
84
|
-
}): void {
|
|
85
|
-
const token = this.tokens.mint(this.options.secret);
|
|
86
|
-
const { name, ...opts } = this.options.cookie;
|
|
87
|
-
if (typeof res.setCookie === 'function') {
|
|
88
|
-
res.setCookie(name, token, opts);
|
|
89
|
-
} else if (typeof res.cookie === 'function') {
|
|
90
|
-
res.cookie(name, token, opts);
|
|
91
|
-
} else {
|
|
92
|
-
this.logger.warn(
|
|
93
|
-
'Unable to issue CSRF cookie: response object exposes neither setCookie nor cookie. ' +
|
|
94
|
-
'Ensure cookie-parser (Express) or @fastify/cookie (Fastify) is registered.',
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
package/lib/guard/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { DefaultCsrfGuard } from './csrf.guard';
|
package/lib/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export { CSRF_MODULE_OPTIONS, SKIP_CSRF_METADATA_KEY } from './constant';
|
|
2
|
-
export { CsrfAsyncModuleOptions, CsrfModule } from './module';
|
|
3
|
-
export { CsrfTokenService, resolveCsrfOptions } from './service';
|
|
4
|
-
export { DefaultCsrfGuard } from './guard';
|
|
5
|
-
export { SkipCsrf } from './decorator';
|
|
6
|
-
export {
|
|
7
|
-
CsrfCookieOptions,
|
|
8
|
-
CsrfModuleOptionsFactory,
|
|
9
|
-
CsrfOptions,
|
|
10
|
-
CsrfSameSite,
|
|
11
|
-
ResolvedCsrfOptions,
|
|
12
|
-
} from './interface';
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export type CsrfSameSite = 'lax' | 'strict' | 'none';
|
|
2
|
-
|
|
3
|
-
export interface CsrfCookieOptions {
|
|
4
|
-
name: string;
|
|
5
|
-
path: string;
|
|
6
|
-
httpOnly: boolean;
|
|
7
|
-
secure: boolean;
|
|
8
|
-
sameSite: CsrfSameSite;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface CsrfOptions {
|
|
12
|
-
secret: string;
|
|
13
|
-
enabled?: boolean;
|
|
14
|
-
headerName?: string;
|
|
15
|
-
ignoreMethods?: string[];
|
|
16
|
-
skipWhenBearer?: boolean;
|
|
17
|
-
cookie?: Partial<CsrfCookieOptions>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ResolvedCsrfOptions {
|
|
21
|
-
secret: string;
|
|
22
|
-
enabled: boolean;
|
|
23
|
-
headerName: string;
|
|
24
|
-
ignoreMethods: string[];
|
|
25
|
-
skipWhenBearer: boolean;
|
|
26
|
-
cookie: CsrfCookieOptions;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface CsrfModuleOptionsFactory {
|
|
30
|
-
createOptions(): Promise<CsrfOptions> | CsrfOptions;
|
|
31
|
-
}
|
package/lib/interface/index.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DynamicModule,
|
|
3
|
-
Module,
|
|
4
|
-
ModuleMetadata,
|
|
5
|
-
Provider,
|
|
6
|
-
Type,
|
|
7
|
-
} from '@nestjs/common';
|
|
8
|
-
import { APP_GUARD, Reflector } from '@nestjs/core';
|
|
9
|
-
import { ConfigurableModuleClass } from '@nathapp/nestjs-common';
|
|
10
|
-
import { CSRF_MODULE_OPTIONS } from '../constant/csrf.constants';
|
|
11
|
-
import { CsrfModuleOptionsFactory, CsrfOptions } from '../interface/csrf-options.interface';
|
|
12
|
-
import { CsrfTokenService } from '../service/csrf-token.service';
|
|
13
|
-
import { resolveCsrfOptions } from '../service/resolve-options';
|
|
14
|
-
import { DefaultCsrfGuard } from '../guard/csrf.guard';
|
|
15
|
-
|
|
16
|
-
export type CsrfAsyncModuleOptions = Pick<ModuleMetadata, 'imports'> & {
|
|
17
|
-
useExisting?: Type<CsrfModuleOptionsFactory>;
|
|
18
|
-
useClass?: Type<CsrfModuleOptionsFactory>;
|
|
19
|
-
useFactory?: (...args: any[]) => Promise<CsrfOptions> | CsrfOptions;
|
|
20
|
-
inject?: any[];
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
@Module({})
|
|
24
|
-
export class CsrfModule extends ConfigurableModuleClass<CsrfOptions>(
|
|
25
|
-
CSRF_MODULE_OPTIONS,
|
|
26
|
-
) {
|
|
27
|
-
static register(options: CsrfOptions): DynamicModule {
|
|
28
|
-
resolveCsrfOptions(options);
|
|
29
|
-
|
|
30
|
-
const base = super.register(options);
|
|
31
|
-
return CsrfModule.withCsrfProviders(base);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
static registerAsync(options: CsrfAsyncModuleOptions): DynamicModule {
|
|
35
|
-
const wrappedAsyncOptions: CsrfAsyncModuleOptions =
|
|
36
|
-
typeof options.useFactory === 'function'
|
|
37
|
-
? {
|
|
38
|
-
...options,
|
|
39
|
-
useFactory: async (...args: any[]) => {
|
|
40
|
-
const resolved = await options.useFactory!(...args);
|
|
41
|
-
resolveCsrfOptions(resolved);
|
|
42
|
-
return resolved;
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
: options;
|
|
46
|
-
|
|
47
|
-
const base = super.registerAsync(wrappedAsyncOptions);
|
|
48
|
-
return CsrfModule.withCsrfProviders(base, {
|
|
49
|
-
imports: options.imports ?? (base.imports ?? []),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private static withCsrfProviders(
|
|
54
|
-
base: DynamicModule,
|
|
55
|
-
overrides: Partial<DynamicModule> = {},
|
|
56
|
-
): DynamicModule {
|
|
57
|
-
const baseProviders = (base.providers ?? []) as Provider[];
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
...base,
|
|
61
|
-
module: CsrfModule,
|
|
62
|
-
global: true,
|
|
63
|
-
...overrides,
|
|
64
|
-
providers: [
|
|
65
|
-
...baseProviders,
|
|
66
|
-
CsrfTokenService,
|
|
67
|
-
Reflector,
|
|
68
|
-
DefaultCsrfGuard,
|
|
69
|
-
{ provide: APP_GUARD, useExisting: DefaultCsrfGuard },
|
|
70
|
-
],
|
|
71
|
-
exports: [CsrfTokenService],
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
package/lib/module/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { CsrfAsyncModuleOptions, CsrfModule } from './csrf.module';
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
|
3
|
-
|
|
4
|
-
function timingSafeStrEqual(a: string, b: string): boolean {
|
|
5
|
-
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
6
|
-
const aBuf = Buffer.from(a, 'utf8');
|
|
7
|
-
const bBuf = Buffer.from(b, 'utf8');
|
|
8
|
-
if (aBuf.length !== bBuf.length) return false;
|
|
9
|
-
return timingSafeEqual(aBuf, bBuf);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
@Injectable()
|
|
13
|
-
export class CsrfTokenService {
|
|
14
|
-
mint(secret: string): string {
|
|
15
|
-
const value = randomBytes(32).toString('hex');
|
|
16
|
-
const sig = createHmac('sha256', secret).update(value).digest('hex');
|
|
17
|
-
return `${value}.${sig}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
verify(secret: string, cookieValue?: string, headerValue?: string): boolean {
|
|
21
|
-
if (!cookieValue || !headerValue) return false;
|
|
22
|
-
if (!timingSafeStrEqual(cookieValue, headerValue)) return false;
|
|
23
|
-
|
|
24
|
-
const dotIndex = cookieValue.indexOf('.');
|
|
25
|
-
if (dotIndex < 0) return false;
|
|
26
|
-
|
|
27
|
-
const value = cookieValue.slice(0, dotIndex);
|
|
28
|
-
const sig = cookieValue.slice(dotIndex + 1);
|
|
29
|
-
if (!value || !sig) return false;
|
|
30
|
-
|
|
31
|
-
const expected = createHmac('sha256', secret).update(value).digest('hex');
|
|
32
|
-
return timingSafeStrEqual(sig, expected);
|
|
33
|
-
}
|
|
34
|
-
}
|
package/lib/service/index.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { CsrfCookieOptions, CsrfOptions, ResolvedCsrfOptions } from '../interface/csrf-options.interface';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_COOKIE_NAME = 'csrf-token';
|
|
4
|
-
const DEFAULT_HEADER_NAME = 'x-csrf-token';
|
|
5
|
-
const DEFAULT_IGNORE_METHODS = ['GET', 'HEAD', 'OPTIONS'];
|
|
6
|
-
const DEFAULT_COOKIE_PATH = '/';
|
|
7
|
-
const DEFAULT_COOKIE_HTTP_ONLY = false;
|
|
8
|
-
const DEFAULT_COOKIE_SECURE = true;
|
|
9
|
-
const DEFAULT_COOKIE_SAME_SITE: CsrfCookieOptions['sameSite'] = 'lax';
|
|
10
|
-
|
|
11
|
-
export function resolveCsrfOptions(options: CsrfOptions): ResolvedCsrfOptions {
|
|
12
|
-
const {
|
|
13
|
-
secret,
|
|
14
|
-
enabled = true,
|
|
15
|
-
headerName: headerNameOverride = DEFAULT_HEADER_NAME,
|
|
16
|
-
ignoreMethods: ignoreMethodsOverride,
|
|
17
|
-
skipWhenBearer = true,
|
|
18
|
-
cookie: cookieOverrides,
|
|
19
|
-
} = options;
|
|
20
|
-
|
|
21
|
-
if (typeof secret !== 'string' || secret.length === 0) {
|
|
22
|
-
throw new Error('CsrfOptions.secret must be a non-empty string');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const headerName = headerNameOverride.toLowerCase();
|
|
26
|
-
|
|
27
|
-
const ignoreMethods = ignoreMethodsOverride
|
|
28
|
-
? [...ignoreMethodsOverride]
|
|
29
|
-
: [...DEFAULT_IGNORE_METHODS];
|
|
30
|
-
|
|
31
|
-
const cookie: CsrfCookieOptions = {
|
|
32
|
-
name: cookieOverrides?.name ?? DEFAULT_COOKIE_NAME,
|
|
33
|
-
path: cookieOverrides?.path ?? DEFAULT_COOKIE_PATH,
|
|
34
|
-
httpOnly: cookieOverrides?.httpOnly ?? DEFAULT_COOKIE_HTTP_ONLY,
|
|
35
|
-
secure: cookieOverrides?.secure ?? DEFAULT_COOKIE_SECURE,
|
|
36
|
-
sameSite: cookieOverrides?.sameSite ?? DEFAULT_COOKIE_SAME_SITE,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
secret,
|
|
41
|
-
enabled,
|
|
42
|
-
headerName,
|
|
43
|
-
ignoreMethods,
|
|
44
|
-
skipWhenBearer,
|
|
45
|
-
cookie,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
-
describe('CsrfTokenService.mint / verify', () => {
|
|
3
|
-
const loadService = () => {
|
|
4
|
-
const mod = require('../lib');
|
|
5
|
-
expect(typeof mod.CsrfTokenService).toBe('function');
|
|
6
|
-
return new mod.CsrfTokenService();
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
it('AC2: verify(secret, token, token) returns true when token was minted with the same secret', () => {
|
|
10
|
-
const svc = loadService();
|
|
11
|
-
const token = svc.mint('super-secret');
|
|
12
|
-
expect(svc.verify('super-secret', token, token)).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('AC2 (boundary): verifies across multiple different secrets', () => {
|
|
16
|
-
const svc = loadService();
|
|
17
|
-
const secrets = ['', 'a', 'long-secret-with-various-chars-!@#$%^&*()'];
|
|
18
|
-
for (const secret of secrets) {
|
|
19
|
-
const token = svc.mint(secret);
|
|
20
|
-
expect(svc.verify(secret, token, token)).toBe(true);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('AC3: verify(secret, token, otherToken) returns false when otherToken !== token', () => {
|
|
25
|
-
const svc = loadService();
|
|
26
|
-
const token = svc.mint('super-secret');
|
|
27
|
-
const otherToken = svc.mint('super-secret');
|
|
28
|
-
expect(token).not.toBe(otherToken);
|
|
29
|
-
expect(svc.verify('super-secret', token, otherToken)).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('AC3 (boundary): any byte difference between cookie and header makes verify false', () => {
|
|
33
|
-
const svc = loadService();
|
|
34
|
-
const token = svc.mint('super-secret');
|
|
35
|
-
expect(svc.verify('super-secret', token, token + 'x')).toBe(false);
|
|
36
|
-
expect(svc.verify('super-secret', 'x' + token, token)).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('AC4: tampered signature makes verify return false', () => {
|
|
40
|
-
const svc = loadService();
|
|
41
|
-
const token = svc.mint('super-secret');
|
|
42
|
-
const [value, sig] = token.split('.');
|
|
43
|
-
const tamperedSig = sig.startsWith('a') ? `b${sig.slice(1)}` : `a${sig.slice(1)}`;
|
|
44
|
-
const tampered = `${value}.${tamperedSig}`;
|
|
45
|
-
expect(tampered).not.toBe(token);
|
|
46
|
-
expect(svc.verify('super-secret', tampered, tampered)).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('AC4 (boundary): tampered value with bogus signature is rejected', () => {
|
|
50
|
-
const svc = loadService();
|
|
51
|
-
const token = svc.mint('super-secret');
|
|
52
|
-
const [value] = token.split('.');
|
|
53
|
-
const tampered = `${value}.${'0'.repeat(64)}`;
|
|
54
|
-
expect(svc.verify('super-secret', tampered, tampered)).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('AC5: verify with a different secret returns false', () => {
|
|
58
|
-
const svc = loadService();
|
|
59
|
-
const token = svc.mint('secret-A');
|
|
60
|
-
expect(svc.verify('secret-B', token, token)).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('AC5 (boundary): verify with an empty string secret against a real token returns false', () => {
|
|
64
|
-
const svc = loadService();
|
|
65
|
-
const token = svc.mint('secret-A');
|
|
66
|
-
expect(svc.verify('', token, token)).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('AC6: verify(secret, "", "") returns false without throwing', () => {
|
|
70
|
-
const svc = loadService();
|
|
71
|
-
let result: unknown;
|
|
72
|
-
expect(() => {
|
|
73
|
-
result = svc.verify('super-secret', '', '');
|
|
74
|
-
}).not.toThrow();
|
|
75
|
-
expect(result).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('AC6 (boundary): undefined cookie / header values return false without throwing', () => {
|
|
79
|
-
const svc = loadService();
|
|
80
|
-
let result: unknown;
|
|
81
|
-
expect(() => {
|
|
82
|
-
result = svc.verify('super-secret', undefined, undefined);
|
|
83
|
-
}).not.toThrow();
|
|
84
|
-
expect(result).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('AC7: verify(secret, "no-separator", "no-separator") returns false without throwing', () => {
|
|
88
|
-
const svc = loadService();
|
|
89
|
-
let result: unknown;
|
|
90
|
-
expect(() => {
|
|
91
|
-
result = svc.verify('super-secret', 'no-separator', 'no-separator');
|
|
92
|
-
}).not.toThrow();
|
|
93
|
-
expect(result).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('AC7 (boundary): token with empty value or empty signature returns false without throwing', () => {
|
|
97
|
-
const svc = loadService();
|
|
98
|
-
let result1: unknown;
|
|
99
|
-
expect(() => {
|
|
100
|
-
result1 = svc.verify('super-secret', '.sig', '.sig');
|
|
101
|
-
}).not.toThrow();
|
|
102
|
-
expect(result1).toBe(false);
|
|
103
|
-
|
|
104
|
-
let result2: unknown;
|
|
105
|
-
expect(() => {
|
|
106
|
-
result2 = svc.verify('super-secret', 'value.', 'value.');
|
|
107
|
-
}).not.toThrow();
|
|
108
|
-
expect(result2).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
-
describe('AC1: CsrfTokenService provider', () => {
|
|
3
|
-
it('AC1: resolves from a compiled Nest testing module', async () => {
|
|
4
|
-
const { Test } = require('@nestjs/testing');
|
|
5
|
-
const { CsrfTokenService } = require('../lib');
|
|
6
|
-
|
|
7
|
-
expect(typeof CsrfTokenService).toBe('function');
|
|
8
|
-
|
|
9
|
-
const moduleRef = await Test.createTestingModule({
|
|
10
|
-
providers: [CsrfTokenService],
|
|
11
|
-
}).compile();
|
|
12
|
-
|
|
13
|
-
const service = moduleRef.get(CsrfTokenService);
|
|
14
|
-
expect(service).toBeDefined();
|
|
15
|
-
expect(typeof service.mint).toBe('function');
|
|
16
|
-
expect(typeof service.verify).toBe('function');
|
|
17
|
-
});
|
|
18
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
-
describe('CSRF constants', () => {
|
|
3
|
-
it('AC14: CSRF_MODULE_OPTIONS and SKIP_CSRF_METADATA_KEY are distinct values', () => {
|
|
4
|
-
const mod = require('../lib');
|
|
5
|
-
const { CSRF_MODULE_OPTIONS, SKIP_CSRF_METADATA_KEY } = mod;
|
|
6
|
-
|
|
7
|
-
expect(CSRF_MODULE_OPTIONS).toBeDefined();
|
|
8
|
-
expect(SKIP_CSRF_METADATA_KEY).toBeDefined();
|
|
9
|
-
expect(CSRF_MODULE_OPTIONS).not.toBe(SKIP_CSRF_METADATA_KEY);
|
|
10
|
-
});
|
|
11
|
-
});
|