Compare commits
2
Commits
develop
...
0dd98b292b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dd98b292b | ||
|
|
53ade94e4b |
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.21.0-bookworm-slim AS base
|
FROM node:24.22.1-bookworm-slim AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM base AS dependencies
|
FROM base AS dependencies
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ export default defineConfig(
|
|||||||
linterOptions: { reportUnusedDisableDirectives: 'error' },
|
linterOptions: { reportUnusedDisableDirectives: 'error' },
|
||||||
settings: {
|
settings: {
|
||||||
'import-x/resolver-next': [createTypeScriptImportResolver()],
|
'import-x/resolver-next': [createTypeScriptImportResolver()],
|
||||||
'node': { tryExtensions: ['.ts', '.js', '.json', '.node'], version: '24.21.0' },
|
'node': { tryExtensions: ['.ts', '.js', '.json', '.node'], version: '24.22.1' },
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'curly': ['error', 'multi-line'],
|
'curly': ['error', 'multi-line'],
|
||||||
|
|||||||
Generated
+1
-1
@@ -75,7 +75,7 @@
|
|||||||
"typescript-eslint": "8.70.1"
|
"typescript-eslint": "8.70.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "v24.21.0"
|
"node": "v24.22.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-devkit/schematics": {
|
"node_modules/@angular-devkit/schematics": {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "v24.21.0"
|
"node": "v24.22.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "prisma generate && nest build",
|
"build": "prisma generate && nest build",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { AuthenticationResult, AuthUser, RefreshPayload } from './auth.types';
|
|||||||
const EXPECTED_ROTATED_SESSIONS = 1;
|
const EXPECTED_ROTATED_SESSIONS = 1;
|
||||||
|
|
||||||
const PASSWORD_HASH_ROUNDS = 12;
|
const PASSWORD_HASH_ROUNDS = 12;
|
||||||
const REFRESH_TTL_MS = 604_800_000;
|
export const REFRESH_TTL_MS = 604_800_000;
|
||||||
|
|
||||||
const ACCESS_TTL = '15m';
|
const ACCESS_TTL = '15m';
|
||||||
const REFRESH_TTL = '7d';
|
const REFRESH_TTL = '7d';
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
|
|
||||||
import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator';
|
import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator';
|
||||||
import { HttpErrorCode } from '../../../common/error-code';
|
import { HttpErrorCode } from '../../../common/error-code';
|
||||||
import { AuthService } from '../application/auth.service';
|
import { AuthService, REFRESH_TTL_MS } from '../application/auth.service';
|
||||||
import { AccessPayload, AuthenticationResult } from '../application/auth.types';
|
import { AccessPayload, AuthenticationResult } from '../application/auth.types';
|
||||||
import { LoginDto, RegisterDto } from './auth.dto';
|
import { LoginDto, RegisterDto } from './auth.dto';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
@@ -134,6 +134,7 @@ export class AuthController {
|
|||||||
sameSite: 'lax' as const,
|
sameSite: 'lax' as const,
|
||||||
secure: process.env['NODE_ENV'] !== 'development',
|
secure: process.env['NODE_ENV'] !== 'development',
|
||||||
path: COOKIE_PATH,
|
path: COOKIE_PATH,
|
||||||
|
maxAge: REFRESH_TTL_MS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-5
@@ -131,6 +131,9 @@ describe('Authentication (e2e)', () => {
|
|||||||
.filter((attribute) => !attribute.startsWith('Expires='))
|
.filter((attribute) => !attribute.startsWith('Expires='))
|
||||||
.sort((a, b) => a.localeCompare(b));
|
.sort((a, b) => a.localeCompare(b));
|
||||||
const host = ['HttpOnly', 'Path=/api/v1/auth', 'SameSite=Lax'];
|
const host = ['HttpOnly', 'Path=/api/v1/auth', 'SameSite=Lax'];
|
||||||
|
// Persistent for the Session lifetime (7 days), otherwise the browser drops it on restart.
|
||||||
|
const persistent = [...host, 'Max-Age=604800'];
|
||||||
|
const sorted = (items: string[]) => [...items].sort((a, b) => a.localeCompare(b));
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
const registration = await request(app.getHttpServer())
|
const registration = await request(app.getHttpServer())
|
||||||
.post('/api/v1/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
@@ -150,7 +153,7 @@ describe('Authentication (e2e)', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
it('issues, rotates and clears it HttpOnly, Lax, path-scoped, host-only and Secure in production', async () => {
|
it('issues and rotates it persistent (Max-Age) HttpOnly, Lax, path-scoped, host-only and Secure in production, and clears it', async () => {
|
||||||
await inEnvironment('production', async () => {
|
await inEnvironment('production', async () => {
|
||||||
const issued = await login();
|
const issued = await login();
|
||||||
const rotated = await request(app.getHttpServer())
|
const rotated = await request(app.getHttpServer())
|
||||||
@@ -162,16 +165,20 @@ describe('Authentication (e2e)', () => {
|
|||||||
.set('Cookie', rotated.headers['set-cookie'][0])
|
.set('Cookie', rotated.headers['set-cookie'][0])
|
||||||
.expect(204);
|
.expect(204);
|
||||||
|
|
||||||
// Exact set: any added Domain/Max-Age or a dropped flag fails.
|
// Exact set: any added Domain, changed Max-Age or dropped flag fails.
|
||||||
for (const cookie of [issued, rotated.headers['set-cookie'][0], cleared.headers['set-cookie'][0]]) {
|
for (const cookie of [issued, rotated.headers['set-cookie'][0]]) {
|
||||||
expect(attributes(cookie)).toEqual([...host, 'Secure'].sort((a, b) => a.localeCompare(b)));
|
expect(attributes(cookie)).toEqual(sorted([...persistent, 'Secure']));
|
||||||
}
|
}
|
||||||
|
// Clearing must not carry Max-Age; Express expires it with a past Expires.
|
||||||
|
const clearedCookie = cleared.headers['set-cookie'][0];
|
||||||
|
expect(attributes(clearedCookie)).toEqual(sorted([...host, 'Secure']));
|
||||||
|
expect(clearedCookie).toContain('Expires=Thu, 01 Jan 1970');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('drops only Secure in development and keeps every other attribute', async () => {
|
it('drops only Secure in development and keeps every other attribute', async () => {
|
||||||
await inEnvironment('development', async () => {
|
await inEnvironment('development', async () => {
|
||||||
expect(attributes(await login())).toEqual(host);
|
expect(attributes(await login())).toEqual(sorted(persistent));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user