mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(auth): sliding session renewal past half the token lifetime (#1927)
This commit is contained in:
@@ -64,6 +64,7 @@ import { trekMcpAccessPolicy, trekMcpValidateAccess } from '../mcp/nest-mcp-poli
|
||||
import { TrekExceptionFilter } from './common/trek-exception.filter';
|
||||
import { SpaFallbackFilter } from './platform/spa-fallback.filter';
|
||||
import { IdempotencyInterceptor } from './common/idempotency.interceptor';
|
||||
import { SessionRenewalInterceptor } from './auth/session-renewal.interceptor';
|
||||
import { IdempotencyCleanupJob } from './common/idempotency-cleanup.job';
|
||||
import { RealtimeGatewayModule } from './realtime/realtime-gateway.module';
|
||||
|
||||
@@ -97,6 +98,9 @@ import { RealtimeGatewayModule } from './realtime/realtime-gateway.module';
|
||||
// Replays the X-Idempotency-Key the client sends on every write, so retried
|
||||
// mutations don't double-apply.
|
||||
{ provide: APP_INTERCEPTOR, useClass: IdempotencyInterceptor },
|
||||
// Sliding session renewal: re-issues the trek_session cookie once a
|
||||
// cookie-authenticated token is past half its lifetime (#1927).
|
||||
{ provide: APP_INTERCEPTOR, useClass: SessionRenewalInterceptor },
|
||||
// Its nightly TTL purge — a provider here because common/ has no module.
|
||||
IdempotencyCleanupJob,
|
||||
// Global Zod validation: any parameter typed with a createZodDto class
|
||||
|
||||
@@ -197,15 +197,19 @@ export class AuthService {
|
||||
return !this.resolveAuthToggles().password_login;
|
||||
}
|
||||
|
||||
generateToken(user: { id: number | bigint; password_version?: number }, rememberMe = false) {
|
||||
generateToken(user: { id: number | bigint; password_version?: number }, remember?: boolean) {
|
||||
const pv = typeof user.password_version === 'number'
|
||||
? user.password_version
|
||||
: (this.db.get<{ password_version?: number }>('SELECT password_version FROM users WHERE id = ?', user.id)?.password_version ?? 0);
|
||||
// "Remember me" extends the JWT lifetime to match the persistent cookie maxAge;
|
||||
// the cookie service decides session-vs-persistent off the same flag.
|
||||
const expiresIn = rememberMe ? SESSION_DURATION_REMEMBER_SECONDS : SESSION_DURATION_SECONDS;
|
||||
const expiresIn = remember === true ? SESSION_DURATION_REMEMBER_SECONDS : SESSION_DURATION_SECONDS;
|
||||
// The flag is embedded as a claim so sliding renewal can re-issue with the
|
||||
// same duration AND cookie semantics (false → browser-session cookie is not
|
||||
// recoverable from exp − iat). Omitted when the caller didn't choose, so
|
||||
// register/demo/passkey tokens keep their historical payload.
|
||||
return jwt.sign(
|
||||
{ id: user.id, pv },
|
||||
{ id: user.id, pv, ...(typeof remember === 'boolean' ? { remember } : {}) },
|
||||
JWT_SECRET,
|
||||
{ expiresIn, algorithm: 'HS256' }
|
||||
);
|
||||
@@ -514,7 +518,8 @@ export class AuthService {
|
||||
changePassword(
|
||||
userId: number,
|
||||
userEmail: string,
|
||||
rawBody: unknown
|
||||
rawBody: unknown,
|
||||
remember?: boolean,
|
||||
): { error?: string; status?: number; success?: boolean; token?: string } {
|
||||
const body = rawBody as { current_password?: string; new_password?: string };
|
||||
if (this.isOidcOnlyMode()) {
|
||||
@@ -554,8 +559,10 @@ export class AuthService {
|
||||
try { revokeUserSessions?.(userId); } catch { /* best-effort */ }
|
||||
|
||||
// Re-issue a session bound to the new password_version so the current device
|
||||
// stays logged in while other existing sessions are rotated out by the pv gate.
|
||||
const token = this.generateToken({ id: userId, password_version: newPv });
|
||||
// stays logged in while other existing sessions are rotated out by the pv
|
||||
// gate — preserving the login's remember choice instead of downgrading a
|
||||
// remembered session to the default duration (#1927).
|
||||
const token = this.generateToken({ id: userId, password_version: newPv }, remember);
|
||||
return { success: true, token };
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,28 @@ export function extractToken(req: Request): string | null {
|
||||
* compares the claim. Several paths used to call `jwt.verify` directly and skip
|
||||
* the DB lookup, so a stolen token kept working after the victim reset.
|
||||
*/
|
||||
export interface SessionClaims {
|
||||
id?: number;
|
||||
pv?: number;
|
||||
remember?: boolean;
|
||||
purpose?: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode (NOT verify) a session token's claims. Only for callers that have
|
||||
* already authenticated the request through a guard and need the token's
|
||||
* metadata — sliding renewal and the password-change cookie re-issue read the
|
||||
* `remember` claim this way. Never use this as an auth check.
|
||||
*/
|
||||
export function decodeSessionClaims(token: string | undefined): SessionClaims | null {
|
||||
if (!token) return null;
|
||||
const decoded = jwt.decode(token);
|
||||
if (!decoded || typeof decoded !== 'object') return null;
|
||||
return decoded as SessionClaims;
|
||||
}
|
||||
|
||||
export function verifyJwtAndLoadUser(token: string): User | null {
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] }) as { id: number; pv?: number; purpose?: string };
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
import { decodeSessionClaims } from './jwt-verify';
|
||||
import { setAuthCookie } from '../common/cookie';
|
||||
|
||||
/**
|
||||
* Sliding session renewal (#1927): once a cookie-authenticated session token is
|
||||
* past half of its lifetime, re-issue the trek_session cookie with a fresh
|
||||
* token so an active user never hits the expiry cliff mid-use. Renewal
|
||||
* preserves the login's remember semantics via the token's `remember` claim
|
||||
* (true → long persistent cookie, false → browser-session cookie, absent → the
|
||||
* historical default duration).
|
||||
*
|
||||
* Cheap by construction — no DB access:
|
||||
* - `req.user` is only set after a guard ran verifyJwtAndLoadUser on this exact
|
||||
* request, which already proved the token's `pv` matches the DB, so both the
|
||||
* user id and pv can be copied from the decoded (not re-verified) token.
|
||||
* - The cookie is only renewed when it is the verified credential: extractToken
|
||||
* prefers the cookie over the Authorization header, so cookie-present +
|
||||
* req.user set means the guard verified the cookie. Bearer-only callers
|
||||
* (MCP, API clients) are never renewed.
|
||||
*
|
||||
* The cookie is written eagerly, before the handler runs: renewal doesn't
|
||||
* depend on the handler's outcome, and this sidesteps @Res()/streaming routes
|
||||
* whose headers are gone by the time the observable settles. When a handler
|
||||
* later sets or clears trek_session itself (password change, logout), Express
|
||||
* appends its Set-Cookie after this one and the browser keeps the last — the
|
||||
* handler's cookie wins, which is the correct outcome.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionRenewalInterceptor implements NestInterceptor {
|
||||
constructor(private readonly auth: AuthService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
if (context.getType() === 'http') {
|
||||
const req = context.switchToHttp().getRequest<Request & { user?: { id: number } }>();
|
||||
const res = context.switchToHttp().getResponse<Response>();
|
||||
this.maybeRenew(req, res);
|
||||
}
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
private maybeRenew(req: Request & { user?: { id: number } }, res: Response): void {
|
||||
const cookieToken: string | undefined = (req as { cookies?: Record<string, string> }).cookies?.trek_session;
|
||||
if (!req.user || !cookieToken || res.headersSent) return;
|
||||
|
||||
const claims = decodeSessionClaims(cookieToken);
|
||||
if (!claims || claims.purpose || claims.id !== req.user.id) return;
|
||||
if (typeof claims.iat !== 'number' || typeof claims.exp !== 'number' || claims.exp <= claims.iat) return;
|
||||
|
||||
const halfLife = claims.iat + (claims.exp - claims.iat) / 2;
|
||||
if (Date.now() / 1000 < halfLife) return;
|
||||
|
||||
const token = this.auth.generateToken(
|
||||
{ id: req.user.id, password_version: claims.pv ?? 0 },
|
||||
claims.remember,
|
||||
);
|
||||
setAuthCookie(res, token, req, claims.remember);
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,28 @@ export function seedUser(db: Database.Database, overrides: Partial<SeededUser> =
|
||||
return user;
|
||||
}
|
||||
|
||||
export interface SessionSignOptions {
|
||||
/** Embed the remember claim (#1927). Omitted when undefined, like the real generateToken. */
|
||||
remember?: boolean;
|
||||
/** Backdate the token: seconds of its lifetime already consumed. Requires lifetime. */
|
||||
consumed?: number;
|
||||
/** Token lifetime in seconds. Default: no exp claim (the historical harness token). */
|
||||
lifetime?: number;
|
||||
}
|
||||
|
||||
/** Sign a `trek_session` token the real guard will accept (matching JWT_SECRET + pv). */
|
||||
export function signSession(userId: number, passwordVersion = 0): string {
|
||||
return jwt.sign({ id: userId, pv: passwordVersion }, JWT_SECRET, { algorithm: 'HS256' });
|
||||
export function signSession(userId: number, passwordVersion = 0, opts: SessionSignOptions = {}): string {
|
||||
const payload: Record<string, unknown> = { id: userId, pv: passwordVersion };
|
||||
if (typeof opts.remember === 'boolean') payload.remember = opts.remember;
|
||||
// jsonwebtoken computes exp = iat + expiresIn when iat is supplied in the payload.
|
||||
if (typeof opts.consumed === 'number') payload.iat = Math.floor(Date.now() / 1000) - opts.consumed;
|
||||
return jwt.sign(payload, JWT_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
...(typeof opts.lifetime === 'number' ? { expiresIn: opts.lifetime } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Convenience: the Cookie header value for a signed session. */
|
||||
export function sessionCookie(userId: number, passwordVersion = 0): string {
|
||||
return `trek_session=${signSession(userId, passwordVersion)}`;
|
||||
export function sessionCookie(userId: number, passwordVersion = 0, opts: SessionSignOptions = {}): string {
|
||||
return `trek_session=${signSession(userId, passwordVersion, opts)}`;
|
||||
}
|
||||
|
||||
@@ -301,4 +301,7 @@ export const TEST_CONFIG = {
|
||||
SESSION_DURATION: '24h',
|
||||
SESSION_DURATION_MS: 86400000,
|
||||
SESSION_DURATION_SECONDS: 86400,
|
||||
SESSION_DURATION_REMEMBER: '30d',
|
||||
SESSION_DURATION_REMEMBER_MS: 2592000000,
|
||||
SESSION_DURATION_REMEMBER_SECONDS: 2592000,
|
||||
};
|
||||
|
||||
@@ -480,6 +480,18 @@ describe('changePassword — session invalidation', () => {
|
||||
|
||||
expect(verifyJwtAndLoadUser(stolen)).toBeNull(); // invalidated by the pv bump
|
||||
});
|
||||
|
||||
it('AUTH-DB-036d: preserves the remember choice in the re-issued session (#1927)', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { user, password } = createUser(testDb);
|
||||
|
||||
const result = svc.changePassword(user.id, user.email, { current_password: password, new_password: 'New1234!' }, true);
|
||||
expect(result.success).toBe(true);
|
||||
const decoded = jwt.decode(result.token!) as { remember?: boolean; iat: number; exp: number };
|
||||
expect(decoded.remember).toBe(true);
|
||||
expect(decoded.exp - decoded.iat).toBe(2592000); // remember window survives the change
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -965,3 +977,25 @@ describe('MCP token verification round-trips', () => {
|
||||
expect(svc.verifyJwtToken('not-a-jwt')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateToken remember claim (#1927)', () => {
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
it('AUTH-TOKEN-001: embeds remember and picks the matching lifetime when the caller chose', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const long = jwt.decode(svc.generateToken({ id: user.id }, true)) as { remember?: boolean; iat: number; exp: number };
|
||||
expect(long.remember).toBe(true);
|
||||
expect(long.exp - long.iat).toBe(2592000);
|
||||
|
||||
const short = jwt.decode(svc.generateToken({ id: user.id }, false)) as { remember?: boolean; iat: number; exp: number };
|
||||
expect(short.remember).toBe(false);
|
||||
expect(short.exp - short.iat).toBe(86400);
|
||||
});
|
||||
|
||||
it('AUTH-TOKEN-002: omits the claim entirely when the caller did not choose (legacy payload)', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const decoded = jwt.decode(svc.generateToken({ id: user.id })) as { remember?: boolean; iat: number; exp: number };
|
||||
expect('remember' in decoded).toBe(false);
|
||||
expect(decoded.exp - decoded.iat).toBe(86400);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Unit tests for SessionRenewalInterceptor (#1927) — sliding renewal of the
|
||||
* trek_session cookie once a cookie-authenticated token is past half of its
|
||||
* lifetime. Covers the renew path (fresh token + cookie semantics per the
|
||||
* `remember` claim) and every skip condition: young token, bearer-only request,
|
||||
* unauthenticated request, purpose token, id mismatch, malformed claims,
|
||||
* headers already sent, non-http context.
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { CallHandler, ExecutionContext } from '@nestjs/common';
|
||||
import { of, lastValueFrom } from 'rxjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
vi.mock('../../../src/config', () => ({
|
||||
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
||||
SESSION_DURATION: '24h',
|
||||
SESSION_DURATION_MS: 86400000,
|
||||
SESSION_DURATION_SECONDS: 86400,
|
||||
SESSION_DURATION_REMEMBER: '30d',
|
||||
SESSION_DURATION_REMEMBER_MS: 2592000000,
|
||||
SESSION_DURATION_REMEMBER_SECONDS: 2592000,
|
||||
updateJwtSecret: () => {},
|
||||
}));
|
||||
|
||||
import { SessionRenewalInterceptor } from '../../../src/nest/auth/session-renewal.interceptor';
|
||||
import type { AuthService } from '../../../src/nest/auth/auth.service';
|
||||
|
||||
const SECRET = 'test-jwt-secret-for-trek-testing-only';
|
||||
|
||||
/** Sign a session token whose lifetime is `lifetime` seconds, `consumed` of which have already passed. */
|
||||
function token(payload: Record<string, unknown>, lifetime: number, consumed: number): string {
|
||||
const iat = Math.floor(Date.now() / 1000) - consumed;
|
||||
// jsonwebtoken computes exp = iat + expiresIn when iat is supplied in the payload.
|
||||
return jwt.sign({ ...payload, iat }, SECRET, { expiresIn: lifetime, algorithm: 'HS256' });
|
||||
}
|
||||
|
||||
function makeRes(headersSent = false) {
|
||||
return { headersSent, cookie: vi.fn() };
|
||||
}
|
||||
|
||||
type ReqShape = { cookies?: Record<string, string>; user?: { id: number }; headers?: Record<string, string>; secure?: boolean };
|
||||
|
||||
function ctx(req: ReqShape, res: ReturnType<typeof makeRes>, type = 'http'): ExecutionContext {
|
||||
return {
|
||||
getType: () => type,
|
||||
switchToHttp: () => ({ getRequest: () => req, getResponse: () => res }),
|
||||
} as unknown as ExecutionContext;
|
||||
}
|
||||
|
||||
function handler(): CallHandler & { handle: ReturnType<typeof vi.fn> } {
|
||||
return { handle: vi.fn(() => of('ok')) };
|
||||
}
|
||||
|
||||
function makeInterceptor() {
|
||||
const generateToken = vi.fn().mockReturnValue('renewed.jwt');
|
||||
const interceptor = new SessionRenewalInterceptor({ generateToken } as unknown as AuthService);
|
||||
return { interceptor, generateToken };
|
||||
}
|
||||
|
||||
async function run(interceptor: SessionRenewalInterceptor, req: ReqShape, res: ReturnType<typeof makeRes>, type = 'http') {
|
||||
const h = handler();
|
||||
const out = await lastValueFrom(interceptor.intercept(ctx(req, res, type), h));
|
||||
expect(out).toBe('ok');
|
||||
expect(h.handle).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
describe('SessionRenewalInterceptor', () => {
|
||||
it('SES-RENEW-001: does not renew a token under half of its lifetime', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
const t = token({ id: 1, pv: 0 }, 86400, 1000); // ~1% consumed
|
||||
await run(interceptor, { cookies: { trek_session: t }, user: { id: 1 } }, res);
|
||||
expect(generateToken).not.toHaveBeenCalled();
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SES-RENEW-002: renews a token past half of its lifetime, copying pv from the old token', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
const t = token({ id: 1, pv: 3 }, 86400, 60000); // ~70% consumed
|
||||
await run(interceptor, { cookies: { trek_session: t }, user: { id: 1 } }, res);
|
||||
expect(generateToken).toHaveBeenCalledWith({ id: 1, password_version: 3 }, undefined);
|
||||
expect(res.cookie).toHaveBeenCalledWith('trek_session', 'renewed.jwt', expect.objectContaining({ maxAge: 86400000, httpOnly: true }));
|
||||
});
|
||||
|
||||
it('SES-RENEW-003: a remember:true claim renews with the long persistent cookie', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
const t = token({ id: 1, pv: 0, remember: true }, 2592000, 1600000); // ~62% consumed
|
||||
await run(interceptor, { cookies: { trek_session: t }, user: { id: 1 } }, res);
|
||||
expect(generateToken).toHaveBeenCalledWith({ id: 1, password_version: 0 }, true);
|
||||
expect(res.cookie).toHaveBeenCalledWith('trek_session', 'renewed.jwt', expect.objectContaining({ maxAge: 2592000000 }));
|
||||
});
|
||||
|
||||
it('SES-RENEW-004: a remember:false claim renews as a browser-session cookie (no maxAge)', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
const t = token({ id: 1, pv: 0, remember: false }, 86400, 60000);
|
||||
await run(interceptor, { cookies: { trek_session: t }, user: { id: 1 } }, res);
|
||||
expect(generateToken).toHaveBeenCalledWith({ id: 1, password_version: 0 }, false);
|
||||
const options = res.cookie.mock.calls[0][2];
|
||||
expect(options).not.toHaveProperty('maxAge');
|
||||
});
|
||||
|
||||
it('SES-RENEW-005: a claimless legacy token renews with the historical default cookie', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
const t = token({ id: 1, pv: 0 }, 86400, 60000);
|
||||
await run(interceptor, { cookies: { trek_session: t }, user: { id: 1 } }, res);
|
||||
expect(generateToken).toHaveBeenCalledWith({ id: 1, password_version: 0 }, undefined);
|
||||
expect(res.cookie).toHaveBeenCalledWith('trek_session', 'renewed.jwt', expect.objectContaining({ maxAge: 86400000 }));
|
||||
});
|
||||
|
||||
it('SES-RENEW-006: never renews a Bearer-only request (no session cookie)', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
await run(interceptor, { headers: { authorization: 'Bearer x' }, user: { id: 1 } }, res);
|
||||
expect(generateToken).not.toHaveBeenCalled();
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SES-RENEW-007: never renews an unauthenticated (public-route) request', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
const t = token({ id: 1, pv: 0 }, 86400, 60000);
|
||||
await run(interceptor, { cookies: { trek_session: t } }, res);
|
||||
expect(generateToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SES-RENEW-008: never renews a purpose-scoped token or a cookie whose id differs from req.user', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const purposeRes = makeRes();
|
||||
const purposeToken = token({ id: 1, pv: 0, purpose: 'mfa_login' }, 86400, 60000);
|
||||
await run(interceptor, { cookies: { trek_session: purposeToken }, user: { id: 1 } }, purposeRes);
|
||||
|
||||
const mismatchRes = makeRes();
|
||||
const otherUsers = token({ id: 2, pv: 0 }, 86400, 60000);
|
||||
await run(interceptor, { cookies: { trek_session: otherUsers }, user: { id: 1 } }, mismatchRes);
|
||||
|
||||
expect(generateToken).not.toHaveBeenCalled();
|
||||
expect(purposeRes.cookie).not.toHaveBeenCalled();
|
||||
expect(mismatchRes.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SES-RENEW-009: skips when headers were already sent or the cookie is not a JWT', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const sentRes = makeRes(true);
|
||||
const t = token({ id: 1, pv: 0 }, 86400, 60000);
|
||||
await run(interceptor, { cookies: { trek_session: t }, user: { id: 1 } }, sentRes);
|
||||
|
||||
const garbageRes = makeRes();
|
||||
await run(interceptor, { cookies: { trek_session: 'not-a-jwt' }, user: { id: 1 } }, garbageRes);
|
||||
|
||||
expect(generateToken).not.toHaveBeenCalled();
|
||||
expect(sentRes.cookie).not.toHaveBeenCalled();
|
||||
expect(garbageRes.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SES-RENEW-010: skips tokens with missing or inverted iat/exp claims', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
// Hand-built token without exp (noTimestamp keeps iat but expiresIn omitted).
|
||||
const noExp = jwt.sign({ id: 1, pv: 0 }, SECRET, { algorithm: 'HS256' });
|
||||
await run(interceptor, { cookies: { trek_session: noExp }, user: { id: 1 } }, res);
|
||||
expect(generateToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SES-RENEW-011: ignores non-http execution contexts', async () => {
|
||||
const { interceptor, generateToken } = makeInterceptor();
|
||||
const res = makeRes();
|
||||
const t = token({ id: 1, pv: 0 }, 86400, 60000);
|
||||
await run(interceptor, { cookies: { trek_session: t }, user: { id: 1 } }, res, 'ws');
|
||||
expect(generateToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user