Skip to content

Commit 43ed802

Browse files
[FEATURE] Enregistrer dans Authentication Methods la date de dernière connexion Pix pour les méthodes de connexion Pix (PIX-16620)
#11514
2 parents f940bfe + c36eb0c commit 43ed802

File tree

12 files changed

+117
-7
lines changed

12 files changed

+117
-7
lines changed

api/db/database-builder/factory/build-authentication-method.js

+5
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ buildAuthenticationMethod.withPoleEmploiAsIdentityProvider = function ({
108108
userId,
109109
createdAt = new Date('2020-01-01'),
110110
updatedAt = new Date('2020-01-02'),
111+
lastLoggedAt = null,
111112
} = {}) {
112113
userId = isUndefined(userId) ? buildUser().id : userId;
113114

@@ -127,6 +128,7 @@ buildAuthenticationMethod.withPoleEmploiAsIdentityProvider = function ({
127128
userId,
128129
createdAt,
129130
updatedAt,
131+
lastLoggedAt,
130132
};
131133
return databaseBuffer.pushInsertable({
132134
tableName: 'authentication-methods',
@@ -157,6 +159,7 @@ buildAuthenticationMethod.withOidcProviderAsIdentityProvider = function ({
157159
}),
158160
createdAt: new Date('2020-01-01'),
159161
updatedAt: new Date('2020-01-02'),
162+
lastLoggedAt: null,
160163
};
161164
return databaseBuffer.pushInsertable({
162165
tableName: 'authentication-methods',
@@ -171,6 +174,7 @@ buildAuthenticationMethod.withIdentityProvider = function ({
171174
userId,
172175
createdAt = new Date('2020-01-01'),
173176
updatedAt = new Date('2020-01-02'),
177+
lastLoggedAt = null,
174178
} = {}) {
175179
userId = isUndefined(userId) ? buildUser().id : userId;
176180

@@ -185,6 +189,7 @@ buildAuthenticationMethod.withIdentityProvider = function ({
185189
userId,
186190
createdAt,
187191
updatedAt,
192+
lastLoggedAt,
188193
};
189194
return databaseBuffer.pushInsertable({
190195
tableName: 'authentication-methods',

api/lib/domain/usecases/authenticate-external-user.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ async function authenticateExternalUser({
5757
const token = tokenService.createAccessTokenForSaml({ userId: userFromCredentials.id, audience });
5858

5959
await userLoginRepository.updateLastLoggedAt({ userId: userFromCredentials.id });
60-
60+
await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
61+
userId: userFromCredentials.id,
62+
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.PIX.code,
63+
});
6164
return token;
6265
} catch (error) {
6366
if (error instanceof UserNotFoundError || error instanceof PasswordNotMatching) {

api/src/identity-access-management/domain/models/AuthenticationMethod.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const validationSchema = Joi.object({
8080
userId: Joi.number().integer().required(),
8181
createdAt: Joi.date().optional(),
8282
updatedAt: Joi.date().optional(),
83+
lastLoggedAt: Joi.date().allow(null).optional(),
8384
});
8485

8586
class AuthenticationMethod {
@@ -91,6 +92,7 @@ class AuthenticationMethod {
9192
createdAt,
9293
updatedAt,
9394
userId,
95+
lastLoggedAt,
9496
} = {}) {
9597
this.id = id;
9698
this.identityProvider = identityProvider;
@@ -99,11 +101,20 @@ class AuthenticationMethod {
99101
this.createdAt = createdAt;
100102
this.updatedAt = updatedAt;
101103
this.userId = userId;
104+
this.lastLoggedAt = lastLoggedAt;
102105

103106
validateEntity(validationSchema, this);
104107
}
105108

106-
static buildPixAuthenticationMethod({ id, password, shouldChangePassword = false, createdAt, updatedAt, userId }) {
109+
static buildPixAuthenticationMethod({
110+
id,
111+
password,
112+
shouldChangePassword = false,
113+
createdAt,
114+
updatedAt,
115+
userId,
116+
lastLoggedAt,
117+
}) {
107118
const authenticationComplement = new PixAuthenticationComplement({ password, shouldChangePassword });
108119
return new AuthenticationMethod({
109120
id,
@@ -113,6 +124,7 @@ class AuthenticationMethod {
113124
createdAt,
114125
updatedAt,
115126
userId,
127+
lastLoggedAt,
116128
});
117129
}
118130
}

api/src/identity-access-management/domain/usecases/authenticate-user.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PIX_ADMIN, PIX_ORGA } from '../../../authorization/domain/constants.js';
22
import { ForbiddenAccess, UserNotFoundError } from '../../../shared/domain/errors.js';
3+
import { NON_OIDC_IDENTITY_PROVIDERS } from '../constants/identity-providers.js';
34
import { createWarningConnectionEmail } from '../emails/create-warning-connection.email.js';
45
import { MissingOrInvalidCredentialsError, PasswordNotMatching, UserShouldChangePasswordError } from '../errors.js';
56
import { RefreshToken } from '../models/RefreshToken.js';
@@ -15,6 +16,7 @@ const authenticateUser = async function ({
1516
tokenService,
1617
userRepository,
1718
userLoginRepository,
19+
authenticationMethodRepository,
1820
adminMemberRepository,
1921
emailRepository,
2022
emailValidationDemandRepository,
@@ -47,7 +49,6 @@ const authenticateUser = async function ({
4749
if (foundUser.hasBeenModified) {
4850
await userRepository.update({ id: foundUser.id, locale: foundUser.locale });
4951
}
50-
5152
const userLogin = await userLoginRepository.findByUserId(foundUser.id);
5253
if (foundUser.email && userLogin?.shouldSendConnectionWarning()) {
5354
const validationToken = !foundUser.emailConfirmedAt
@@ -64,6 +65,11 @@ const authenticateUser = async function ({
6465
}
6566
await userLoginRepository.updateLastLoggedAt({ userId: foundUser.id });
6667

68+
await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
69+
userId: foundUser.id,
70+
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.PIX.code,
71+
});
72+
6773
return { accessToken, refreshToken: refreshToken.value, expirationDelaySeconds };
6874
} catch (error) {
6975
if (error instanceof UserNotFoundError || error instanceof PasswordNotMatching) {

api/src/identity-access-management/infrastructure/repositories/authentication-method.repository.js

+12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const COLUMNS = Object.freeze([
1717
'userId',
1818
'createdAt',
1919
'updatedAt',
20+
'lastLoggedAt',
2021
]);
2122

2223
const create = async function ({ authenticationMethod }) {
@@ -123,6 +124,16 @@ const hasIdentityProviderPIX = async function ({ userId }) {
123124
return Boolean(authenticationMethodDTO);
124125
};
125126

127+
const updateLastLoggedAtByIdentityProvider = async function ({ userId, identityProvider }) {
128+
const knexConn = DomainTransaction.getConnection();
129+
return knexConn(AUTHENTICATION_METHODS_TABLE)
130+
.where({
131+
userId,
132+
identityProvider,
133+
})
134+
.update({ lastLoggedAt: new Date() });
135+
};
136+
126137
const removeByUserIdAndIdentityProvider = async function ({ userId, identityProvider }) {
127138
return knex(AUTHENTICATION_METHODS_TABLE).where({ userId, identityProvider }).del();
128139
};
@@ -268,6 +279,7 @@ export {
268279
updateAuthenticationComplementByUserIdAndIdentityProvider,
269280
updateAuthenticationMethodUserId,
270281
updateExternalIdentifierByUserIdAndIdentityProvider,
282+
updateLastLoggedAtByIdentityProvider,
271283
updatePassword,
272284
};
273285

api/tests/identity-access-management/integration/infrastructure/repositories/authentication-method.repository.test.js

+36
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('Integration | Identity Access Management | Infrastructure | Repository
3434
'id',
3535
'createdAt',
3636
'updatedAt',
37+
'lastLoggedAt',
3738
]);
3839
});
3940

@@ -794,6 +795,41 @@ describe('Integration | Identity Access Management | Infrastructure | Repository
794795
});
795796
});
796797

798+
describe('#updateLastLoggedAtByIdentityProvider', function () {
799+
context('when authentication method is Pix', function () {
800+
it('updates lastLoggedAt in database', async function () {
801+
// given
802+
const now = new Date('2021-01-02');
803+
const createdAt = new Date('2020-01-02');
804+
const clock = sinon.useFakeTimers({ now, toFake: ['Date'] });
805+
const identityProvider = NON_OIDC_IDENTITY_PROVIDERS.PIX.code;
806+
const userId = databaseBuilder.factory.buildUser().id;
807+
await databaseBuilder.factory.buildAuthenticationMethod.withPixAsIdentityProviderAndHashedPassword({
808+
userId: userId,
809+
createdAt: createdAt,
810+
});
811+
await databaseBuilder.commit();
812+
813+
// when
814+
await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
815+
userId,
816+
identityProvider,
817+
});
818+
819+
// then
820+
const updatedAuthenticationMethod = await knex('authentication-methods')
821+
.where({ userId: userId, identityProvider: identityProvider })
822+
.first();
823+
824+
expect(updatedAuthenticationMethod).to.exist;
825+
expect(updatedAuthenticationMethod.createdAt.toISOString()).to.equal(createdAt.toISOString());
826+
expect(updatedAuthenticationMethod.lastLoggedAt.toISOString()).to.equal(now.toISOString());
827+
828+
clock.restore();
829+
});
830+
});
831+
});
832+
797833
describe('#findByUserId', function () {
798834
it("should return the user's authentication methods", async function () {
799835
// given

api/tests/identity-access-management/unit/domain/usecases/authenticate-user_test.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PIX_ADMIN, PIX_CERTIF, PIX_ORGA } from '../../../../../src/authorization/domain/constants.js';
2+
import { NON_OIDC_IDENTITY_PROVIDERS } from '../../../../../src/identity-access-management/domain/constants/identity-providers.js';
23
import { createWarningConnectionEmail } from '../../../../../src/identity-access-management/domain/emails/create-warning-connection.email.js';
34
import {
45
MissingOrInvalidCredentialsError,
@@ -20,6 +21,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
2021
let adminMemberRepository;
2122
let pixAuthenticationService;
2223
let emailRepository;
24+
let authenticationMethodRepository;
2325
let emailValidationDemandRepository;
2426
let clock;
2527

@@ -46,6 +48,9 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
4648
updateLastLoggedAt: sinon.stub(),
4749
findByUserId: sinon.stub(),
4850
};
51+
authenticationMethodRepository = {
52+
updateLastLoggedAtByIdentityProvider: sinon.stub(),
53+
};
4954
adminMemberRepository = {
5055
get: sinon.stub(),
5156
};
@@ -192,6 +197,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
192197
pixAuthenticationService,
193198
userRepository,
194199
userLoginRepository,
200+
authenticationMethodRepository,
195201
adminMemberRepository,
196202
refreshTokenRepository,
197203
tokenService,
@@ -243,6 +249,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
243249
refreshTokenRepository,
244250
userRepository,
245251
userLoginRepository,
252+
authenticationMethodRepository,
246253
audience,
247254
});
248255

@@ -267,7 +274,6 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
267274
const audience = 'https://certif.pix.fr';
268275

269276
pixAuthenticationService.getUserByUsernameAndPassword.resolves(user);
270-
271277
const refreshToken = { value: 'jwt.refresh.token', userId: '456', scope, audience };
272278
sinon.stub(RefreshToken, 'generate').returns(refreshToken);
273279

@@ -286,6 +292,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
286292
tokenService,
287293
userRepository,
288294
userLoginRepository,
295+
authenticationMethodRepository,
289296
audience,
290297
});
291298

@@ -307,7 +314,6 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
307314
const audience = 'https://certif.pix.fr';
308315

309316
const user = domainBuilder.buildUser({ email: userEmail });
310-
311317
pixAuthenticationService.getUserByUsernameAndPassword.resolves(user);
312318
tokenService.createAccessTokenFromUser
313319
.withArgs({ userId: user.id, source, audience })
@@ -324,11 +330,16 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
324330
tokenService,
325331
userRepository,
326332
userLoginRepository,
333+
authenticationMethodRepository,
327334
audience,
328335
});
329336

330337
// then
331338
expect(userLoginRepository.updateLastLoggedAt).to.have.been.calledWithExactly({ userId: user.id });
339+
expect(authenticationMethodRepository.updateLastLoggedAtByIdentityProvider).to.have.been.calledWithExactly({
340+
userId: user.id,
341+
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.PIX.code,
342+
});
332343
});
333344

334345
it('should rejects an error when given username (email) does not match an existing one', async function () {
@@ -413,6 +424,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
413424
tokenService,
414425
userRepository,
415426
userLoginRepository,
427+
authenticationMethodRepository,
416428
emailRepository,
417429
emailValidationDemandRepository,
418430
audience,
@@ -459,6 +471,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
459471
tokenService,
460472
userRepository,
461473
userLoginRepository,
474+
authenticationMethodRepository,
462475
emailRepository,
463476
audience,
464477
});
@@ -503,6 +516,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
503516
tokenService,
504517
userRepository,
505518
userLoginRepository,
519+
authenticationMethodRepository,
506520
emailRepository,
507521
audience,
508522
});
@@ -577,6 +591,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
577591
tokenService,
578592
userRepository,
579593
userLoginRepository,
594+
authenticationMethodRepository,
580595
audience,
581596
});
582597

@@ -612,6 +627,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
612627
refreshTokenRepository,
613628
userRepository,
614629
userLoginRepository,
630+
authenticationMethodRepository,
615631
audience,
616632
});
617633

@@ -645,6 +661,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
645661
tokenService,
646662
userRepository,
647663
userLoginRepository,
664+
authenticationMethodRepository,
648665
audience,
649666
});
650667

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

+2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
157157
externalIdentifier: 'saml-id',
158158
createdAt: new Date('2020-01-01'),
159159
updatedAt: new Date('2020-02-01'),
160+
lastLoggedAt: null,
160161
});
161162
userRepository.getBySamlId.withArgs('saml-id').resolves(user);
162163
authenticationMethodRepository.findOneByUserIdAndIdentityProvider
@@ -181,6 +182,7 @@ describe('Unit | UseCase | get-external-authentication-redirection-url', functio
181182
userFirstName: 'Vassili',
182183
userLastName: 'Lisitsa',
183184
externalIdentifier: 'saml-id',
185+
lastLoggedAt: null,
184186
});
185187
expect(authenticationMethodRepository.update).to.have.been.calledWithExactly(expectedAuthenticationMethod);
186188
});

api/tests/shared/unit/domain/services/user-service_test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('Unit | Shared | Domain | Service | user-service', function () {
5151
hashedPassword,
5252
});
5353
userToCreateRepository.create.resolves(user);
54-
const expectedAuthenticationMethod = omit(authenticationMethod, ['id', 'createdAt', 'updatedAt']);
54+
const expectedAuthenticationMethod = omit(authenticationMethod, ['id', 'createdAt', 'updatedAt', 'lastLoggedAt']);
5555

5656
//when
5757
await userService.createUserWithPassword({

0 commit comments

Comments
 (0)