fix: align password policy to 8-72 printable ASCII on register (WealthLedger/backend#60)
Backend verify / commits (pull_request) Successful in 32s
Backend verify / security (pull_request) Successful in 13s
Backend verify / verify (pull_request) Successful in 4m59s

Register accepts only printable ASCII (letters, digits, symbols, space),
8-72 characters, so characters equal bytes and the bcrypt 72-byte limit
holds without silent truncation. Login only caps the password at 72 bytes
so accounts created under the previous policy can still sign in.

CredentialsDto is split into RegisterDto and LoginDto; OpenAPI publishes
the register pattern and length bounds.
This commit is contained in:
2026-10-16 09:41:58 +03:00
parent 774e58ef80
commit ec95d2756d
4 changed files with 127 additions and 15 deletions
+41
View File
@@ -2,7 +2,9 @@ import { INestApplication } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import * as bcrypt from 'bcrypt';
import { PARAMS_PROVIDER_TOKEN } from 'nestjs-pino';
import { randomUUID } from 'node:crypto';
import request from 'supertest';
import { App } from 'supertest/types';
@@ -265,6 +267,45 @@ describe('Authentication (e2e)', () => {
expect(await app.get(PrismaService).user.count()).toBe(0);
});
it('applies the ASCII 8..72 password policy on register and keeps login lenient', async () => {
const validation = (detail: string) => ({
status: 400,
code: 'VALIDATION_ERROR',
errors: [{ pointer: '#/password', detail }],
});
for (const [index, password] of ['a'.repeat(8), ' '.repeat(2) + 'b'.repeat(70)].entries()) {
const credentials = { email: `user${index}@example.com`, password };
await request(app.getHttpServer()).post('/api/v1/auth/register').send(credentials).expect(200);
await request(app.getHttpServer()).post('/api/v1/auth/login').send(credentials).expect(200);
}
for (const password of ['a'.repeat(7), 'a'.repeat(73), 'пароль-пароль', 'passw😀rd', 'pass\tword1']) {
await request(app.getHttpServer())
.post('/api/v1/auth/register')
.send({ email: 'rejected@example.com', password })
.expect(400)
.expect('Content-Type', /application\/problem\+json/)
.expect(({ body }: { body: unknown }) =>
expect(body).toEqual(validation('must be 8-72 printable ASCII characters')),
);
}
expect(await app.get(PrismaService).user.count()).toBe(2);
// An account created under the previous policy (Cyrillic, 12+ chars) must still log in.
const legacy = { email: 'legacy@example.com', password: 'пароль-пароль' };
const prisma = app.get(PrismaService);
await prisma.user.create({
data: { id: randomUUID(), email: legacy.email, passwordHash: await bcrypt.hash(legacy.password, 4) },
});
await request(app.getHttpServer()).post('/api/v1/auth/login').send(legacy).expect(200);
await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send({ email: legacy.email, password: 'a'.repeat(73) })
.expect(400)
.expect(({ body }: { body: unknown }) =>
expect(body).toEqual(validation('must not exceed 72 UTF-8 bytes')),
);
});
it('guards T-Bank connection and never returns a token', async () => {
const registration = await request(app.getHttpServer())
.post('/api/v1/auth/register')