Skip to content

Commit 4ae0228

Browse files
VincentHardouinHEYGULnlepage
committed
feat: use client application repo
Co-authored-by: Guillaume Lagorce <guillaume.lagorce@pix.fr> Co-authored-by: Nicolas Lepage <19571875+nlepage@users.noreply.github.com>
1 parent a85b34d commit 4ae0228

File tree

6 files changed

+133
-54
lines changed

6 files changed

+133
-54
lines changed

Diff for: api/db/database-builder/factory/build-client-application.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cryptoService } from '../../../src/shared/domain/services/crypto-service.js';
12
import { databaseBuffer } from '../database-buffer.js';
23

34
export function buildClientApplication({
@@ -7,13 +8,15 @@ export function buildClientApplication({
78
clientSecret = 'super-secret',
89
scopes = ['scope1', 'scope2'],
910
} = {}) {
11+
// eslint-disable-next-line no-sync
12+
const hashedSecret = cryptoService.hashPasswordSync(clientSecret);
1013
return databaseBuffer.pushInsertable({
1114
tableName: 'client_applications',
1215
values: {
1316
id,
1417
name,
1518
clientId,
16-
clientSecret,
19+
clientSecret: hashedSecret,
1720
scopes,
1821
},
1922
});

Diff for: api/lib/domain/usecases/authenticate-application.js

+24-20
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,50 @@
1-
import lodash from 'lodash';
2-
31
import { config } from '../../../src/shared/config.js';
42
import {
53
ApplicationScopeNotAllowedError,
64
ApplicationWithInvalidClientIdError,
75
ApplicationWithInvalidClientSecretError,
86
} from '../../../src/shared/domain/errors.js';
9-
const { apimRegisterApplicationsCredentials, jwtConfig } = config;
10-
11-
const { find } = lodash;
127

13-
const authenticateApplication = async function ({ clientId, clientSecret, scope, tokenService }) {
14-
const application = find(apimRegisterApplicationsCredentials, { clientId });
15-
_checkClientId(application, clientId);
16-
_checkClientSecret(application, clientSecret);
8+
const { authentication } = config;
9+
10+
export async function authenticateApplication({
11+
clientId,
12+
clientSecret,
13+
scope,
14+
tokenService,
15+
clientApplicationRepository,
16+
cryptoService,
17+
}) {
18+
const application = await clientApplicationRepository.findByClientId(clientId);
19+
_checkApplication(application, clientId);
20+
await _checkClientSecret(application, clientSecret, cryptoService);
1721
_checkAppScope(application, scope);
1822

1923
return tokenService.createAccessTokenFromApplication(
2024
clientId,
21-
application.source,
25+
application.name,
2226
scope,
23-
jwtConfig[application.source].secret,
24-
jwtConfig[application.source].tokenLifespan,
27+
authentication.secret,
28+
authentication.accessTokenLifespanMs,
2529
);
26-
};
30+
}
2731

28-
function _checkClientId(application, clientId) {
29-
if (!application || application.clientId !== clientId) {
32+
function _checkApplication(application) {
33+
if (!application) {
3034
throw new ApplicationWithInvalidClientIdError('The client ID is invalid.');
3135
}
3236
}
3337

34-
function _checkClientSecret(application, clientSecret) {
35-
if (application.clientSecret !== clientSecret) {
38+
async function _checkClientSecret(application, clientSecret, cryptoService) {
39+
try {
40+
await cryptoService.checkPassword({ password: clientSecret, passwordHash: application.clientSecret });
41+
} catch {
3642
throw new ApplicationWithInvalidClientSecretError('The client secret is invalid.');
3743
}
3844
}
3945

4046
function _checkAppScope(application, scope) {
41-
if (application.scope !== scope) {
47+
if (!application.scopes.includes(scope)) {
4248
throw new ApplicationScopeNotAllowedError('The scope is invalid.');
4349
}
4450
}
45-
46-
export { authenticateApplication };

Diff for: api/lib/domain/usecases/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import * as resetPasswordService from '../../../src/identity-access-management/d
7979
import { scoAccountRecoveryService } from '../../../src/identity-access-management/domain/services/sco-account-recovery.service.js';
8080
import { accountRecoveryDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/account-recovery-demand.repository.js';
8181
import * as authenticationMethodRepository from '../../../src/identity-access-management/infrastructure/repositories/authentication-method.repository.js';
82+
import { clientApplicationRepository } from '../../../src/identity-access-management/infrastructure/repositories/client-application.repository.js';
8283
import { emailValidationDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js';
8384
// Not used in lib
8485
import { userAnonymizedEventLoggingJobRepository } from '../../../src/identity-access-management/infrastructure/repositories/jobs/user-anonymized-event-logging-job-repository.js';
@@ -244,6 +245,7 @@ const dependencies = {
244245
certificationCpfCityRepository,
245246
certificationOfficerRepository,
246247
challengeRepository,
248+
clientApplicationRepository,
247249
codeGenerator,
248250
codeUtils,
249251
competenceEvaluationRepository,

Diff for: api/tests/acceptance/application/authentication/authentication-route_test.js

+8
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ describe('Acceptance | Controller | authentication-controller', function () {
133133
'x-forwarded-host': 'app.pix.fr',
134134
},
135135
};
136+
137+
databaseBuilder.factory.buildClientApplication({
138+
name: 'osmose',
139+
clientId: OSMOSE_CLIENT_ID,
140+
clientSecret: OSMOSE_CLIENT_SECRET,
141+
scopes: [SCOPE],
142+
});
143+
await databaseBuilder.commit();
136144
});
137145

138146
it('should return an 200 with accessToken when clientId, client secret and scope are registred', async function () {

Diff for: api/tests/prescription/organization-place/acceptance/application/get-data-organization-places_test.js

+11-19
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,31 @@
11
import querystring from 'node:querystring';
22

3-
import { createServer, expect } from '../../../../test-helper.js';
3+
import {
4+
createServer,
5+
databaseBuilder,
6+
expect,
7+
generateValidRequestAuthorizationHeaderForApplication,
8+
} from '../../../../test-helper.js';
49

510
describe('Acceptance | Route | Get Data Organization Places', function () {
611
describe('GET /api/data/organization-places', function () {
712
it('should return 200 HTTP status code', async function () {
813
// given
914
const PIX_DATA_CLIENT_ID = 'test-pixDataCliendId';
10-
const PIX_DATA_CLIENT_SECRET = 'pixDataClientSecret';
1115
const PIX_DATA_SCOPE = 'statistics';
1216

1317
const server = await createServer();
1418

15-
const optionsForToken = {
16-
method: 'POST',
17-
url: `/api/application/token`,
18-
headers: {
19-
'content-type': 'application/x-www-form-urlencoded',
20-
},
21-
payload: querystring.stringify({
22-
grant_type: 'client_credentials',
23-
client_id: PIX_DATA_CLIENT_ID,
24-
client_secret: PIX_DATA_CLIENT_SECRET,
25-
scope: PIX_DATA_SCOPE,
26-
}),
27-
};
28-
const tokenResponse = await server.inject(optionsForToken);
29-
const accessToken = tokenResponse.result.access_token;
30-
3119
// when
3220
const options = {
3321
method: 'GET',
3422
url: `/api/data/organization-places`,
3523
headers: {
36-
authorization: `Bearer ${accessToken}`,
24+
authorization: generateValidRequestAuthorizationHeaderForApplication(
25+
PIX_DATA_CLIENT_ID,
26+
'pix-data',
27+
PIX_DATA_SCOPE,
28+
),
3729
},
3830
};
3931

Diff for: api/tests/unit/domain/usecases/authenticate-application_test.js

+84-14
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import { authenticateApplication } from '../../../../lib/domain/usecases/authenticate-application.js';
2+
import { PasswordNotMatching } from '../../../../src/identity-access-management/domain/errors.js';
3+
import { config } from '../../../../src/shared/config.js';
24
import {
35
ApplicationScopeNotAllowedError,
46
ApplicationWithInvalidClientIdError,
57
ApplicationWithInvalidClientSecretError,
68
} from '../../../../src/shared/domain/errors.js';
7-
import { catchErr, expect, sinon } from '../../../test-helper.js';
9+
import { catchErr, domainBuilder, expect, sinon } from '../../../test-helper.js';
810

911
describe('Unit | Usecase | authenticate-application', function () {
1012
context('when application is not found', function () {
1113
it('should throw an error', async function () {
12-
const client = {
14+
const payload = {
1315
clientId: Symbol('id'),
1416
clientSecret: Symbol('secret'),
1517
};
1618

17-
const err = await catchErr(authenticateApplication)(client);
19+
const clientApplicationRepository = {
20+
findByClientId: sinon.stub(),
21+
};
22+
clientApplicationRepository.findByClientId.withArgs(payload.clientId).resolves(undefined);
23+
24+
const err = await catchErr(authenticateApplication)({ ...payload, clientApplicationRepository });
1825

1926
expect(err).to.be.instanceOf(ApplicationWithInvalidClientIdError);
2027
});
@@ -23,48 +30,111 @@ describe('Unit | Usecase | authenticate-application', function () {
2330
context('when application is found', function () {
2431
context('when client secrets are different', function () {
2532
it('should throw an error', async function () {
26-
const client = {
33+
const payload = {
2734
clientId: 'test-apimOsmoseClientId',
28-
clientSecret: Symbol('toto'),
35+
clientSecret: 'mauvais-secret',
36+
};
37+
38+
const clientApplicationRepository = {
39+
findByClientId: sinon.stub(),
2940
};
41+
const application = domainBuilder.buildClientApplication({
42+
name: 'test-apimOsmoseClientId',
43+
clientSecret: 'mon-secret',
44+
scopes: [],
45+
});
46+
clientApplicationRepository.findByClientId.withArgs(payload.clientId).resolves(application);
3047

31-
const err = await catchErr(authenticateApplication)(client);
48+
const cryptoService = {
49+
checkPassword: sinon.stub(),
50+
};
51+
cryptoService.checkPassword
52+
.withArgs({ password: payload.clientSecret, passwordHash: application.clientSecret })
53+
.rejects(new PasswordNotMatching());
54+
55+
const err = await catchErr(authenticateApplication)({ ...payload, clientApplicationRepository, cryptoService });
3256

3357
expect(err).to.be.instanceOf(ApplicationWithInvalidClientSecretError);
3458
});
3559
});
3660

3761
context('when client scopes are different', function () {
3862
it('should throw an error', async function () {
39-
const client = {
63+
const payload = {
4064
clientId: 'test-apimOsmoseClientId',
41-
clientSecret: 'test-apimOsmoseClientSecret',
65+
clientSecret: 'bon-secret',
4266
scope: 'mauvais-scope',
4367
};
4468

45-
const err = await catchErr(authenticateApplication)(client);
69+
const clientApplicationRepository = {
70+
findByClientId: sinon.stub(),
71+
};
72+
const application = domainBuilder.buildClientApplication({
73+
name: 'test-apimOsmoseClientId',
74+
clientSecret: 'bon-secret',
75+
scopes: ['bon-scope'],
76+
});
77+
clientApplicationRepository.findByClientId.withArgs(payload.clientId).resolves(application);
78+
79+
const cryptoService = {
80+
checkPassword: sinon.stub(),
81+
};
82+
cryptoService.checkPassword
83+
.withArgs({ password: payload.clientSecret, passwordHash: application.clientSecret })
84+
.resolves();
85+
86+
const err = await catchErr(authenticateApplication)({ ...payload, clientApplicationRepository, cryptoService });
4687

4788
expect(err).to.be.instanceOf(ApplicationScopeNotAllowedError);
4889
});
4990
});
5091

5192
context('when given information is correct', function () {
5293
it('should return created token', async function () {
53-
const client = {
94+
const payload = {
5495
clientId: 'test-apimOsmoseClientId',
55-
clientSecret: 'test-apimOsmoseClientSecret',
56-
scope: 'organizations-certifications-result',
96+
clientSecret: 'bon-secret',
97+
scope: 'bon-scope',
98+
};
99+
100+
const clientApplicationRepository = {
101+
findByClientId: sinon.stub(),
102+
};
103+
const application = domainBuilder.buildClientApplication({
104+
name: 'mon-application',
105+
clientId: 'test-apimOsmoseClientId',
106+
clientSecret: 'bon-secret',
107+
scopes: ['bon-scope'],
108+
});
109+
clientApplicationRepository.findByClientId.withArgs(payload.clientId).resolves(application);
110+
111+
const cryptoService = {
112+
checkPassword: sinon.stub(),
57113
};
114+
cryptoService.checkPassword
115+
.withArgs({ password: payload.clientSecret, passwordHash: application.clientSecret })
116+
.resolves();
58117

59118
const tokenService = {
60119
createAccessTokenFromApplication: sinon.stub(),
61120
};
62121
const expectedToken = Symbol('Mon Super token');
63122
tokenService.createAccessTokenFromApplication
64-
.withArgs(client.clientId, 'livretScolaire', client.scope, 'test-secretOsmose', '4h')
123+
.withArgs(
124+
application.clientId,
125+
application.name,
126+
payload.scope,
127+
config.authentication.secret,
128+
config.authentication.accessTokenLifespanMs,
129+
)
65130
.resolves(expectedToken);
66131

67-
const token = await authenticateApplication({ ...client, tokenService });
132+
const token = await authenticateApplication({
133+
...payload,
134+
tokenService,
135+
clientApplicationRepository,
136+
cryptoService,
137+
});
68138

69139
expect(token).to.be.equal(expectedToken);
70140
});

0 commit comments

Comments
 (0)