Skip to content

Commit

Permalink
[FEATURE] Enregistrer la date de dernière connexion Pix par applicati…
Browse files Browse the repository at this point in the history
…on pour les méthodes de connexion OIDC (PIX-16623)

 #11505
  • Loading branch information
pix-service-auto-merge authored Feb 27, 2025
2 parents 7b39c6a + 11b363c commit 6d950c3
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PIX_ADMIN } from '../../../authorization/domain/constants.js';
import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js';
import { usecases } from '../../domain/usecases/index.js';
import * as oidcProviderSerializer from '../../infrastructure/serializers/jsonapi/oidc-identity-providers.serializer.js';
import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
import { getForwardedOrigin, RequestedApplication } from '../../infrastructure/utils/network.js';

/**
* @param request
Expand Down Expand Up @@ -45,6 +45,7 @@ async function reconcileUserForAdmin(
) {
const { email, identityProvider, authenticationKey } = request.deserializedPayload;
const origin = getForwardedOrigin(request.headers);
const requestedApplication = RequestedApplication.fromOrigin(origin);

await dependencies.oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await dependencies.oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProvider);
Expand All @@ -60,6 +61,7 @@ async function reconcileUserForAdmin(
authenticationKey,
oidcAuthenticationService,
audience: origin,
requestedApplication,
});

return h.response({ access_token: accessToken }).code(200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { requestResponseUtils } from '../../../shared/infrastructure/utils/reque
import { usecases } from '../../domain/usecases/index.js';
import * as oidcProviderSerializer from '../../infrastructure/serializers/jsonapi/oidc-identity-providers.serializer.js';
import * as oidcSerializer from '../../infrastructure/serializers/jsonapi/oidc-serializer.js';
import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
import { getForwardedOrigin, RequestedApplication } from '../../infrastructure/utils/network.js';

/**
* @typedef {function} authenticateOidcUser
Expand All @@ -14,6 +14,7 @@ import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
async function authenticateOidcUser(request, h) {
const { code, state, iss, identityProvider: identityProviderCode, target } = request.deserializedPayload;
const origin = getForwardedOrigin(request.headers);
const requestedApplication = RequestedApplication.fromOrigin(origin);

const sessionState = request.yar.get('state', true);
const nonce = request.yar.get('nonce', true);
Expand All @@ -32,6 +33,7 @@ async function authenticateOidcUser(request, h) {
nonce,
sessionState,
audience: origin,
requestedApplication,
});

if (result.isAuthenticationComplete) {
Expand Down Expand Up @@ -62,13 +64,15 @@ async function createUser(request, h, dependencies = { requestResponseUtils }) {
const localeFromCookie = request.state?.locale;
const language = dependencies.requestResponseUtils.extractLocaleFromRequest(request);
const origin = getForwardedOrigin(request.headers);
const requestedApplication = RequestedApplication.fromOrigin(origin);

const { accessToken: access_token, logoutUrlUUID: logout_url_uuid } = await usecases.createOidcUser({
authenticationKey,
identityProvider,
localeFromCookie,
language,
audience: origin,
requestedApplication,
});

return h.response({ access_token, logout_url_uuid }).code(200);
Expand Down Expand Up @@ -152,11 +156,13 @@ async function reconcileUser(request, h) {
const { identityProvider, authenticationKey } = request.deserializedPayload;

const origin = getForwardedOrigin(request.headers);
const requestedApplication = RequestedApplication.fromOrigin(origin);

const result = await usecases.reconcileOidcUser({
authenticationKey,
identityProvider,
audience: origin,
requestedApplication,
});

return h.response({ access_token: result.accessToken, logout_url_uuid: result.logoutUrlUUID }).code(200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { ForbiddenAccess } from '../../../shared/domain/errors.js';
* @param {AuthenticationMethodRepository} params.authenticationMethodRepository
* @param {UserLoginRepository} params.userLoginRepository
* @param {UserRepository} params.userRepository
* @param {LastUserApplicationConnectionsRepository} params.LastUserApplicationConnectionsRepository,
* @param {RequestedApplication} params.RequestedApplication,
* @return {Promise<{isAuthenticationComplete: boolean, givenName: string, familyName: string, authenticationKey: string, email: string}|{isAuthenticationComplete: boolean, pixAccessToken: string, logoutUrlUUID: string}>}
*/
async function authenticateOidcUser({
Expand All @@ -34,6 +36,8 @@ async function authenticateOidcUser({
authenticationMethodRepository,
userLoginRepository,
userRepository,
lastUserApplicationConnectionsRepository,
requestedApplication,
}) {
await oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProviderCode);
Expand Down Expand Up @@ -86,6 +90,11 @@ async function authenticateOidcUser({
}

await userLoginRepository.updateLastLoggedAt({ userId: user.id });
await lastUserApplicationConnectionsRepository.upsert({
userId: user.id,
application: requestedApplication.applicationName,
lastLoggedAt: new Date(),
});

return { pixAccessToken, logoutUrlUUID, isAuthenticationComplete: true };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { UserToCreate } from '../models/UserToCreate.js';
* authenticationMethodRepository: AuthenticationMethodRepository,
* userToCreateRepository: UserToCreateRepository,
* userLoginRepository: UserLoginRepository,
* lastUserApplicationConnectionsRepository: LastUserApplicationConnectionsRepository,
* requestedApplication: RequestedApplication,
* }} params
* @return {Promise<{accessToken: string, logoutUrlUUID: string}>}
*/
Expand All @@ -27,6 +29,8 @@ async function createOidcUser({
authenticationMethodRepository,
userToCreateRepository,
userLoginRepository,
lastUserApplicationConnectionsRepository,
requestedApplication,
}) {
const sessionContentAndUserInfo = await authenticationSessionService.getByKey(authenticationKey);
if (!sessionContentAndUserInfo) {
Expand Down Expand Up @@ -77,6 +81,11 @@ async function createOidcUser({
}

await userLoginRepository.updateLastLoggedAt({ userId });
await lastUserApplicationConnectionsRepository.upsert({
userId,
application: requestedApplication.applicationName,
lastLoggedAt: new Date(),
});

return { accessToken, logoutUrlUUID };
}
Expand Down
2 changes: 2 additions & 0 deletions api/src/identity-access-management/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { emailValidationDemandRepository } from '../../infrastructure/repositori
import { eventLoggingJobRepository } from '../../infrastructure/repositories/jobs/event-logging-job.repository.js';
import { garAnonymizedBatchEventsLoggingJobRepository } from '../../infrastructure/repositories/jobs/gar-anonymized-batch-events-logging-job-repository.js';
import { userAnonymizedEventLoggingJobRepository } from '../../infrastructure/repositories/jobs/user-anonymized-event-logging-job-repository.js';
import { lastUserApplicationConnectionsRepository } from '../../infrastructure/repositories/last-user-application-connections.repository.js';
import { legalDocumentApiRepository } from '../../infrastructure/repositories/legal-document-api.repository.js';
import { oidcProviderRepository } from '../../infrastructure/repositories/oidc-provider-repository.js';
import * as privacyUsersApiRepository from '../../infrastructure/repositories/privacy-users-api.repository.js';
Expand Down Expand Up @@ -64,6 +65,7 @@ const repositories = {
emailValidationDemandRepository,
emailRepository,
eventLoggingJobRepository,
lastUserApplicationConnectionsRepository,
legalDocumentApiRepository,
membershipRepository,
oidcProviderRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { AuthenticationMethod } from '../models/AuthenticationMethod.js';
* @param {string} params.identityProvider
* @param {string} params.audience
* @param {OidcAuthenticationService} params.oidcAuthenticationService
* @param {RequestedApplication} params.requestedApplication
* @param {lastUserApplicationConnectionsRepository} params.lastUserApplicationConnectionsRepository
* @param {AuthenticationSessionService} params.authenticationSessionService
* @param {AuthenticationMethodRepository} params.authenticationMethodRepository
* @param {UserRepository} params.userRepository
Expand All @@ -30,6 +32,8 @@ export const reconcileOidcUserForAdmin = async function ({
authenticationMethodRepository,
userRepository,
userLoginRepository,
lastUserApplicationConnectionsRepository,
requestedApplication,
audience,
}) {
const sessionContentAndUserInfo = await authenticationSessionService.getByKey(authenticationKey);
Expand Down Expand Up @@ -61,7 +65,13 @@ export const reconcileOidcUserForAdmin = async function ({
});

const accessToken = await oidcAuthenticationService.createAccessToken({ userId, audience });

await userLoginRepository.updateLastLoggedAt({ userId });
await lastUserApplicationConnectionsRepository.upsert({
userId,
application: requestedApplication.applicationName,
lastLoggedAt: new Date(),
});

return accessToken;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { AuthenticationMethod } from '../models/AuthenticationMethod.js';
* @param {AuthenticationMethodRepository} params.authenticationMethodRepository
* @param {OidcAuthenticationServiceRegistry} params.oidcAuthenticationServiceRegistry
* @param {UserLoginRepository} params.userLoginRepository
* @param {LastUserApplicationConnectionsRepository} params.lastUserApplicationConnectionsRepository
* @param {RequestedApplication} params.requestedApplication
* @return {Promise<{accessToken: string, logoutUrlUUID: string}|AuthenticationKeyExpired|MissingUserAccountError>}
*/
export const reconcileOidcUser = async function ({
Expand All @@ -20,7 +22,9 @@ export const reconcileOidcUser = async function ({
authenticationMethodRepository,
oidcAuthenticationServiceRegistry,
userLoginRepository,
lastUserApplicationConnectionsRepository,
audience,
requestedApplication,
}) {
await oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProvider);
Expand Down Expand Up @@ -65,6 +69,11 @@ export const reconcileOidcUser = async function ({
}

await userLoginRepository.updateLastLoggedAt({ userId });
await lastUserApplicationConnectionsRepository.upsert({
userId,
application: requestedApplication.applicationName,
lastLoggedAt: new Date(),
});

return { accessToken, logoutUrlUUID };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { knex } from '../../../../db/knex-database-connection.js';
const TABLE_NAME = 'last-user-application-connections';

async function upsert({ userId, application, lastLoggedAt }) {
const existingConnection = await knex(TABLE_NAME).where({ userId, application }).first();

if (existingConnection) {
return knex(TABLE_NAME).where({ userId, application }).update({ lastLoggedAt });
}

return knex(TABLE_NAME).insert({ userId, application, lastLoggedAt });
}

export const lastUserApplicationConnectionsRepository = { upsert };
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,14 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
lastName: 'Doe',
},
};
const headers = generateAuthenticatedUserRequestHeaders();
headers.cookie = cookies[0];

// when
const response = await server.inject({
method: 'POST',
url: '/api/oidc/token',
headers: { cookie: cookies[0] },
headers,
payload,
});

Expand Down Expand Up @@ -331,11 +333,14 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
// const getAccessTokenRequest = nock(settings.poleEmploi.tokenUrl).post('/').reply(200, getAccessTokenResponse);
oidcExampleNetProvider.client.callback.resolves(getAccessTokenResponse);

const headers = generateAuthenticatedUserRequestHeaders();
headers.cookie = cookies[0];

// when
const response = await server.inject({
method: 'POST',
url: '/api/oidc/token',
headers: { cookie: cookies[0] },
headers,
payload,
});

Expand Down Expand Up @@ -505,6 +510,7 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
const request = {
method: 'POST',
url: '/api/oidc/users',
headers: generateAuthenticatedUserRequestHeaders(),
payload: {
data: {
attributes: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { lastUserApplicationConnectionsRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/last-user-application-connections.repository.js';
import { databaseBuilder, expect } from '../../../../test-helper.js';

describe('Integration | Identity Access Management | Infrastructure | Repository | last-user-application-connections', function () {
describe('#upsert', function () {
it('saves a last user application connection', async function () {
// given
const userId = databaseBuilder.factory.buildUser().id;
const application = 'orga';
const lastLoggedAt = new Date();
await databaseBuilder.commit();

// when
await lastUserApplicationConnectionsRepository.upsert({
userId,
application,
lastLoggedAt,
});

// then
const lastUserApplicationConnections = await databaseBuilder
.knex('last-user-application-connections')
.where({ userId, application })
.first();

expect(lastUserApplicationConnections).to.deep.equal({
id: lastUserApplicationConnections.id,
userId,
application,
lastLoggedAt,
});
});

context('when the last user application connection already exists', function () {
it('updates the last user application connection', async function () {
// given
const userId = databaseBuilder.factory.buildUser().id;
const application = 'orga';
const formerLastLoggedAt = new Date('2021-01-01');
const newLastLoggedAt = new Date('2021-01-02');
await databaseBuilder.commit();

await databaseBuilder.knex('last-user-application-connections').insert({
userId,
application,
lastLoggedAt: formerLastLoggedAt,
});

// when
await lastUserApplicationConnectionsRepository.upsert({
userId,
application,
lastLoggedAt: newLastLoggedAt,
});

// then
const lastUserApplicationConnections = await databaseBuilder
.knex('last-user-application-connections')
.where({ userId, application });

expect(lastUserApplicationConnections).to.have.lengthOf(1);
expect(lastUserApplicationConnections).to.deep.equal([
{
id: lastUserApplicationConnections[0].id,
userId,
application,
lastLoggedAt: newLastLoggedAt,
},
]);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { oidcProviderAdminController } from '../../../../src/identity-access-management/application/oidc-provider/oidc-provider.admin.controller.js';
import { usecases } from '../../../../src/identity-access-management/domain/usecases/index.js';
import { RequestedApplication } from '../../../../src/identity-access-management/infrastructure/utils/network.js';
import { DomainTransaction } from '../../../../src/shared/domain/DomainTransaction.js';
import { expect, hFake, sinon } from '../../../test-helper.js';

Expand Down Expand Up @@ -180,11 +181,22 @@ describe('Unit | Identity Access Management | Application | Controller | Admin |
email: '[email protected]',
},
};
const requestedApplication = new RequestedApplication('admin');

const dependencies = {
oidcAuthenticationServiceRegistry: oidcAuthenticationServiceRegistryStub,
};
sinon.stub(usecases, 'reconcileOidcUserForAdmin').resolves('accessToken');
sinon
.stub(usecases, 'reconcileOidcUserForAdmin')
.withArgs({
email: '[email protected]',
identityProvider,
authenticationKey: '123abc',
audience: 'https://admin.pix.fr',
requestedApplication,
oidcAuthenticationService: sinon.match.any,
})
.resolves('accessToken');

// when
const result = await oidcProviderAdminController.reconcileUserForAdmin(request, hFake, dependencies);
Expand Down
Loading

0 comments on commit 6d950c3

Please sign in to comment.