Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CRYPTO_SECRET=6KYQBP847D4ATSFA
CRYPTO_SECRET2=8Q8VMUE3BJZV87GT
OPAQUE_SECRET=awirun08Dxx3yBpGdd0W2-j4Tl5ip02M5Uu7EVRhtqUzEEdW5EhlP1QC1z3UX8hB7cavoCyem4Kl0iCymdTsbk_tbiJu8-zzrWF3S1nQ2cGY5TkDXIatNKh5riaw7xINwkTOycgxvsIENsPn2W19OgAw2_Zih_1f4Px6ncj7-iw
GATEWAY_SECRET=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTDlDTVRlZGEramdIcGJuTmtlSm51TlpnYzg5TGFvMGNQNkl6dlJrYTJ0MUVKbnh5ZTA1CndSWGZLMXFpbTFOMGU3cGhkd0RkRWYvNGJ1eFc5V2g1UWxzQ0F3RUFBUUpCQUpnRXljLzF2VDdGWFNyK3JpTWcKWFAxQ09LNTdaeCtCUFVyamZQTytHYSszWk1MRHhqaG44dGZmV1E4VUpKemJ5VkQ0Q0JqTmNra2xRN3phQ29BNwo1WWtDSVFEd0h2MXhVRkFVUkI2b3QwL0JMMWNxek5SNU80dFBMT0NjL2gyK0o4Y09WUUloQU12b0FrMm5IQWhSClpRNmhNZGFTdWtPVTE3MTYvRGxnNWNiSXNWYXh0bDN2QWlBTUdTT2YzL0lJODEyd0ZueFlPWEJrNGFrYTZwc2MKUkNDVkNHQ3JRZ25QZVFJZ2NTU2E2cFc0YzFFZTN5Qkl0RVNVZ0YxOTNKRDZsYWdUdDlxeXRHVkZ5UmNDSVFDYgp6dE85ampXcERmYTlnWTV2dVB4MFgyUkcxbjJQb0ZYVjVXT29RanNqbnc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
DRIVE_GATEWAY_PUBLIC_SECRET=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE0bmVlL0Y5OC8wTjJhbFNKQ3JnMAp5bzJRRysydzR5SGk3ZXVDT3JYYUhENzFmN0NrWExWdHlxWTFRRWNFVnFuQm5QUnVpR1EvSklzWkJKVXhoQTlOCmdwdUpIbVY0aytnMEorRGcxeS9wd3k4L0lNM0FhTnNtWWp4c0NZQUZBSWhqUDZNZlBsVTNTYWdyekVRNklFU3MKeHBzT1JhRXd3WUZIWm42TU50b0FGbktMb3VlMVprUVpSSlVwNGZSSlMvcGNrdVNSNjZFcjFLMjhYckpieGE5egpCNG9SbFJMb0ExQ2cvTFN6ZEFQc2lVMzlSOWtlamxBKzFOMkxLVWFrNVJ3OWN5TkM0N3lHS3ZicSt2TTNYaUFmCk43Wk1teTdkY09aeXcyZW9idFFUVzVtTmR1WElWOHhWeWUzMkpyY3BuYWVqbDlUMG5TWGJEWE9OV2d6N0lWcmoKNFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t
HOST_DRIVE_WEB=http://localhost:3000
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ DRIVE_GATEWAY_PUBLIC_SECRET=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2tx

CRYPTO_SECRET=6KYQBP847D4ATSFA
CRYPTO_SECRET2=8Q8VMUE3BJZV87GT
OPAQUE_SECRET=awirun08Dxx3yBpGdd0W2-j4Tl5ip02M5Uu7EVRhtqUzEEdW5EhlP1QC1z3UX8hB7cavoCyem4Kl0iCymdTsbk_tbiJu8-zzrWF3S1nQ2cGY5TkDXIatNKh5riaw7xINwkTOycgxvsIENsPn2W19OgAw2_Zih_1f4Px6ncj7-iw

NOTIFICATIONS_URL=http://localhost:3000
NOTIFICATIONS_API_KEY=secret
Expand Down
24 changes: 24 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { JestConfigWithTsJest } from 'ts-jest';

const config: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: {
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
},
],
},
roots: ['src', 'test'],
collectCoverageFrom: ['**/*.(t|j)s'],
testRegex: String.raw`.*\.spec\.ts$`,
transformIgnorePatterns: ['node_modules/(?!@serenity-kit/opaque)'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};

export default config;
7 changes: 7 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ if (typeof globalThis.BigInt === 'function') {
const { TextEncoder, TextDecoder } = require('util');
global.TextEncoder = global.TextEncoder || TextEncoder;
global.TextDecoder = global.TextDecoder || TextDecoder;

// eslint-disable-next-line @typescript-eslint/no-var-requires
const opaque = require('@serenity-kit/opaque');

beforeAll(async () => {
await opaque.ready;
});
19 changes: 19 additions & 0 deletions migrations/20251216131653-add-registration-record.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const tableName = 'users';
const newColumn = 'registration_record';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(tableName, newColumn, {
type: Sequelize.STRING(300),
});
},

async down(queryInterface, Sequelize) {
await queryInterface.removeColumn(tableName, newColumn, {
type: Sequelize.STRING(300),
});
},
};
23 changes: 1 addition & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@nestjs/swagger": "^11.2.3",
"@nestjs/throttler": "^6.4.0",
"@sendgrid/mail": "^8.1.6",
"@serenity-kit/opaque": "^1.0.0",
"@types/passport-jwt": "^3.0.13",
"agentkeepalive": "^4.5.0",
"axios": "^1.12.0",
Expand Down Expand Up @@ -131,28 +132,6 @@
"tsconfig-paths": "4.2.0",
"typescript": "^5.8.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"roots": [
"src",
"test"
],
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful, we use jest to generate the report SonarCloud uses to determine the test coverage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it to jest.config.ts

"**/*.(t|j)s"
],
"testEnvironment": "node",
"setupFilesAfterEnv": [
"<rootDir>/jest.setup.js"
]
},
"lint-staged": {
"./src/**/*.{js,ts}": "yarn run lint"
}
Expand Down
1 change: 1 addition & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default () => ({
redisConnectionString: process.env.REDIS_CONNECTION_STRING,
},
secrets: {
serverSetup: process.env.OPAQUE_SECRET,
cryptoSecret: process.env.CRYPTO_SECRET,
cryptoSecret2: process.env.CRYPTO_SECRET2,
jwt: process.env.JWT_SECRET,
Expand Down
33 changes: 33 additions & 0 deletions src/externals/crypto/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { AesService } from './aes';
import CryptoJS from 'crypto-js';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import * as opaque from '@serenity-kit/opaque';
import { v4 as uuidv4 } from 'uuid';

export enum AsymmetricEncryptionAlgorithms {
EllipticCurve = 'ed25519',
Expand All @@ -14,9 +16,11 @@ export class CryptoService {
private readonly configService: ConfigService;
private readonly aesService: AesService;
private readonly cryptoSecret: string;
private readonly serverSetup: string;

constructor(configService: ConfigService) {
this.configService = configService;
this.serverSetup = this.configService.get('secrets.serverSetup');
this.aesService = new AesService(
this.configService.get('secrets.cryptoSecret2'),
);
Expand Down Expand Up @@ -167,4 +171,33 @@ export class CryptoService {
return null;
}
}

startLoginOpaque(
email: string,
registrationRecord: string,
startLoginRequest: string,
): { loginResponse: string; serverLoginState: string } {
const { loginResponse, serverLoginState } = opaque.server.startLogin({
userIdentifier: email,
registrationRecord,
serverSetup: this.serverSetup,
startLoginRequest,
});

return { loginResponse, serverLoginState };
}

finishLoginOpaque(
finishLoginRequest: string,
serverLoginState: string,
): { sessionKey: string } {
return opaque.server.finishLogin({
finishLoginRequest,
serverLoginState,
});
}

generateSessionID(): string {
return uuidv4();
}
}
141 changes: 141 additions & 0 deletions src/modules/auth/auth.contoller.opaque.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { AuthController } from './auth.controller';
import { UserUseCases } from '../user/user.usecase';
import { CryptoService } from '../../externals/crypto/crypto.service';

import {
LoginAccessOpaqueFinishDto,
LoginAccessOpaqueStartDto,
} from './dto/login-access.dto';
import { Logger } from '@nestjs/common';
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { v4 } from 'uuid';

import { Test } from '@nestjs/testing';
import * as opaque from '@serenity-kit/opaque';
import { ConfigService } from '@nestjs/config';

describe('AuthController', () => {
let authController: AuthController;
let userUseCases: DeepMocked<UserUseCases>;
let cryptoService: DeepMocked<CryptoService>;

let serverSetupMock: string;

beforeAll(async () => {
serverSetupMock = opaque.server.createSetup();
});

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [AuthController],
providers: [
CryptoService,
{
provide: ConfigService,
useValue: {
get: (key: string) => {
if (key === 'secrets.serverSetup') return serverSetupMock;
if (key === 'secrets.cryptoSecret2') return 'a'.repeat(64); // Valid hex key
if (key === 'secrets.cryptoSecret') return 'b'.repeat(64); // Valid hex key
return null;
},
},
},
],
})
.setLogger(createMock<Logger>())
.useMocker((token) => {
if (token === CryptoService || token === ConfigService) {
return undefined;
}
return createMock();
})
.compile();

authController = moduleRef.get(AuthController);
userUseCases = moduleRef.get(UserUseCases);
cryptoService = moduleRef.get(CryptoService);
});

describe('POST /login-opaque', () => {
it('Should sucessfully finish both phases of the login', async () => {
const email = 'USER_test@gmail.com';
const password = v4();
const { startLoginRequest, clientLoginState } = opaque.client.startLogin({
password,
});
const { clientRegistrationState, registrationRequest } =
opaque.client.startRegistration({ password });
const { registrationResponse } = opaque.server.createRegistrationResponse(
{
serverSetup: serverSetupMock,
userIdentifier: email.toLowerCase(),
registrationRequest,
},
);
const { registrationRecord: registrationRecordMock } =
opaque.client.finishRegistration({
clientRegistrationState,
registrationResponse,
password,
});
const loginOpaqueDto = new LoginAccessOpaqueStartDto();
loginOpaqueDto.email = email;
loginOpaqueDto.startLoginRequest = startLoginRequest;

jest.spyOn(userUseCases, 'findByEmail').mockResolvedValue({
registrationRecord: registrationRecordMock,
} as any);
const startLoginOpaqueSpy = jest.spyOn(cryptoService, 'startLoginOpaque');
const resultPhaseOne =
await authController.loginOpaqueStart(loginOpaqueDto);
const serverLoginStateValue = userUseCases.setLoginState.mock.calls[0][1];

expect(startLoginOpaqueSpy).toHaveBeenCalledTimes(1);
expect(startLoginOpaqueSpy).toHaveBeenCalledWith(
loginOpaqueDto.email.toLowerCase(),
registrationRecordMock,
loginOpaqueDto.startLoginRequest,
);
expect(resultPhaseOne.loginResponse).toBeDefined();
expect(resultPhaseOne).toEqual({ loginResponse: expect.any(String) });

const loginOpaqueFinishDto = new LoginAccessOpaqueFinishDto();
loginOpaqueFinishDto.email = email;

jest
.spyOn(userUseCases, 'getLoginState')
.mockResolvedValue(serverLoginStateValue);

const { finishLoginRequest, sessionKey } = opaque.client.finishLogin({
clientLoginState,
loginResponse: resultPhaseOne.loginResponse,
password,
});

loginOpaqueFinishDto.finishLoginRequest = finishLoginRequest;

const finishLoginOpaqueSpy = jest.spyOn(
cryptoService,
'finishLoginOpaque',
);

const resultPhaseTwo =
await authController.loginOpaqueFinish(loginOpaqueFinishDto);

expect(finishLoginOpaqueSpy).toHaveBeenCalledTimes(1);
expect(finishLoginOpaqueSpy).toHaveBeenCalledWith(
finishLoginRequest,
serverLoginStateValue,
);

expect(resultPhaseTwo.user).toBeDefined();
expect(resultPhaseTwo.token).toBeDefined();
expect(resultPhaseTwo.sessionID).toBeDefined();
expect(userUseCases.setSessionKey).toHaveBeenCalledWith(
resultPhaseTwo.sessionID,
sessionKey,
);
});
});
});
1 change: 1 addition & 0 deletions src/modules/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('AuthController', () => {
hasEccKeys: true,
sKey: 'encryptedText',
tfa: true,
useOpaqueLogin: false,
});
});

Expand Down
Loading
Loading