Skip to content
Merged
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
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,70 @@ $ docker compose run --rm init-core
> This `init-core-fca-low` container is a dependency of the `core-fca-low` container.
> It will run the migration script every time the `core-fca-low` container is started.
> No need to do it manually when using the `dks switch`


## Generate a new version of class-validator-0.14.2

We forked the class-validator package because we needed inhertiance features and is not yet merged [into the main branch](https://github.com/typestack/class-validator/pull/2641).

To update the package, follow these steps.

Get and update the project:
```bash
git clone git@github.com:proconnect-gouv/class-validator.git
```

Update the version attribute of `class-validator/package.json`:
```json
{
"name": "class-validator",
"version": "0.14.2-proconnect.1",
"...": "..."
}
```

Build the new package:
```bash
rm -rf build
npm ci --ignore-scripts
npm run prettier:check
npm run lint:check
npm run test:ci
npm run build:es2015
npm run build:esm5
npm run build:cjs
npm run build:umd
npm run build:types
cp LICENSE build/LICENSE
cp README.md build/README.md
jq 'del(.devDependencies) | del(.scripts)' package.json > build/package.json
npm pack ./build
```

Then push the built package in a dedicated branch:
```bash
git checkout -b build-0.14.2-proconnect.1
find . -mindepth 1 -maxdepth 1 \
! -name '.git' \
! -name '.idea' \
! -name '.gitignore' \
! -name 'build' \
-exec rm -rf {} +
mv build/* .
rmdir build
git add .
git commit -m "Build version 0.14.2-proconnect.1"
git push
```

Update `federation/back/package.json`:

```json
{
"dependencies": {
"class-validator": "git+https://github.com/proconnect-gouv/class-validator.git#build-0.14.2-proconnect.1",
}
}
```

Run yarn install.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ISessionService, SessionService } from '@fc/session';
import { getLoggerMock } from '@mocks/logger';

// --- Mocks for external dependencies ---
import { UserSession } from '../dto';
import { GetVerifySessionDto, UserSession } from '../dto';
import { CoreFcaAgentNotFromPublicServiceException } from '../exceptions';
import { CoreFcaControllerService } from '../services';
import { InteractionController } from './interaction.controller';
Expand Down Expand Up @@ -380,7 +380,7 @@ describe('InteractionController', () => {
idpIdentity: { sub: 'user1', extraClaims: 'extra' },
}),
set: jest.fn(),
} as unknown as ISessionService<UserSession>;
} as unknown as ISessionService<GetVerifySessionDto>;
const interactionAcr = 'high';

identityProviderMock.isActiveById.mockResolvedValue(true);
Expand Down Expand Up @@ -422,7 +422,7 @@ describe('InteractionController', () => {
isSilentAuthentication: true,
idpId: 'idp123',
}),
} as unknown as ISessionService<UserSession>;
} as unknown as ISessionService<GetVerifySessionDto>;

identityProviderMock.isActiveById.mockResolvedValue(false);

Expand All @@ -440,7 +440,7 @@ describe('InteractionController', () => {
interactionId: 'interaction123',
idpId: 'idp123',
}),
} as unknown as ISessionService<UserSession>;
} as unknown as ISessionService<GetVerifySessionDto>;

identityProviderMock.isActiveById.mockResolvedValue(false);
configServiceMock.get.mockReturnValueOnce({ urlPrefix: '/prefix' });
Expand All @@ -461,7 +461,7 @@ describe('InteractionController', () => {
idpId: 'idp123',
idpIdentity: { is_service_public: false },
}),
} as unknown as ISessionService<UserSession>;
} as unknown as ISessionService<GetVerifySessionDto>;

identityProviderMock.isActiveById.mockResolvedValue(true);
serviceProviderMock.getById.mockResolvedValue({ type: 'public' });
Expand All @@ -479,7 +479,7 @@ describe('InteractionController', () => {
spEssentialAcr: 'high',
idpAcr: 'low',
}),
} as unknown as ISessionService<UserSession>;
} as unknown as ISessionService<GetVerifySessionDto>;

identityProviderMock.isActiveById.mockResolvedValue(true);
serviceProviderMock.getById.mockResolvedValue({ type: 'public' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class InteractionController {
@Res() res: Response,
@Param() _params: Interaction,
@UserSessionDecorator(GetVerifySessionDto)
userSessionService: ISessionService<UserSession>,
userSessionService: ISessionService<GetVerifySessionDto>,
) {
const {
amr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AccountFcaService } from '@fc/account-fca';
import { validateDto } from '@fc/common';
import { ConfigService } from '@fc/config';
import { UserSession } from '@fc/core';
import { GetIdentityProviderSelectionSessionDto, UserSession } from '@fc/core';
import { CryptographyService } from '@fc/cryptography';
import { CsrfService } from '@fc/csrf';
import { LoggerService } from '@fc/logger';
Expand Down Expand Up @@ -128,7 +128,7 @@ describe('OidcClientController', () => {
const email = 'user@example.com';
const userSession = {
get: jest.fn().mockReturnValue({ idpLoginHint: email }),
} as unknown as ISessionService<UserSession>;
} as unknown as ISessionService<GetIdentityProviderSelectionSessionDto>;

const providers = [
{ title: 'Provider One', uid: 'idp1' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class OidcClientController {
async getIdentityProviderSelection(
@Res() res: Response,
@UserSessionDecorator(GetIdentityProviderSelectionSessionDto)
userSession: ISessionService<UserSession>,
userSession: ISessionService<GetIdentityProviderSelectionSessionDto>,
) {
const { idpLoginHint: email } = userSession.get();

Expand Down Expand Up @@ -201,7 +201,7 @@ export class OidcClientController {
* @ticket FC-1020
*/
@UserSessionDecorator(GetOidcCallbackSessionDto)
userSession: ISessionService<UserSession>,
userSession: ISessionService<GetOidcCallbackSessionDto>,
) {
// The session is duplicated here to mitigate cookie-theft-based attacks.
// For more information, refer to: https://gitlab.dev-franceconnect.fr/france-connect/fc/-/issues/1288
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,45 +63,25 @@ describe('UserSessionDecoratorFactory', () => {
jest.restoreAllMocks();
});

it('should return the bound session service with all methods when validations pass (with mandatory DTO)', async () => {
it('should return the bound session service with all methods when validations pass', async () => {
// Define a dummy DTO for mandatory validation.
class DummyDto {}

// Simulate no validation errors for both the mandatory DTO and the UserSession.
const validateMock = jest.mocked(validate);
validateMock.mockResolvedValueOnce([]); // For validating DummyDto instance.
validateMock.mockResolvedValueOnce([]); // For validating UserSession instance.
validateMock.mockResolvedValueOnce([]);

const result = await UserSessionDecoratorFactory(
DummyDto,
fakeExecutionContext,
);

// Expect the bound session service to have all expected methods.
expect(typeof result.get).toBe('function');
expect(typeof result.set).toBe('function');
expect(typeof result.commit).toBe('function');
expect(typeof result.duplicate).toBe('function');
expect(typeof result.reset).toBe('function');
expect(typeof result.destroy).toBe('function');

// Call duplicate and ensure that it calls the underlying sessionService.duplicate with the response.
const duplicateResult = result.duplicate();
expect(fakeSessionService.duplicate).toHaveBeenCalledWith(fakeResponse);
expect(duplicateResult).toBe('duplicatedSession');

// Call reset and destroy to verify they are bound with the response.
await result.reset();
expect(fakeSessionService.reset).toHaveBeenCalledWith(fakeResponse);
await result.destroy();
expect(fakeSessionService.destroy).toHaveBeenCalledWith(fakeResponse);

// Commit is not bound with the response so just check it is called.
await result.commit();
expect(fakeSessionService.commit).toHaveBeenCalled();
});

it('should throw SessionInvalidSessionException if mandatory DTO validation fails', async () => {
it('should throw SessionInvalidSessionException if a session DTO validation fails', async () => {
class DummyDto {}
const validationError = [
{
Expand All @@ -110,9 +90,8 @@ describe('UserSessionDecoratorFactory', () => {
},
];

// Simulate a validation error for the mandatory DTO.
const validateMock = jest.mocked(validate);
validateMock.mockResolvedValueOnce(validationError); // First call (for DummyDto)
validateMock.mockResolvedValueOnce(validationError);

await expect(
UserSessionDecoratorFactory(DummyDto, fakeExecutionContext),
Expand All @@ -121,28 +100,9 @@ describe('UserSessionDecoratorFactory', () => {
expect(validateMock).toHaveBeenCalledTimes(1);
});

it('should throw SessionInvalidSessionException if UserSession validation fails', async () => {
class DummyDto {}
const validationError = [
{ property: 'user', constraints: { isDefined: 'user must be defined' } },
];

// Simulate successful validation for the mandatory DTO, then a failure for the UserSession.
const validateMock = jest.mocked(validate);
validateMock.mockResolvedValueOnce([]); // For DummyDto
validateMock.mockResolvedValueOnce(validationError); // For UserSession

await expect(
UserSessionDecoratorFactory(DummyDto, fakeExecutionContext),
).rejects.toThrowError(SessionInvalidSessionException);

expect(validateMock).toHaveBeenCalledTimes(2);
});

it('should validate only UserSession when no mandatory DTO is provided', async () => {
// When no mandatory DTO is provided, only one validation (for UserSession) occurs.
it('should validate only UserSession when no session DTO is provided', async () => {
const validateMock = jest.mocked(validate);
validateMock.mockResolvedValueOnce([]); // For UserSession
validateMock.mockResolvedValueOnce([]);

const result = await UserSessionDecoratorFactory(
undefined,
Expand All @@ -152,14 +112,13 @@ describe('UserSessionDecoratorFactory', () => {
expect(typeof result.get).toBe('function');
});

it('should throw SessionInvalidSessionException if UserSession validation fails when no mandatory DTO is provided', async () => {
it('should throw SessionInvalidSessionException if UserSession validation fails when no session DTO is provided', async () => {
const validationError = [
{ property: 'user', constraints: { isDefined: 'user must be defined' } },
];

// Simulate a validation error for UserSession when no mandatory DTO is provided.
const validateMock = jest.mocked(validate);
validateMock.mockResolvedValueOnce(validationError); // For UserSession
validateMock.mockResolvedValueOnce(validationError);

await expect(
UserSessionDecoratorFactory(undefined, fakeExecutionContext),
Expand Down
19 changes: 7 additions & 12 deletions back/apps/core-fca-low/src/decorators/user-session.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ISessionService } from '@fc/session/interfaces';
import { SessionService } from '@fc/session/services';

export const UserSessionDecoratorFactory = async (
mandatoryPropertiesDto: Class<unknown>,
userSessionDto: Class<UserSession> = UserSession,
ctx: ExecutionContext,
): Promise<ISessionService<UserSession>> => {
const sessionService =
Expand All @@ -40,22 +40,17 @@ export const UserSessionDecoratorFactory = async (

const sessionData = boundSessionService.get();

if (mandatoryPropertiesDto) {
const object = plainToInstance(mandatoryPropertiesDto, sessionData);
const mandatoryPropertiesErrors = await validate(object as object);
const object = plainToInstance(userSessionDto, sessionData);
const validationErrors = await validate(object as object);

if (mandatoryPropertiesErrors.length) {
if (validationErrors.length) {
if (userSessionDto === UserSession) {
throw new SessionInvalidSessionException();
} else {
throw new SessionInvalidMandatoryFieldsException();
}
}

const object = plainToInstance(UserSession, sessionData);
const typeErrors = await validate(object as object);

if (typeErrors.length) {
throw new SessionInvalidSessionException();
}

return boundSessionService;
};

Expand Down
72 changes: 0 additions & 72 deletions back/apps/core-fca-low/src/dto/base-identity.dto.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IsDefined } from 'class-validator';

export class GetIdentityProviderSelectionSessionDto {
import { UserSession } from '@fc/core/dto/user-session.dto';

export class GetIdentityProviderSelectionSessionDto extends UserSession {
@IsDefined()
readonly idpLoginHint: string;
declare idpLoginHint: string;
}
Loading
Loading