Skip to content

Commit

Permalink
[FEATURE] Enregistrer la date de dernière connexion Pix pour les méth…
Browse files Browse the repository at this point in the history
…odes de connexion GAR (PIX-16624)

 #11552
  • Loading branch information
pix-service-auto-merge authored Mar 5, 2025
2 parents 0b4ec3a + 48ce320 commit a0b9c97
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import dayjs from 'dayjs';

import * as studentInformationForAccountRecoverySerializer from '../../../src/identity-access-management/infrastructure/serializers/jsonapi/student-information-for-account-recovery-serializer.js';
import { getForwardedOrigin } from '../../../src/identity-access-management/infrastructure/utils/network.js';
import {
getForwardedOrigin,
RequestedApplication,
} from '../../../src/identity-access-management/infrastructure/utils/network.js';
import * as scoOrganizationLearnerSerializer from '../../../src/prescription/learner-management/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js';
import { DomainTransaction } from '../../../src/shared/domain/DomainTransaction.js';
import * as requestResponseUtils from '../../../src/shared/infrastructure/utils/request-response-utils.js';
Expand Down Expand Up @@ -68,11 +71,13 @@ const createUserAndReconcileToOrganizationLearnerFromExternalUser = async functi
const { birthdate, 'campaign-code': campaignCode, 'external-user-token': token } = request.payload.data.attributes;

const origin = getForwardedOrigin(request.headers);
const requestedApplication = RequestedApplication.fromOrigin(origin);
const accessToken = await usecases.createUserAndReconcileToOrganizationLearnerFromExternalUser({
birthdate,
campaignCode,
token,
audience: origin,
requestedApplication,
});

const scoOrganizationLearner = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const createUserAndReconcileToOrganizationLearnerFromExternalUser = async functi
obfuscationService,
tokenService,
audience,
requestedApplication,
userReconciliationService,
userService,
authenticationMethodRepository,
Expand All @@ -26,6 +27,7 @@ const createUserAndReconcileToOrganizationLearnerFromExternalUser = async functi
userToCreateRepository,
organizationLearnerRepository,
prescriptionOrganizationLearnerRepository,
lastUserApplicationConnectionsRepository,
studentRepository,
}) {
const campaign = await campaignRepository.getByCode(campaignCode);
Expand Down Expand Up @@ -115,9 +117,37 @@ const createUserAndReconcileToOrganizationLearnerFromExternalUser = async functi
}
}
const tokenUserId = userWithSamlId ? userWithSamlId.id : userId;

await _updateUserLastConnection({
userId: tokenUserId,
requestedApplication,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
userLoginRepository,
});

const accessToken = tokenService.createAccessTokenForSaml({ userId: tokenUserId, audience });
await userLoginRepository.updateLastLoggedAt({ userId: tokenUserId });

return accessToken;
};

export { createUserAndReconcileToOrganizationLearnerFromExternalUser };

async function _updateUserLastConnection({
userId,
requestedApplication,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
userLoginRepository,
}) {
await userLoginRepository.updateLastLoggedAt({ userId });
await lastUserApplicationConnectionsRepository.upsert({
userId,
application: requestedApplication.applicationName,
lastLoggedAt: new Date(),
});
await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
userId,
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.GAR.code,
});
}
1 change: 1 addition & 0 deletions api/lib/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const oidcAuthenticationServiceRegistry = new OidcAuthenticationServiceRegistry(
* @typedef {userRepository} UserRepository
* @typedef {certificationChallengesService} CertificationChallengesService
* @typedef {assessmentRepository} AssessmentRepository
* @typedef {LastUserApplicationConnectionsRepository} LastUserApplicationConnectionsRepository
*/
const dependencies = {
accountRecoveryDemandRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { tokenService } from '../../../shared/domain/services/token-service.js';
import { logger } from '../../../shared/infrastructure/utils/logger.js';
import { usecases } from '../../domain/usecases/index.js';
import * as saml from '../../infrastructure/saml.js';
import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
import { getForwardedOrigin, RequestedApplication } from '../../infrastructure/utils/network.js';

const metadata = function (request, h) {
return h.response(saml.getServiceProviderMetadata()).type('application/xml');
Expand All @@ -24,11 +24,13 @@ const assert = async function (request, h) {

try {
const origin = getForwardedOrigin(request.headers);
const requestedApplication = RequestedApplication.fromOrigin(origin);
const redirectionUrl = await usecases.getSamlAuthenticationRedirectionUrl({
userAttributes,
tokenService,
config,
audience: origin,
requestedApplication,
});

return h.redirect(redirectionUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ const getSamlAuthenticationRedirectionUrl = async function ({
userRepository,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config,
audience,
requestedApplication,
}) {
const { attributeMapping } = config.saml;
const externalUser = {
Expand All @@ -18,15 +20,30 @@ const getSamlAuthenticationRedirectionUrl = async function ({
};

const user = await userRepository.getBySamlId(externalUser.samlId);

if (user) {
return await _getUrlWithAccessToken({
await _updateUserLastConnection({
user,
requestedApplication,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
userLoginRepository,
});

await _saveUserFirstAndLastName({
authenticationMethodRepository,
user,
externalUser,
});

return _getUrlWithAccessToken({
user,
audience,
externalUser,
tokenService,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
requestedApplication,
});
}

Expand All @@ -35,20 +52,27 @@ const getSamlAuthenticationRedirectionUrl = async function ({

export { getSamlAuthenticationRedirectionUrl };

async function _getUrlWithAccessToken({
user,
audience,
externalUser,
tokenService,
userLoginRepository,
authenticationMethodRepository,
}) {
async function _getUrlWithAccessToken({ user, audience, tokenService }) {
const token = tokenService.createAccessTokenForSaml({ userId: user.id, audience });
await userLoginRepository.updateLastLoggedAt({ userId: user.id });
await _saveUserFirstAndLastName({ authenticationMethodRepository, user, externalUser });

return `/connexion/gar#${encodeURIComponent(token)}`;
}

function _externalUserFirstAndLastNameMatchesAuthenticationMethodFirstAndLastName({
authenticationMethod,
externalUser,
}) {
return (
externalUser.firstName === authenticationMethod.authenticationComplement?.firstName &&
externalUser.lastName === authenticationMethod.authenticationComplement?.lastName
);
}

function _getUrlForReconciliationPage({ tokenService, externalUser }) {
const externalUserToken = tokenService.createIdTokenForUserReconciliation(externalUser);
return `/campagnes?externalUser=${encodeURIComponent(externalUserToken)}`;
}

async function _saveUserFirstAndLastName({ authenticationMethodRepository, user, externalUser }) {
const authenticationMethod = await authenticationMethodRepository.findOneByUserIdAndIdentityProvider({
userId: user.id,
Expand All @@ -69,17 +93,21 @@ async function _saveUserFirstAndLastName({ authenticationMethodRepository, user,
authenticationMethodRepository.update(authenticationMethod);
}

function _externalUserFirstAndLastNameMatchesAuthenticationMethodFirstAndLastName({
authenticationMethod,
externalUser,
async function _updateUserLastConnection({
user,
requestedApplication,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
userLoginRepository,
}) {
return (
externalUser.firstName === authenticationMethod.authenticationComplement?.firstName &&
externalUser.lastName === authenticationMethod.authenticationComplement?.lastName
);
}

function _getUrlForReconciliationPage({ tokenService, externalUser }) {
const externalUserToken = tokenService.createIdTokenForUserReconciliation(externalUser);
return `/campagnes?externalUser=${encodeURIComponent(externalUserToken)}`;
await userLoginRepository.updateLastLoggedAt({ userId: user.id });
await lastUserApplicationConnectionsRepository.upsert({
userId: user.id,
application: requestedApplication.applicationName,
lastLoggedAt: new Date(),
});
await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
userId: user.id,
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.GAR.code,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ describe('Acceptance | Controller | sco-organization-learners', function () {
options = {
method: 'POST',
url: '/api/sco-organization-learners/external',
headers: {},
headers: generateAuthenticatedUserRequestHeaders(),
payload: {},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { NON_OIDC_IDENTITY_PROVIDERS } from '../../../../../src/identity-access-
import { AuthenticationMethod } from '../../../../../src/identity-access-management/domain/models/AuthenticationMethod.js';
import { User } from '../../../../../src/identity-access-management/domain/models/User.js';
import { getSamlAuthenticationRedirectionUrl } from '../../../../../src/identity-access-management/domain/usecases/get-saml-authentication-redirection-url.js';
import { RequestedApplication } from '../../../../../src/identity-access-management/infrastructure/utils/network.js';
import { domainBuilder, expect, sinon } from '../../../../test-helper.js';

describe('Unit | UseCase | get-external-authentication-redirection-url', function () {
let userRepository;
let userLoginRepository;
let authenticationMethodRepository;
let lastUserApplicationConnectionsRepository;
let tokenService;
let samlSettings;
const audience = 'https://app.pix.fr';
const requestedApplication = new RequestedApplication('app');

beforeEach(function () {
userRepository = {
Expand All @@ -23,6 +26,7 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio

authenticationMethodRepository = {
findOneByUserIdAndIdentityProvider: sinon.stub(),
updateLastLoggedAtByIdentityProvider: sinon.stub(),
update: sinon.stub(),
};

Expand All @@ -40,6 +44,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
},
},
};

lastUserApplicationConnectionsRepository = {
upsert: sinon.stub(),
};
});

context('when user does not exist in database yet', function () {
Expand All @@ -59,6 +67,7 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
userAttributes,
userRepository,
userLoginRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config: samlSettings,
});
Expand Down Expand Up @@ -107,16 +116,18 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
userRepository,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config: samlSettings,
requestedApplication,
});

// then
const expectedUrl = '/connexion/gar#access-token';
expect(result).to.deep.equal(expectedUrl);
});

it('should save the last login date', async function () {
it('should save the last login dates', async function () {
// given
const userAttributes = {
IDO: 'saml-id-for-adele',
Expand All @@ -137,12 +148,23 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
userRepository,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config: samlSettings,
requestedApplication,
});

// then
expect(userLoginRepository.updateLastLoggedAt).to.have.been.calledWithExactly({ userId: 777 });
expect(lastUserApplicationConnectionsRepository.upsert).to.have.been.calledWithExactly({
userId: 777,
application: 'app',
lastLoggedAt: sinon.match.instanceOf(Date),
});
expect(authenticationMethodRepository.updateLastLoggedAtByIdentityProvider).to.have.been.calledWithExactly({
userId: 777,
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.GAR.code,
});
});

context("when user's authentication method does not contain first and last name", function () {
Expand Down Expand Up @@ -170,8 +192,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
userRepository,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config: samlSettings,
requestedApplication,
});

// then
Expand Down Expand Up @@ -207,8 +231,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
userRepository,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config: samlSettings,
requestedApplication,
});

// then
Expand Down Expand Up @@ -243,8 +269,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
userRepository,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config: samlSettings,
requestedApplication,
});

// then
Expand Down Expand Up @@ -279,8 +307,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
userRepository,
userLoginRepository,
authenticationMethodRepository,
lastUserApplicationConnectionsRepository,
tokenService,
config: samlSettings,
requestedApplication,
});

// then
Expand Down
Loading

0 comments on commit a0b9c97

Please sign in to comment.