Skip to content

Commit a0b9c97

Browse files
[FEATURE] Enregistrer la date de dernière connexion Pix pour les méthodes de connexion GAR (PIX-16624)
#11552
2 parents 0b4ec3a + 48ce320 commit a0b9c97

File tree

9 files changed

+181
-30
lines changed

9 files changed

+181
-30
lines changed

api/lib/application/sco-organization-learners/sco-organization-learner-controller.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import dayjs from 'dayjs';
22

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

7073
const origin = getForwardedOrigin(request.headers);
74+
const requestedApplication = RequestedApplication.fromOrigin(origin);
7175
const accessToken = await usecases.createUserAndReconcileToOrganizationLearnerFromExternalUser({
7276
birthdate,
7377
campaignCode,
7478
token,
7579
audience: origin,
80+
requestedApplication,
7681
});
7782

7883
const scoOrganizationLearner = {

api/lib/domain/usecases/create-user-and-reconcile-to-organization-learner-from-external-user.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const createUserAndReconcileToOrganizationLearnerFromExternalUser = async functi
1717
obfuscationService,
1818
tokenService,
1919
audience,
20+
requestedApplication,
2021
userReconciliationService,
2122
userService,
2223
authenticationMethodRepository,
@@ -26,6 +27,7 @@ const createUserAndReconcileToOrganizationLearnerFromExternalUser = async functi
2627
userToCreateRepository,
2728
organizationLearnerRepository,
2829
prescriptionOrganizationLearnerRepository,
30+
lastUserApplicationConnectionsRepository,
2931
studentRepository,
3032
}) {
3133
const campaign = await campaignRepository.getByCode(campaignCode);
@@ -115,9 +117,37 @@ const createUserAndReconcileToOrganizationLearnerFromExternalUser = async functi
115117
}
116118
}
117119
const tokenUserId = userWithSamlId ? userWithSamlId.id : userId;
120+
121+
await _updateUserLastConnection({
122+
userId: tokenUserId,
123+
requestedApplication,
124+
authenticationMethodRepository,
125+
lastUserApplicationConnectionsRepository,
126+
userLoginRepository,
127+
});
128+
118129
const accessToken = tokenService.createAccessTokenForSaml({ userId: tokenUserId, audience });
119-
await userLoginRepository.updateLastLoggedAt({ userId: tokenUserId });
130+
120131
return accessToken;
121132
};
122133

123134
export { createUserAndReconcileToOrganizationLearnerFromExternalUser };
135+
136+
async function _updateUserLastConnection({
137+
userId,
138+
requestedApplication,
139+
authenticationMethodRepository,
140+
lastUserApplicationConnectionsRepository,
141+
userLoginRepository,
142+
}) {
143+
await userLoginRepository.updateLastLoggedAt({ userId });
144+
await lastUserApplicationConnectionsRepository.upsert({
145+
userId,
146+
application: requestedApplication.applicationName,
147+
lastLoggedAt: new Date(),
148+
});
149+
await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
150+
userId,
151+
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.GAR.code,
152+
});
153+
}

api/lib/domain/usecases/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const oidcAuthenticationServiceRegistry = new OidcAuthenticationServiceRegistry(
103103
* @typedef {userRepository} UserRepository
104104
* @typedef {certificationChallengesService} CertificationChallengesService
105105
* @typedef {assessmentRepository} AssessmentRepository
106+
* @typedef {LastUserApplicationConnectionsRepository} LastUserApplicationConnectionsRepository
106107
*/
107108
const dependencies = {
108109
accountRecoveryDemandRepository,

api/src/identity-access-management/application/saml/saml.controller.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tokenService } from '../../../shared/domain/services/token-service.js';
33
import { logger } from '../../../shared/infrastructure/utils/logger.js';
44
import { usecases } from '../../domain/usecases/index.js';
55
import * as saml from '../../infrastructure/saml.js';
6-
import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
6+
import { getForwardedOrigin, RequestedApplication } from '../../infrastructure/utils/network.js';
77

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

2525
try {
2626
const origin = getForwardedOrigin(request.headers);
27+
const requestedApplication = RequestedApplication.fromOrigin(origin);
2728
const redirectionUrl = await usecases.getSamlAuthenticationRedirectionUrl({
2829
userAttributes,
2930
tokenService,
3031
config,
3132
audience: origin,
33+
requestedApplication,
3234
});
3335

3436
return h.redirect(redirectionUrl);

api/src/identity-access-management/domain/usecases/get-saml-authentication-redirection-url.js

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ const getSamlAuthenticationRedirectionUrl = async function ({
66
userRepository,
77
userLoginRepository,
88
authenticationMethodRepository,
9+
lastUserApplicationConnectionsRepository,
910
tokenService,
1011
config,
1112
audience,
13+
requestedApplication,
1214
}) {
1315
const { attributeMapping } = config.saml;
1416
const externalUser = {
@@ -18,15 +20,30 @@ const getSamlAuthenticationRedirectionUrl = async function ({
1820
};
1921

2022
const user = await userRepository.getBySamlId(externalUser.samlId);
21-
2223
if (user) {
23-
return await _getUrlWithAccessToken({
24+
await _updateUserLastConnection({
25+
user,
26+
requestedApplication,
27+
authenticationMethodRepository,
28+
lastUserApplicationConnectionsRepository,
29+
userLoginRepository,
30+
});
31+
32+
await _saveUserFirstAndLastName({
33+
authenticationMethodRepository,
34+
user,
35+
externalUser,
36+
});
37+
38+
return _getUrlWithAccessToken({
2439
user,
2540
audience,
2641
externalUser,
2742
tokenService,
2843
userLoginRepository,
2944
authenticationMethodRepository,
45+
lastUserApplicationConnectionsRepository,
46+
requestedApplication,
3047
});
3148
}
3249

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

3653
export { getSamlAuthenticationRedirectionUrl };
3754

38-
async function _getUrlWithAccessToken({
39-
user,
40-
audience,
41-
externalUser,
42-
tokenService,
43-
userLoginRepository,
44-
authenticationMethodRepository,
45-
}) {
55+
async function _getUrlWithAccessToken({ user, audience, tokenService }) {
4656
const token = tokenService.createAccessTokenForSaml({ userId: user.id, audience });
47-
await userLoginRepository.updateLastLoggedAt({ userId: user.id });
48-
await _saveUserFirstAndLastName({ authenticationMethodRepository, user, externalUser });
57+
4958
return `/connexion/gar#${encodeURIComponent(token)}`;
5059
}
5160

61+
function _externalUserFirstAndLastNameMatchesAuthenticationMethodFirstAndLastName({
62+
authenticationMethod,
63+
externalUser,
64+
}) {
65+
return (
66+
externalUser.firstName === authenticationMethod.authenticationComplement?.firstName &&
67+
externalUser.lastName === authenticationMethod.authenticationComplement?.lastName
68+
);
69+
}
70+
71+
function _getUrlForReconciliationPage({ tokenService, externalUser }) {
72+
const externalUserToken = tokenService.createIdTokenForUserReconciliation(externalUser);
73+
return `/campagnes?externalUser=${encodeURIComponent(externalUserToken)}`;
74+
}
75+
5276
async function _saveUserFirstAndLastName({ authenticationMethodRepository, user, externalUser }) {
5377
const authenticationMethod = await authenticationMethodRepository.findOneByUserIdAndIdentityProvider({
5478
userId: user.id,
@@ -69,17 +93,21 @@ async function _saveUserFirstAndLastName({ authenticationMethodRepository, user,
6993
authenticationMethodRepository.update(authenticationMethod);
7094
}
7195

72-
function _externalUserFirstAndLastNameMatchesAuthenticationMethodFirstAndLastName({
73-
authenticationMethod,
74-
externalUser,
96+
async function _updateUserLastConnection({
97+
user,
98+
requestedApplication,
99+
authenticationMethodRepository,
100+
lastUserApplicationConnectionsRepository,
101+
userLoginRepository,
75102
}) {
76-
return (
77-
externalUser.firstName === authenticationMethod.authenticationComplement?.firstName &&
78-
externalUser.lastName === authenticationMethod.authenticationComplement?.lastName
79-
);
80-
}
81-
82-
function _getUrlForReconciliationPage({ tokenService, externalUser }) {
83-
const externalUserToken = tokenService.createIdTokenForUserReconciliation(externalUser);
84-
return `/campagnes?externalUser=${encodeURIComponent(externalUserToken)}`;
103+
await userLoginRepository.updateLastLoggedAt({ userId: user.id });
104+
await lastUserApplicationConnectionsRepository.upsert({
105+
userId: user.id,
106+
application: requestedApplication.applicationName,
107+
lastLoggedAt: new Date(),
108+
});
109+
await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
110+
userId: user.id,
111+
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.GAR.code,
112+
});
85113
}

api/tests/acceptance/application/sco-organization-learners/sco-organization-learner-controller_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('Acceptance | Controller | sco-organization-learners', function () {
152152
options = {
153153
method: 'POST',
154154
url: '/api/sco-organization-learners/external',
155-
headers: {},
155+
headers: generateAuthenticatedUserRequestHeaders(),
156156
payload: {},
157157
};
158158

api/tests/identity-access-management/unit/domain/usecases/get-saml-authentication-redirection-url_test.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import { NON_OIDC_IDENTITY_PROVIDERS } from '../../../../../src/identity-access-
22
import { AuthenticationMethod } from '../../../../../src/identity-access-management/domain/models/AuthenticationMethod.js';
33
import { User } from '../../../../../src/identity-access-management/domain/models/User.js';
44
import { getSamlAuthenticationRedirectionUrl } from '../../../../../src/identity-access-management/domain/usecases/get-saml-authentication-redirection-url.js';
5+
import { RequestedApplication } from '../../../../../src/identity-access-management/infrastructure/utils/network.js';
56
import { domainBuilder, expect, sinon } from '../../../../test-helper.js';
67

78
describe('Unit | UseCase | get-external-authentication-redirection-url', function () {
89
let userRepository;
910
let userLoginRepository;
1011
let authenticationMethodRepository;
12+
let lastUserApplicationConnectionsRepository;
1113
let tokenService;
1214
let samlSettings;
1315
const audience = 'https://app.pix.fr';
16+
const requestedApplication = new RequestedApplication('app');
1417

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

2427
authenticationMethodRepository = {
2528
findOneByUserIdAndIdentityProvider: sinon.stub(),
29+
updateLastLoggedAtByIdentityProvider: sinon.stub(),
2630
update: sinon.stub(),
2731
};
2832

@@ -40,6 +44,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
4044
},
4145
},
4246
};
47+
48+
lastUserApplicationConnectionsRepository = {
49+
upsert: sinon.stub(),
50+
};
4351
});
4452

4553
context('when user does not exist in database yet', function () {
@@ -59,6 +67,7 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
5967
userAttributes,
6068
userRepository,
6169
userLoginRepository,
70+
lastUserApplicationConnectionsRepository,
6271
tokenService,
6372
config: samlSettings,
6473
});
@@ -107,16 +116,18 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
107116
userRepository,
108117
userLoginRepository,
109118
authenticationMethodRepository,
119+
lastUserApplicationConnectionsRepository,
110120
tokenService,
111121
config: samlSettings,
122+
requestedApplication,
112123
});
113124

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

119-
it('should save the last login date', async function () {
130+
it('should save the last login dates', async function () {
120131
// given
121132
const userAttributes = {
122133
IDO: 'saml-id-for-adele',
@@ -137,12 +148,23 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
137148
userRepository,
138149
userLoginRepository,
139150
authenticationMethodRepository,
151+
lastUserApplicationConnectionsRepository,
140152
tokenService,
141153
config: samlSettings,
154+
requestedApplication,
142155
});
143156

144157
// then
145158
expect(userLoginRepository.updateLastLoggedAt).to.have.been.calledWithExactly({ userId: 777 });
159+
expect(lastUserApplicationConnectionsRepository.upsert).to.have.been.calledWithExactly({
160+
userId: 777,
161+
application: 'app',
162+
lastLoggedAt: sinon.match.instanceOf(Date),
163+
});
164+
expect(authenticationMethodRepository.updateLastLoggedAtByIdentityProvider).to.have.been.calledWithExactly({
165+
userId: 777,
166+
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.GAR.code,
167+
});
146168
});
147169

148170
context("when user's authentication method does not contain first and last name", function () {
@@ -170,8 +192,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
170192
userRepository,
171193
userLoginRepository,
172194
authenticationMethodRepository,
195+
lastUserApplicationConnectionsRepository,
173196
tokenService,
174197
config: samlSettings,
198+
requestedApplication,
175199
});
176200

177201
// then
@@ -207,8 +231,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
207231
userRepository,
208232
userLoginRepository,
209233
authenticationMethodRepository,
234+
lastUserApplicationConnectionsRepository,
210235
tokenService,
211236
config: samlSettings,
237+
requestedApplication,
212238
});
213239

214240
// then
@@ -243,8 +269,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
243269
userRepository,
244270
userLoginRepository,
245271
authenticationMethodRepository,
272+
lastUserApplicationConnectionsRepository,
246273
tokenService,
247274
config: samlSettings,
275+
requestedApplication,
248276
});
249277

250278
// then
@@ -279,8 +307,10 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
279307
userRepository,
280308
userLoginRepository,
281309
authenticationMethodRepository,
310+
lastUserApplicationConnectionsRepository,
282311
tokenService,
283312
config: samlSettings,
313+
requestedApplication,
284314
});
285315

286316
// then

0 commit comments

Comments
 (0)