diff --git a/api/package-lock.json b/api/package-lock.json index 089c67036ce..18ce23350e9 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -59,7 +59,7 @@ "node-cache": "^5.1.2", "node-stream-zip": "^1.15.0", "nodemailer": "^6.9.6", - "openid-client": "^5.6.4", + "openid-client": "^6.3.3", "papaparse": "^5.3.2", "pdf-lib": "^1.17.1", "pg": "^8.7.3", @@ -8212,9 +8212,9 @@ } }, "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.8.tgz", + "integrity": "sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -10057,6 +10057,15 @@ "node": "*" } }, + "node_modules/oauth4webapi": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.0.tgz", + "integrity": "sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10073,15 +10082,6 @@ "dev": true, "license": "MIT" }, - "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-to-spawn-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", @@ -10092,15 +10092,6 @@ "node": ">=8.0.0" } }, - "node_modules/oidc-token-hash": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", - "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", - "license": "MIT", - "engines": { - "node": "^10.13.0 || >=12.0.0" - } - }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -10127,38 +10118,18 @@ "peer": true }, "node_modules/openid-client": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", - "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.3.3.tgz", + "integrity": "sha512-lTK8AV8SjqCM4qznLX0asVESAwzV39XTVdfMAM185ekuaZCnkWdPzcxMTXNlsm9tsUAMa1Q30MBmKAykdT1LWw==", "license": "MIT", "dependencies": { - "jose": "^4.15.9", - "lru-cache": "^6.0.0", - "object-hash": "^2.2.0", - "oidc-token-hash": "^5.0.3" + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0" }, "funding": { "url": "https://github.com/sponsors/panva" } }, - "node_modules/openid-client/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/openid-client/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/api/package.json b/api/package.json index 5b3a842473b..d1f33c02ffc 100644 --- a/api/package.json +++ b/api/package.json @@ -65,7 +65,7 @@ "node-cache": "^5.1.2", "node-stream-zip": "^1.15.0", "nodemailer": "^6.9.6", - "openid-client": "^5.6.4", + "openid-client": "^6.3.3", "papaparse": "^5.3.2", "pdf-lib": "^1.17.1", "pg": "^8.7.3", diff --git a/api/src/identity-access-management/domain/services/oidc-authentication-service-registry.js b/api/src/identity-access-management/domain/services/oidc-authentication-service-registry.js index 7dd046d3c98..2829f7c10e1 100644 --- a/api/src/identity-access-management/domain/services/oidc-authentication-service-registry.js +++ b/api/src/identity-access-management/domain/services/oidc-authentication-service-registry.js @@ -23,9 +23,9 @@ export class OidcAuthenticationServiceRegistry { ); if (!oidcProviderService) return; - if (oidcProviderService.client) return; - await oidcProviderService.createClient(); + await oidcProviderService.initializeClientConfig(); + return true; } @@ -88,4 +88,10 @@ export class OidcAuthenticationServiceRegistry { ); return true; } + + testOnly_reset() { + this.#allOidcProviderServices = null; + this.#readyOidcProviderServices = null; + this.#readyOidcProviderServicesForPixAdmin = null; + } } diff --git a/api/src/identity-access-management/domain/services/oidc-authentication-service.js b/api/src/identity-access-management/domain/services/oidc-authentication-service.js index 7ec957e843f..941edc14f6e 100644 --- a/api/src/identity-access-management/domain/services/oidc-authentication-service.js +++ b/api/src/identity-access-management/domain/services/oidc-authentication-service.js @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import jsonwebtoken from 'jsonwebtoken'; import lodash from 'lodash'; import ms from 'ms'; -import { Issuer } from 'openid-client'; +import * as client from 'openid-client'; import { config } from '../../../shared/config.js'; import { OIDC_ERRORS } from '../../../shared/domain/constants.js'; @@ -23,6 +23,8 @@ const defaultSessionTemporaryStorage = temporaryStorage.withPrefix('oidc-session export class OidcAuthenticationService { #isReady = false; #isReadyForPixAdmin = false; + #openIdClient; + #openIdClientConfig; constructor( { @@ -47,7 +49,7 @@ export class OidcAuthenticationService { isVisible = true, claimMapping, }, - { sessionTemporaryStorage = defaultSessionTemporaryStorage } = {}, + { sessionTemporaryStorage = defaultSessionTemporaryStorage, openIdClient = client } = {}, ) { this.accessTokenLifespanMs = ms(accessTokenLifespan); this.additionalRequiredProperties = additionalRequiredProperties; @@ -68,6 +70,7 @@ export class OidcAuthenticationService { this.slug = slug; this.source = source; this.isVisible = isVisible; + this.#openIdClient = openIdClient; claimMapping = claimMapping || DEFAULT_CLAIM_MAPPING; @@ -105,20 +108,20 @@ export class OidcAuthenticationService { return this.#isReadyForPixAdmin; } - async createClient() { + async initializeClientConfig() { + if (this.#openIdClientConfig) return; + try { - const issuer = await Issuer.discover(this.openidConfigurationUrl); const metadata = { - client_id: this.clientId, client_secret: this.clientSecret, - redirect_uris: [this.redirectUri], + ...this.openidClientExtraMetadata, }; - if (this.openidClientExtraMetadata) { - Object.assign(metadata, this.openidClientExtraMetadata); - } - - this.client = new issuer.Client(metadata); + this.#openIdClientConfig = await this.#openIdClient.discovery( + new URL(this.openidConfigurationUrl), + this.clientId, + metadata, + ); } catch (error) { logger.error(`OIDC Provider "${this.identityProvider}" is UNAVAILABLE: ${error}`); } @@ -145,10 +148,26 @@ export class OidcAuthenticationService { } async exchangeCodeForTokens({ code, state, iss, nonce, sessionState }) { - let tokenSet; - try { - tokenSet = await this.client.callback(this.redirectUri, { code, state, iss }, { nonce, state: sessionState }); + const currentUrl = new URL(this.redirectUri); + currentUrl.searchParams.append('code', code); + currentUrl.searchParams.append('state', state); + currentUrl.searchParams.append('session_state', sessionState); + + const checks = { expectedNonce: nonce, expectedState: sessionState }; + + const tokenResponse = await this.#openIdClient.authorizationCodeGrant( + this.#openIdClientConfig, + currentUrl, + checks, + ); + + return new AuthenticationSessionContent({ + accessToken: tokenResponse.access_token, + expiresIn: tokenResponse.expires_in, + idToken: tokenResponse.id_token, + refreshToken: tokenResponse.refresh_token, + }); } catch (error) { _monitorOidcError(error.message, { data: { code, nonce, organizationName: this.organizationName, sessionState, state, iss }, @@ -157,40 +176,23 @@ export class OidcAuthenticationService { }); throw new OidcError({ message: error.message }); } - - const { - access_token: accessToken, - expires_in: expiresIn, - id_token: idToken, - refresh_token: refreshToken, - } = tokenSet; - - return new AuthenticationSessionContent({ - accessToken, - expiresIn, - idToken, - refreshToken, - }); } getAuthorizationUrl() { - const state = randomUUID(); - const nonce = randomUUID(); - const authorizationParameters = { - nonce, - redirect_uri: this.redirectUri, - scope: this.scope, - state, - }; - - if (this.extraAuthorizationUrlParameters) { - Object.assign(authorizationParameters, this.extraAuthorizationUrlParameters); - } + try { + const state = randomUUID(); + const nonce = randomUUID(); + const parameters = { + nonce, + redirect_uri: this.redirectUri, + scope: this.scope, + state, + ...this.extraAuthorizationUrlParameters, + }; - let redirectTarget; + const redirectTarget = this.#openIdClient.buildAuthorizationUrl(this.#openIdClientConfig, parameters); - try { - redirectTarget = this.client.authorizationUrl(authorizationParameters); + return { redirectTarget, state, nonce }; } catch (error) { _monitorOidcError(error.message, { data: { organizationName: this.organizationName }, @@ -199,15 +201,13 @@ export class OidcAuthenticationService { }); throw new OidcError({ message: error.message }); } - - return { redirectTarget, state, nonce }; } async getUserInfo({ idToken, accessToken }) { let userInfo = jsonwebtoken.decode(idToken); if (this.claimManager.hasMissingClaims(userInfo)) { - userInfo = await this._getUserInfoFromEndpoint({ accessToken }); + userInfo = await this._getUserInfoFromEndpoint({ accessToken, expectedSubject: userInfo.sub }); } return { @@ -260,7 +260,7 @@ export class OidcAuthenticationService { } try { - const endSessionUrl = this.client.endSessionUrl(parameters); + const endSessionUrl = this.#openIdClient.buildEndSessionUrl(this.#openIdClientConfig, parameters); await this.sessionTemporaryStorage.delete(key); @@ -275,11 +275,11 @@ export class OidcAuthenticationService { } } - async _getUserInfoFromEndpoint({ accessToken }) { + async _getUserInfoFromEndpoint({ accessToken, expectedSubject }) { let userInfo; try { - userInfo = await this.client.userinfo(accessToken); + userInfo = await this.#openIdClient.fetchUserInfo(this.#openIdClientConfig, accessToken, expectedSubject); } catch (error) { _monitorOidcError(error.message, { data: { organizationName: this.organizationName }, diff --git a/api/tests/identity-access-management/acceptance/application/oidc-provider.admin.route.test.js b/api/tests/identity-access-management/acceptance/application/oidc-provider.admin.route.test.js index 7590781799e..1b82e08611d 100644 --- a/api/tests/identity-access-management/acceptance/application/oidc-provider.admin.route.test.js +++ b/api/tests/identity-access-management/acceptance/application/oidc-provider.admin.route.test.js @@ -10,11 +10,13 @@ import { insertUserWithRoleSuperAdmin, knex, } from '../../../test-helper.js'; +import { createMockedTestOidcProvider } from '../../../tooling/openid-client/openid-client-mocks.js'; describe('Acceptance | Identity Access Management | Route | Admin | oidc-provider', function () { let server; beforeEach(async function () { + await createMockedTestOidcProvider(); server = await createServer(); }); diff --git a/api/tests/identity-access-management/acceptance/application/oidc-provider.route.test.js b/api/tests/identity-access-management/acceptance/application/oidc-provider.route.test.js index 0aebc6b08c8..5533b89bda2 100644 --- a/api/tests/identity-access-management/acceptance/application/oidc-provider.route.test.js +++ b/api/tests/identity-access-management/acceptance/application/oidc-provider.route.test.js @@ -2,7 +2,6 @@ import querystring from 'node:querystring'; import jsonwebtoken from 'jsonwebtoken'; -import { oidcAuthenticationServiceRegistry } from '../../../../lib/domain/usecases/index.js'; import { authenticationSessionService } from '../../../../src/identity-access-management/domain/services/authentication-session.service.js'; import { AuthenticationSessionContent } from '../../../../src/shared/domain/models/index.js'; import { decodeIfValid } from '../../../../src/shared/domain/services/token-service.js'; @@ -12,15 +11,16 @@ import { expect, generateAuthenticatedUserRequestHeaders, knex, - sinon, } from '../../../test-helper.js'; +import { createMockedTestOidcProvider } from '../../../tooling/openid-client/openid-client-mocks.js'; const UUID_PATTERN = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); describe('Acceptance | Identity Access Management | Application | Route | oidc-provider', function () { - let server; + let server, openIdClientMock; beforeEach(async function () { + openIdClientMock = await createMockedTestOidcProvider(); server = await createServer(); }); @@ -81,9 +81,7 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p describe('GET /api/oidc/authorization-url', function () { it('returns an object which contains the authentication url with an HTTP status code 200', async function () { // given - const query = querystring.stringify({ - identity_provider: 'OIDC_EXAMPLE_NET', - }); + const query = querystring.stringify({ identity_provider: 'OIDC_EXAMPLE_NET' }); // when const response = await server.inject({ @@ -112,17 +110,10 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p }); describe('POST /api/oidc/token', function () { - let clock, payload, cookies, oidcExampleNetProvider; + let payload, cookies; beforeEach(async function () { - clock = sinon.useFakeTimers({ - now: Date.now(), - toFake: ['Date'], - }); - - const query = querystring.stringify({ - identity_provider: 'OIDC_EXAMPLE_NET', - }); + const query = querystring.stringify({ identity_provider: 'OIDC_EXAMPLE_NET' }); const authUrlResponse = await server.inject({ method: 'GET', url: `/api/oidc/authorization-url?${query}`, @@ -144,58 +135,20 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p }, }, }; - - oidcExampleNetProvider = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({ - identityProviderCode: 'OIDC_EXAMPLE_NET', - }); - sinon.stub(oidcExampleNetProvider.client, 'callback'); - }); - - afterEach(async function () { - clock.restore(); }); context('When user does not have an account', function () { it('returns status code 401 with authentication key matching session content and error code to validate cgu', async function () { // given - const idToken = jsonwebtoken.sign( - { - given_name: 'John', - family_name: 'Doe', - sub: 'sub', - }, - 'secret', - ); + const idToken = jsonwebtoken.sign({ given_name: 'John', family_name: 'Doe', sub: 'sub' }, 'secret'); - const getAccessTokenResponse = { + openIdClientMock.authorizationCodeGrant.resolves({ access_token: 'access_token', id_token: idToken, expires_in: 60, refresh_token: 'refresh_token', - }; - - /* - Le code ci-dessous a été commenté parce qu'on utilise un fournisseur d'identité - non valide d'exemple et l'utilisation de nock n'est pas possible car la librairie - openid-client tentera de valider le token reçu avec une configuration de chiffrement - d'exemple. - */ - //nock('https://oidc.example.net').post('/ea5ac20c-5076-4806-860a-b0aeb01645d4/oauth2/v2.0/token').reply(200, getAccessTokenResponse); - oidcExampleNetProvider.client.callback.resolves(getAccessTokenResponse); + }); - const sessionContentAndUserInfo = { - sessionContent: new AuthenticationSessionContent({ - accessToken: 'access_token', - idToken, - expiresIn: 60, - refreshToken: 'refresh_token', - }), - userInfo: { - externalIdentityId: 'sub', - firstName: 'John', - lastName: 'Doe', - }, - }; const headers = generateAuthenticatedUserRequestHeaders(); headers.cookie = cookies[0]; @@ -219,8 +172,21 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p const authenticationKey = error.meta.authenticationKey; expect(authenticationKey).to.exist; + const result = await authenticationSessionService.getByKey(authenticationKey); - expect(result).to.deep.equal(sessionContentAndUserInfo); + expect(result).to.deep.equal({ + sessionContent: new AuthenticationSessionContent({ + accessToken: 'access_token', + idToken, + expiresIn: 60, + refreshToken: 'refresh_token', + }), + userInfo: { + externalIdentityId: 'sub', + firstName: 'John', + lastName: 'Doe', + }, + }); }); }); @@ -231,43 +197,25 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p const lastName = 'Doe'; const externalIdentifier = 'sub'; - const userId = databaseBuilder.factory.buildUser({ - firstName, - lastName, - }).id; - + const userId = databaseBuilder.factory.buildUser({ firstName, lastName }).id; databaseBuilder.factory.buildAuthenticationMethod.withIdentityProvider({ identityProvider: 'OIDC_EXAMPLE_NET', externalIdentifier, - accessToken: 'access_token', - refreshToken: 'refresh_token', - expiresIn: 1000, userId, }); await databaseBuilder.commit(); const idToken = jsonwebtoken.sign( - { - given_name: firstName, - family_name: lastName, - sub: externalIdentifier, - }, + { given_name: firstName, family_name: lastName, sub: externalIdentifier }, 'secret', ); - const getAccessTokenResponse = { + + openIdClientMock.authorizationCodeGrant.resolves({ access_token: 'access_token', id_token: idToken, expires_in: 60, refresh_token: 'refresh_token', - }; - /* - Le code ci-dessous a été commenté parce qu'on utilise un fournisseur d'identité - non valide d'exemple et l'utilisation de nock n'est pas possible car la librairie - openid-client tentera de valider le token reçu avec une configuration de chiffrement - d'exemple. - */ - // const getAccessTokenRequest = nock(settings.poleEmploi.tokenUrl).post('/').reply(200, getAccessTokenResponse); - oidcExampleNetProvider.client.callback.resolves(getAccessTokenResponse); + }); // when const response = await server.inject({ @@ -282,19 +230,11 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p }); // then - /* - Le code ci-dessous a été commenté parce qu'on utilise un fournisseur d'identité - non valide d'exemple et l'utilisation de nock n'est pas possible car la librairie - openid-client tentera de valider le token reçu avec une configuration de chiffrement - d'exemple. - */ - // expect(getAccessTokenRequest.isDone()).to.be.true; - expect(oidcExampleNetProvider.client.callback).to.have.been.calledOnce; + expect(openIdClientMock.authorizationCodeGrant).to.have.been.calledOnce; expect(response.result.access_token).to.exist; + const decodedAccessToken = await decodeIfValid(response.result.access_token); - expect(decodedAccessToken).to.include({ - aud: 'https://orga.pix.fr', - }); + expect(decodedAccessToken).to.include({ aud: 'https://orga.pix.fr' }); expect(response.statusCode).to.equal(200); expect(response.result['logout_url_uuid']).to.match(UUID_PATTERN); }); @@ -308,43 +248,25 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p const lastName = 'Doe'; const externalIdentifier = 'sub'; - const userId = databaseBuilder.factory.buildUser({ - firstName, - lastName, - }).id; - + const userId = databaseBuilder.factory.buildUser({ firstName, lastName }).id; databaseBuilder.factory.buildAuthenticationMethod.withIdentityProvider({ identityProvider: 'OIDC_EXAMPLE_NET', externalIdentifier, - accessToken: 'access_token', - refreshToken: 'refresh_token', - expiresIn: 1000, userId, }); await databaseBuilder.commit(); const idToken = jsonwebtoken.sign( - { - given_name: firstName, - family_name: lastName, - sub: externalIdentifier, - }, + { given_name: firstName, family_name: lastName, sub: externalIdentifier }, 'secret', ); - const getAccessTokenResponse = { + + openIdClientMock.authorizationCodeGrant.resolves({ access_token: 'access_token', id_token: idToken, expires_in: 60, refresh_token: 'refresh_token', - }; - /* - Le code ci-dessous a été commenté parce qu'on utilise un fournisseur d'identité - non valide d'exemple et l'utilisation de nock n'est pas possible car la librairie - openid-client tentera de valider le token reçu avec une configuration de chiffrement - d'exemple. - */ - // const getAccessTokenRequest = nock(settings.poleEmploi.tokenUrl).post('/').reply(200, getAccessTokenResponse); - oidcExampleNetProvider.client.callback.resolves(getAccessTokenResponse); + }); const headers = generateAuthenticatedUserRequestHeaders(); headers.cookie = cookies[0]; @@ -362,14 +284,7 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p }); // then - /* - Le code ci-dessous a été commenté parce qu'on utilise un fournisseur d'identité - non valide d'exemple et l'utilisation de nock n'est pas possible car la librairie - openid-client tentera de valider le token reçu avec une configuration de chiffrement - d'exemple. - */ - // expect(getAccessTokenRequest.isDone()).to.be.true; - expect(oidcExampleNetProvider.client.callback).to.have.been.calledOnce; + expect(openIdClientMock.authorizationCodeGrant).to.have.been.calledOnce; expect(response.statusCode).to.equal(403); }); }); @@ -381,45 +296,25 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p const lastName = 'Doe'; const externalIdentifier = 'sub'; - const userId = databaseBuilder.factory.buildUser.withRole({ - firstName, - lastName, - role: 'SUPER_ADMIN', - }).id; - + const userId = databaseBuilder.factory.buildUser.withRole({ firstName, lastName, role: 'SUPER_ADMIN' }).id; databaseBuilder.factory.buildAuthenticationMethod.withIdentityProvider({ identityProvider: 'OIDC_EXAMPLE_NET', externalIdentifier, - accessToken: 'access_token', - refreshToken: 'refresh_token', - expiresIn: 1000, userId, }); - await databaseBuilder.commit(); const idToken = jsonwebtoken.sign( - { - given_name: firstName, - family_name: lastName, - sub: externalIdentifier, - }, + { given_name: firstName, family_name: lastName, sub: externalIdentifier }, 'secret', ); - const getAccessTokenResponse = { + + openIdClientMock.authorizationCodeGrant.resolves({ access_token: 'access_token', id_token: idToken, expires_in: 60, refresh_token: 'refresh_token', - }; - /* - Le code ci-dessous a été commenté parce qu'on utilise un fournisseur d'identité - non valide d'exemple et l'utilisation de nock n'est pas possible car la librairie - openid-client tentera de valider le token reçu avec une configuration de chiffrement - d'exemple. - */ - // const getAccessTokenRequest = nock(settings.poleEmploi.tokenUrl).post('/').reply(200, getAccessTokenResponse); - oidcExampleNetProvider.client.callback.resolves(getAccessTokenResponse); + }); // when const response = await server.inject({ @@ -430,19 +325,11 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p }); // then - /* - Le code ci-dessous a été commenté parce qu'on utilise un fournisseur d'identité - non valide d'exemple et l'utilisation de nock n'est pas possible car la librairie - openid-client tentera de valider le token reçu avec une configuration de chiffrement - d'exemple. - */ - // expect(getAccessTokenRequest.isDone()).to.be.true; - expect(oidcExampleNetProvider.client.callback).to.have.been.calledOnce; + expect(openIdClientMock.authorizationCodeGrant).to.have.been.calledOnce; expect(response.result.access_token).to.exist; + const decodedAccessToken = await decodeIfValid(response.result.access_token); - expect(decodedAccessToken).to.include({ - aud: 'https://admin.pix.fr', - }); + expect(decodedAccessToken).to.include({ aud: 'https://admin.pix.fr' }); expect(response.statusCode).to.equal(200); }); }); @@ -456,18 +343,11 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p const lastName = 'Glace'; const externalIdentifier = 'sub'; const idToken = jsonwebtoken.sign( - { - given_name: firstName, - family_name: lastName, - nonce: 'nonce', - sub: externalIdentifier, - }, + { given_name: firstName, family_name: lastName, nonce: 'nonce', sub: externalIdentifier }, 'secret', ); - const sessionContent = new AuthenticationSessionContent({ - idToken, - }); + const sessionContent = new AuthenticationSessionContent({ idToken }); const userAuthenticationKey = await authenticationSessionService.save({ sessionContent, userInfo: { @@ -504,9 +384,7 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p expect(response.statusCode).to.equal(200); expect(response.result.access_token).to.exist; const decodedAccessToken = await decodeIfValid(response.result.access_token); - expect(decodedAccessToken).to.include({ - aud: 'https://app.pix.fr', - }); + expect(decodedAccessToken).to.include({ aud: 'https://app.pix.fr' }); const createdUser = await knex('users').first(); expect(createdUser.firstName).to.equal('Brice'); @@ -555,12 +433,7 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p await databaseBuilder.commit(); const idToken = jsonwebtoken.sign( - { - given_name: 'Brice', - family_name: 'Glace', - nonce: 'nonce', - sub: 'some-user-unique-id', - }, + { given_name: 'Brice', family_name: 'Glace', nonce: 'nonce', sub: 'some-user-unique-id' }, 'secret', ); const userAuthenticationKey = await authenticationSessionService.save({ @@ -609,12 +482,7 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p await databaseBuilder.commit(); const idToken = jsonwebtoken.sign( - { - given_name: 'Brice', - family_name: 'Glace', - nonce: 'nonce', - sub: 'some-user-unique-id', - }, + { given_name: 'Brice', family_name: 'Glace', nonce: 'nonce', sub: 'some-user-unique-id' }, 'secret', ); const userAuthenticationKey = await authenticationSessionService.save({ @@ -649,10 +517,9 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p // then expect(response.statusCode).to.equal(200); expect(response.result.access_token).to.exist; + const decodedAccessToken = await decodeIfValid(response.result.access_token); - expect(decodedAccessToken).to.include({ - aud: 'https://app.pix.fr', - }); + expect(decodedAccessToken).to.include({ aud: 'https://app.pix.fr' }); expect(response.result['logout_url_uuid']).to.match(UUID_PATTERN); }); }); diff --git a/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service-registry_test.js b/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service-registry.test.js similarity index 88% rename from api/tests/identity-access-management/unit/domain/services/oidc-authentication-service-registry_test.js rename to api/tests/identity-access-management/unit/domain/services/oidc-authentication-service-registry.test.js index c38c8be3470..747b0d805b2 100644 --- a/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service-registry_test.js +++ b/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service-registry.test.js @@ -112,12 +112,13 @@ describe('Unit | Identity Access Management | Domain | Services | oidc-authentic context('when oidc provider service exists and loaded', function () { it('configures openid client for ready oidc provider service and returns true', async function () { // given - const createClient = sinon.stub().resolves(); + const initializeClientConfig = sinon.stub().resolves(); const oidcProviderServices = [ { code: 'OIDC', isReady: true, - createClient, + isClientConfigInitialized: false, + initializeClientConfig, }, ]; await oidcAuthenticationServiceRegistry.loadOidcProviderServices(oidcProviderServices); @@ -129,27 +130,7 @@ describe('Unit | Identity Access Management | Domain | Services | oidc-authentic // then expect(result).to.be.true; - expect(createClient).to.have.been.calledOnce; - }); - - context('when there is already a client instantiated', function () { - it('returns undefined', async function () { - // given - const oidcProviderServices = [ - { - code: 'OIDC', - isReady: true, - client: {}, - }, - ]; - await oidcAuthenticationServiceRegistry.loadOidcProviderServices(oidcProviderServices); - - // when - const result = await oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode('OIDC'); - - // then - expect(result).to.be.undefined; - }); + expect(initializeClientConfig).to.have.been.calledOnce; }); }); }); diff --git a/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service_test.js b/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service.test.js similarity index 77% rename from api/tests/identity-access-management/unit/domain/services/oidc-authentication-service_test.js rename to api/tests/identity-access-management/unit/domain/services/oidc-authentication-service.test.js index ff60514ec10..ef637d21a2c 100644 --- a/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service_test.js +++ b/api/tests/identity-access-management/unit/domain/services/oidc-authentication-service.test.js @@ -1,6 +1,5 @@ import jsonwebtoken from 'jsonwebtoken'; import ms from 'ms'; -import { Issuer } from 'openid-client'; import { OidcAuthenticationService } from '../../../../../src/identity-access-management/domain/services/oidc-authentication-service.js'; import { config as settings } from '../../../../../src/shared/config.js'; @@ -14,11 +13,17 @@ import { } from '../../../../../src/shared/domain/models/index.js'; import { monitoringTools } from '../../../../../src/shared/infrastructure/monitoring-tools.js'; import { catchErr, catchErrSync, expect, sinon } from '../../../../test-helper.js'; +import { createOpenIdClientMock } from '../../../../tooling/openid-client/openid-client-mocks.js'; const uuidV4Regex = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; +const MOCK_OIDC_PROVIDER_CONFIG = Symbol('config'); + describe('Unit | Domain | Services | oidc-authentication-service', function () { + let openIdClient; + beforeEach(function () { + openIdClient = createOpenIdClientMock(MOCK_OIDC_PROVIDER_CONFIG); sinon.stub(monitoringTools, 'logErrorWithCorrelationIds'); }); @@ -29,7 +34,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { const args = {}; // when - const oidcAuthenticationService = new OidcAuthenticationService(args); + const oidcAuthenticationService = new OidcAuthenticationService(args, { openIdClient }); // then expect(oidcAuthenticationService.shouldCloseSession).to.be.false; @@ -44,7 +49,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { const args = { claimMapping: null, claimsToStore: null }; // when - const { claimManager } = new OidcAuthenticationService(args); + const { claimManager } = new OidcAuthenticationService(args, { openIdClient }); const claims = claimManager.getMissingClaims(); // then @@ -58,7 +63,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { const args = { claimMapping: { firstName: ['hello'] }, claimsToStore: null }; // when - const { claimManager } = new OidcAuthenticationService(args); + const { claimManager } = new OidcAuthenticationService(args, { openIdClient }); const claims = claimManager.getMissingClaims(); // then @@ -72,7 +77,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { const args = { claimMapping: { firstName: ['hello'] }, claimsToStore: 'employeeNumber,studentGroup' }; // when - const { claimManager } = new OidcAuthenticationService(args); + const { claimManager } = new OidcAuthenticationService(args, { openIdClient }); const claims = claimManager.getMissingClaims(); // then @@ -85,16 +90,19 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { context('when enabled in config', function () { it('returns true', function () { // given - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId: 'anId', - clientSecret: 'aSecret', - additionalRequiredProperties: { - aProperty: 'a property value', + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId: 'anId', + clientSecret: 'aSecret', + additionalRequiredProperties: { + aProperty: 'a property value', + }, + enabled: true, + openidConfigurationUrl: 'https://example.net/.well-known/openid-configuration', + redirectUri: 'https://example.net/connexion/redirect', }, - enabled: true, - openidConfigurationUrl: 'https://example.net/.well-known/openid-configuration', - redirectUri: 'https://example.net/connexion/redirect', - }); + { openIdClient }, + ); // when const isOidcAuthenticationServiceReady = oidcAuthenticationService.isReady; @@ -107,7 +115,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { context('when not enabled in config', function () { it('returns false', function () { // given - const oidcAuthenticationService = new OidcAuthenticationService({}); + const oidcAuthenticationService = new OidcAuthenticationService({}, { openIdClient }); // when const result = oidcAuthenticationService.isReady; @@ -131,7 +139,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { .withArgs(payload, settings.authentication.secret, jwtOptions) .returns(accessToken); - const oidcAuthenticationService = new OidcAuthenticationService(settings.oidcExampleNet); + const oidcAuthenticationService = new OidcAuthenticationService(settings.oidcExampleNet, { openIdClient }); // when const result = oidcAuthenticationService.createAccessToken({ userId, audience }); @@ -147,7 +155,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { // given const userInfo = {}; const identityProvider = 'genericOidcProviderCode'; - const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider }); + const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider }, { openIdClient }); // when const result = oidcAuthenticationService.createAuthenticationComplement({ userInfo }); @@ -166,7 +174,10 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { const claimsToStoreWithValues = { family_name, given_name }; const userInfo = { ...claimsToStoreWithValues }; const identityProvider = 'genericOidcProviderCode'; - const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider, claimsToStore }); + const oidcAuthenticationService = new OidcAuthenticationService( + { identityProvider, claimsToStore }, + { openIdClient }, + ); // when const result = oidcAuthenticationService.createAuthenticationComplement({ userInfo }); @@ -188,8 +199,9 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { const oidcAuthenticationService = new OidcAuthenticationService(settings.oidcExampleNet, { sessionTemporaryStorage, + openIdClient, }); - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); // when const result = await oidcAuthenticationService.saveIdToken({ idToken, userId }); @@ -211,21 +223,19 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { }; const postLogoutRedirectUriEncoded = encodeURIComponent(settings.oidcExampleNet.postLogoutRedirectUri); const endSessionUrl = `https://example.net/logout?post_logout_redirect_uri=${postLogoutRedirectUriEncoded}&id_token_hint=some_dummy_id_token`; - const clientInstance = { endSessionUrl: sinon.stub().resolves(endSessionUrl) }; - const Client = sinon.stub().returns(clientInstance); - - sinon.stub(Issuer, 'discover').resolves({ Client }); + openIdClient.buildEndSessionUrl.resolves(endSessionUrl); const oidcAuthenticationService = new OidcAuthenticationService(settings.oidcExampleNet, { sessionTemporaryStorage, + openIdClient, }); - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); // when const result = await oidcAuthenticationService.getRedirectLogoutUrl({ userId, logoutUrlUUID }); // then - expect(clientInstance.endSessionUrl).to.have.been.calledWith({ + expect(openIdClient.buildEndSessionUrl).to.have.been.calledWith(MOCK_OIDC_PROVIDER_CONFIG, { id_token_hint: idToken, post_logout_redirect_uri: settings.oidcExampleNet.postLogoutRedirectUri, }); @@ -245,16 +255,13 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { delete: sinon.stub().resolves(), }; const errorThrown = new Error('Fails to generate endSessionUrl'); - - const clientInstance = { endSessionUrl: sinon.stub().throws(errorThrown) }; - const Client = sinon.stub().returns(clientInstance); - - sinon.stub(Issuer, 'discover').resolves({ Client }); + openIdClient.buildEndSessionUrl.throws(errorThrown); const oidcAuthenticationService = new OidcAuthenticationService(settings.oidcExampleNet, { sessionTemporaryStorage, + openIdClient, }); - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); // when const error = await catchErr( @@ -289,41 +296,36 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { const expiresIn = Symbol(60); const idToken = Symbol('idToken'); const refreshToken = Symbol('refreshToken'); - const code = Symbol('AUTHORIZATION_CODE'); - const state = Symbol('STATE'); - const nonce = Symbol('NONCE'); + const code = 'AUTHORIZATION_CODE'; + const state = 'STATE'; + const nonce = 'NONCE'; const oidcAuthenticationSessionContent = new AuthenticationSessionContent({ idToken, accessToken, expiresIn, refreshToken, }); - const tokenSet = { + openIdClient.authorizationCodeGrant.resolves({ access_token: accessToken, expires_in: expiresIn, id_token: idToken, refresh_token: refreshToken, - }; - const clientInstance = { callback: sinon.stub().resolves(tokenSet) }; - const Client = sinon.stub().returns(clientInstance); - - sinon.stub(Issuer, 'discover').resolves({ Client }); - - const oidcAuthenticationService = new OidcAuthenticationService({ - clientSecret, - clientId, - redirectUri, - openidConfigurationUrl, - tokenUrl, }); - await oidcAuthenticationService.createClient(); + + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientSecret, + clientId, + redirectUri, + openidConfigurationUrl, + tokenUrl, + }, + { openIdClient }, + ); + await oidcAuthenticationService.initializeClientConfig(); // when - const result = await oidcAuthenticationService.exchangeCodeForTokens({ - code, - nonce, - state, - }); + const result = await oidcAuthenticationService.exchangeCodeForTokens({ code, nonce, state }); // then expect(result).to.be.an.instanceOf(AuthenticationSessionContent); @@ -332,11 +334,11 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { context('when OpenId Client callback fails', function () { it('throws an error and logs a message in datadog', async function () { - const clientId = Symbol('clientId'); - const clientSecret = Symbol('clientSecret'); - const identityProvider = Symbol('identityProvider'); - const redirectUri = Symbol('redirectUri'); - const openidConfigurationUrl = Symbol('openidConfigurationUrl'); + const clientId = 'clientId'; + const clientSecret = 'clientSecret'; + const identityProvider = 'identityProvider'; + const redirectUri = 'https://example.org/please-redirect-to-me'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; const code = 'code'; const nonce = 'nonce'; const iss = 'https://issuer.url'; @@ -347,20 +349,20 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { errorThrown.error_uri = '/oauth2/token'; errorThrown.response = 'api call response here'; - const clientInstance = { callback: sinon.stub().rejects(errorThrown) }; - const Client = sinon.stub().returns(clientInstance); + openIdClient.authorizationCodeGrant.rejects(errorThrown); - sinon.stub(Issuer, 'discover').resolves({ Client }); - - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - identityProvider, - redirectUri, - openidConfigurationUrl, - organizationName: 'Oidc Example', - }); - await oidcAuthenticationService.createClient(); + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + organizationName: 'Oidc Example', + }, + { openIdClient }, + ); + await oidcAuthenticationService.initializeClientConfig(); // when const error = await catchErr( @@ -399,19 +401,25 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { // given const clientId = 'OIDC_CLIENT_ID'; const clientSecret = 'OIDC_CLIENT_SECRET'; + const identityProvider = 'identityProvider'; const redirectUri = 'https://example.org/please-redirect-to-me'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - redirectUri, - }); + openIdClient.buildAuthorizationUrl.returns(''); - const clientInstance = { authorizationUrl: sinon.stub().returns('') }; - const Client = sinon.stub().returns(clientInstance); + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + organizationName: 'Oidc Example', + }, + { openIdClient }, + ); - sinon.stub(Issuer, 'discover').resolves({ Client }); - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); // when const { nonce, state } = oidcAuthenticationService.getAuthorizationUrl(); @@ -420,7 +428,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { expect(nonce).to.match(uuidV4Regex); expect(state).to.match(uuidV4Regex); - expect(clientInstance.authorizationUrl).to.have.been.calledWithExactly({ + expect(openIdClient.buildAuthorizationUrl).to.have.been.calledWithExactly(MOCK_OIDC_PROVIDER_CONFIG, { nonce, redirect_uri: 'https://example.org/please-redirect-to-me', scope: 'openid profile', @@ -431,27 +439,27 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { context('when generating the authorization url fails', function () { it('throws an error and logs a message in datadog', async function () { // given - const clientId = Symbol('clientId'); - const clientSecret = Symbol('clientSecret'); - const identityProvider = Symbol('identityProvider'); - const redirectUri = Symbol('redirectUri'); - const openidConfigurationUrl = Symbol('openidConfigurationUrl'); + const clientId = 'clientId'; + const clientSecret = 'clientSecret'; + const identityProvider = 'identityProvider'; + const redirectUri = 'https://example.org/please-redirect-to-me'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; const errorThrown = new Error('Fails to generate authorization url'); - const clientInstance = { authorizationUrl: sinon.stub().throws(errorThrown) }; - const Client = sinon.stub().returns(clientInstance); + openIdClient.buildAuthorizationUrl.throws(errorThrown); - sinon.stub(Issuer, 'discover').resolves({ Client }); - - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - identityProvider, - redirectUri, - openidConfigurationUrl, - organizationName: 'Oidc Example', - }); - await oidcAuthenticationService.createClient(); + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + organizationName: 'Oidc Example', + }, + { openIdClient }, + ); + await oidcAuthenticationService.initializeClientConfig(); // when const error = catchErrSync(oidcAuthenticationService.getAuthorizationUrl, oidcAuthenticationService)(); @@ -484,7 +492,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { 'secret', ); - const oidcAuthenticationService = new OidcAuthenticationService({}); + const oidcAuthenticationService = new OidcAuthenticationService({}, { openIdClient }); // when const result = await oidcAuthenticationService.getUserInfo({ @@ -514,7 +522,10 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { 'secret', ); - const oidcAuthenticationService = new OidcAuthenticationService({ claimsToStore: 'employeeNumber' }); + const oidcAuthenticationService = new OidcAuthenticationService( + { claimsToStore: 'employeeNumber' }, + { openIdClient }, + ); // when const result = await oidcAuthenticationService.getUserInfo({ @@ -550,7 +561,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { lastName: ['usual_name'], externalIdentityId: ['sub'], }; - const oidcAuthenticationService = new OidcAuthenticationService({ claimMapping }); + const oidcAuthenticationService = new OidcAuthenticationService({ claimMapping }, { openIdClient }); // when const result = await oidcAuthenticationService.getUserInfo({ @@ -586,10 +597,10 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { lastName: ['usual_name'], externalIdentityId: ['sub'], }; - const oidcAuthenticationService = new OidcAuthenticationService({ - claimMapping, - claimsToStore: 'employeeNumber', - }); + const oidcAuthenticationService = new OidcAuthenticationService( + { claimMapping, claimsToStore: 'employeeNumber' }, + { openIdClient }, + ); // when const result = await oidcAuthenticationService.getUserInfo({ @@ -618,18 +629,16 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { 'secret', ); - const oidcAuthenticationService = new OidcAuthenticationService({}); + const oidcAuthenticationService = new OidcAuthenticationService({}, { openIdClient }); sinon.stub(oidcAuthenticationService, '_getUserInfoFromEndpoint').resolves({}); // when - await oidcAuthenticationService.getUserInfo({ - idToken, - accessToken: 'accessToken', - }); + await oidcAuthenticationService.getUserInfo({ idToken, accessToken: 'accessToken' }); // then expect(oidcAuthenticationService._getUserInfoFromEndpoint).to.have.been.calledOnceWithExactly({ accessToken: 'accessToken', + expectedSubject: 'sub-id', }); }); }); @@ -647,7 +656,10 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { 'secret', ); - const oidcAuthenticationService = new OidcAuthenticationService({ claimsToStore: 'employeeNumber' }); + const oidcAuthenticationService = new OidcAuthenticationService( + { claimsToStore: 'employeeNumber' }, + { openIdClient }, + ); sinon.stub(oidcAuthenticationService, '_getUserInfoFromEndpoint').resolves({}); // when @@ -659,6 +671,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { // then expect(oidcAuthenticationService._getUserInfoFromEndpoint).to.have.been.calledOnceWithExactly({ accessToken: 'accessToken', + expectedSubject: 'sub-id', }); }); }); @@ -669,33 +682,44 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { // given const clientId = 'OIDC_CLIENT_ID'; const clientSecret = 'OIDC_CLIENT_SECRET'; + const identityProvider = 'identityProvider'; const redirectUri = 'https://example.org/please-redirect-to-me'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - redirectUri, + openIdClient.fetchUserInfo.returns({ + sub: 'sub-id', + given_name: 'givenName', + family_name: 'familyName', }); - const clientInstance = { - userinfo: sinon.stub().resolves({ - sub: 'sub-id', - given_name: 'givenName', - family_name: 'familyName', - }), - }; - const Client = sinon.stub().returns(clientInstance); + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + organizationName: 'Oidc Example', + }, + { openIdClient }, + ); - sinon.stub(Issuer, 'discover').resolves({ Client }); - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); const accessToken = 'thisIsSerializedInformation'; // when - const pickedUserInfo = await oidcAuthenticationService._getUserInfoFromEndpoint({ accessToken }); + const pickedUserInfo = await oidcAuthenticationService._getUserInfoFromEndpoint({ + accessToken, + expectedSubject: 'sub-id', + }); // then - expect(clientInstance.userinfo).to.have.been.calledOnceWithExactly(accessToken); + expect(openIdClient.fetchUserInfo).to.have.been.calledOnceWithExactly( + MOCK_OIDC_PROVIDER_CONFIG, + accessToken, + 'sub-id', + ); expect(pickedUserInfo).to.deep.equal({ sub: 'sub-id', given_name: 'givenName', @@ -705,27 +729,27 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { context('when OpenId Client userinfo fails', function () { it('throws an error and logs a message in datadog', async function () { - const clientId = Symbol('clientId'); - const clientSecret = Symbol('clientSecret'); - const identityProvider = Symbol('identityProvider'); - const redirectUri = Symbol('redirectUri'); - const openidConfigurationUrl = Symbol('openidConfigurationUrl'); + const clientId = 'OIDC_CLIENT_ID'; + const clientSecret = 'OIDC_CLIENT_SECRET'; + const identityProvider = 'identityProvider'; + const redirectUri = 'https://example.org/please-redirect-to-me'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; const errorThrown = new Error('Fails to get user info'); - const clientInstance = { userinfo: sinon.stub().rejects(errorThrown) }; - const Client = sinon.stub().returns(clientInstance); - - sinon.stub(Issuer, 'discover').resolves({ Client }); + openIdClient.fetchUserInfo.rejects(errorThrown); - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - identityProvider, - redirectUri, - openidConfigurationUrl, - organizationName: 'Oidc Example', - }); - await oidcAuthenticationService.createClient(); + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + organizationName: 'Oidc Example', + }, + { openIdClient }, + ); + await oidcAuthenticationService.initializeClientConfig(); // when const error = await catchErr(oidcAuthenticationService._getUserInfoFromEndpoint, oidcAuthenticationService)({}); @@ -749,38 +773,38 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { // given const clientId = 'OIDC_CLIENT_ID'; const clientSecret = 'OIDC_CLIENT_SECRET'; + const identityProvider = 'identityProvider'; const redirectUri = 'https://example.org/please-redirect-to-me'; - const organizationName = 'Example'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - redirectUri, - organizationName, + openIdClient.fetchUserInfo.returns({ + sub: 'sub-id', + given_name: 'givenName', + family_name: undefined, }); - const clientInstance = { - userinfo: sinon.stub().resolves({ - sub: 'sub-id', - given_name: 'givenName', - family_name: undefined, - }), - }; - const Client = sinon.stub().returns(clientInstance); + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + organizationName: 'Oidc Example', + }, + { openIdClient }, + ); - sinon.stub(Issuer, 'discover').resolves({ Client }); - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); const accessToken = 'thisIsSerializedInformation'; - const errorMessage = `Un ou des champs obligatoires (family_name) n'ont pas été renvoyés par votre fournisseur d'identité Example.`; + const errorMessage = `Un ou des champs obligatoires (family_name) n'ont pas été renvoyés par votre fournisseur d'identité Oidc Example.`; // when const error = await catchErr( oidcAuthenticationService._getUserInfoFromEndpoint, oidcAuthenticationService, - )({ - accessToken, - }); + )({ accessToken, expectedSubject: 'sub-id' }); // then expect(error).to.be.instanceOf(OidcMissingFieldsError); @@ -808,40 +832,39 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { // given const clientId = 'OIDC_CLIENT_ID'; const clientSecret = 'OIDC_CLIENT_SECRET'; + const identityProvider = 'identityProvider'; const redirectUri = 'https://example.org/please-redirect-to-me'; - const organizationName = 'Example'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; - const oidcAuthenticationService = new OidcAuthenticationService({ - claimsToStore: 'population', - clientId, - clientSecret, - redirectUri, - organizationName, + openIdClient.fetchUserInfo.returns({ + sub: 'sub-id', + given_name: 'givenName', + family_name: 'familyName', + population: '', }); - const clientInstance = { - userinfo: sinon.stub().resolves({ - sub: 'sub-id', - given_name: 'givenName', - family_name: 'familyName', - population: '', - }), - }; - const Client = sinon.stub().returns(clientInstance); - - sinon.stub(Issuer, 'discover').resolves({ Client }); - await oidcAuthenticationService.createClient(); + const oidcAuthenticationService = new OidcAuthenticationService( + { + claimsToStore: 'population', + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + organizationName: 'Oidc Example', + }, + { openIdClient }, + ); + await oidcAuthenticationService.initializeClientConfig(); const accessToken = 'thisIsSerializedInformation'; - const errorMessage = `Un ou des champs obligatoires (population) n'ont pas été renvoyés par votre fournisseur d'identité Example.`; + const errorMessage = `Un ou des champs obligatoires (population) n'ont pas été renvoyés par votre fournisseur d'identité Oidc Example.`; // when const error = await catchErr( oidcAuthenticationService._getUserInfoFromEndpoint, oidcAuthenticationService, - )({ - accessToken, - }); + )({ accessToken, expectedSubject: 'sub-id' }); // then expect(error).to.be.instanceOf(OidcMissingFieldsError); @@ -899,7 +922,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { externalIdentifier: externalIdentityId, userId, }); - const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider }); + const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider }, { openIdClient }); // when const result = await oidcAuthenticationService.createUserAccount({ @@ -935,7 +958,7 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { externalIdentifier: externalIdentityId, userId, }); - const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider }); + const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider }, { openIdClient }); // when await oidcAuthenticationService.createUserAccount({ @@ -974,7 +997,10 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { externalIdentifier: externalIdentityId, userId, }); - const oidcAuthenticationService = new OidcAuthenticationService({ identityProvider, claimsToStore }); + const oidcAuthenticationService = new OidcAuthenticationService( + { identityProvider, claimsToStore }, + { openIdClient }, + ); // when await oidcAuthenticationService.createUserAccount({ @@ -993,70 +1019,62 @@ describe('Unit | Domain | Services | oidc-authentication-service', function () { }); }); - describe('#createClient', function () { + describe('#initializeClientConfig', function () { it('creates an openid client', async function () { // given - const clientId = Symbol('clientId'); - const clientSecret = Symbol('clientSecret'); - const identityProvider = Symbol('identityProvider'); - const redirectUri = Symbol('redirectUri'); - const openidConfigurationUrl = Symbol('openidConfigurationUrl'); - const Client = sinon.spy(); - - sinon.stub(Issuer, 'discover').resolves({ Client }); - - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - identityProvider, - redirectUri, - openidConfigurationUrl, - }); + const clientId = 'clientId'; + const clientSecret = 'clientSecret'; + const identityProvider = 'identityProvider'; + const redirectUri = 'https://example.org/please-redirect-to-me'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; + + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + }, + { openIdClient }, + ); // when - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); // then - expect(Issuer.discover).to.have.been.calledWithExactly(openidConfigurationUrl); - expect(Client).to.have.been.calledWithNew; - expect(Client).to.have.been.calledWithExactly({ - client_id: clientId, + expect(openIdClient.discovery).to.have.been.calledWithExactly(new URL(openidConfigurationUrl), clientId, { client_secret: clientSecret, - redirect_uris: [redirectUri], }); }); it('creates an openid client with extra meatadata', async function () { // given - const clientId = Symbol('clientId'); - const clientSecret = Symbol('clientSecret'); - const identityProvider = Symbol('identityProvider'); - const redirectUri = Symbol('redirectUri'); - const openidConfigurationUrl = Symbol('openidConfigurationUrl'); + const clientId = 'clientId'; + const clientSecret = 'clientSecret'; + const identityProvider = 'identityProvider'; + const redirectUri = 'https://example.org/please-redirect-to-me'; + const openidConfigurationUrl = 'https://example.org/oidc-provider-configuration'; const openidClientExtraMetadata = { token_endpoint_auth_method: 'client_secret_post' }; - const Client = sinon.spy(); - sinon.stub(Issuer, 'discover').resolves({ Client }); - - const oidcAuthenticationService = new OidcAuthenticationService({ - clientId, - clientSecret, - identityProvider, - redirectUri, - openidConfigurationUrl, - openidClientExtraMetadata, - }); + const oidcAuthenticationService = new OidcAuthenticationService( + { + clientId, + clientSecret, + identityProvider, + redirectUri, + openidConfigurationUrl, + openidClientExtraMetadata, + }, + { openIdClient }, + ); // when - await oidcAuthenticationService.createClient(); + await oidcAuthenticationService.initializeClientConfig(); // then - expect(Issuer.discover).to.have.been.calledWithExactly(openidConfigurationUrl); - expect(Client).to.have.been.calledWithNew; - expect(Client).to.have.been.calledWithExactly({ - client_id: clientId, + expect(openIdClient.discovery).to.have.been.calledWithExactly(new URL(openidConfigurationUrl), clientId, { client_secret: clientSecret, - redirect_uris: [redirectUri], token_endpoint_auth_method: 'client_secret_post', }); }); diff --git a/api/tests/identity-access-management/unit/domain/services/pix-authentication-service_test.js b/api/tests/identity-access-management/unit/domain/services/pix-authentication-service.test.js similarity index 100% rename from api/tests/identity-access-management/unit/domain/services/pix-authentication-service_test.js rename to api/tests/identity-access-management/unit/domain/services/pix-authentication-service.test.js diff --git a/api/tests/identity-access-management/unit/domain/services/pole-emploi-oidc-authentication-service_test.js b/api/tests/identity-access-management/unit/domain/services/pole-emploi-oidc-authentication-service.test.js similarity index 53% rename from api/tests/identity-access-management/unit/domain/services/pole-emploi-oidc-authentication-service_test.js rename to api/tests/identity-access-management/unit/domain/services/pole-emploi-oidc-authentication-service.test.js index f1e33708ab8..100e8c16b05 100644 --- a/api/tests/identity-access-management/unit/domain/services/pole-emploi-oidc-authentication-service_test.js +++ b/api/tests/identity-access-management/unit/domain/services/pole-emploi-oidc-authentication-service.test.js @@ -1,24 +1,32 @@ -import { Issuer } from 'openid-client'; - import { AuthenticationMethod } from '../../../../../src/identity-access-management/domain/models/AuthenticationMethod.js'; import { PoleEmploiOidcAuthenticationService } from '../../../../../src/identity-access-management/domain/services/pole-emploi-oidc-authentication-service.js'; import { config as settings } from '../../../../../src/shared/config.js'; import { expect, sinon } from '../../../../test-helper.js'; +import { createOpenIdClientMock } from '../../../../tooling/openid-client/openid-client-mocks.js'; describe('Unit | Identity Access Management | Domain | Services | pole-emploi-oidc-authentication-service', function () { + let openIdClient; + + beforeEach(function () { + openIdClient = createOpenIdClientMock(); + }); + describe('#constructor', function () { describe('when additionalRequiredProperties is not defined', function () { it('is not ready', async function () { // when - const oidcAuthenticationService = new PoleEmploiOidcAuthenticationService({ - ...settings.oidcExampleNet, - openidClientExtraMetadata: { token_endpoint_auth_method: 'client_secret_post' }, - identityProvider: 'POLE_EMPLOI', - organizationName: 'France Travail', - shouldCloseSession: true, - slug: 'pole-emploi', - source: 'pole_emploi_connect', - }); + const oidcAuthenticationService = new PoleEmploiOidcAuthenticationService( + { + ...settings.oidcExampleNet, + openidClientExtraMetadata: { token_endpoint_auth_method: 'client_secret_post' }, + identityProvider: 'POLE_EMPLOI', + organizationName: 'France Travail', + shouldCloseSession: true, + slug: 'pole-emploi', + source: 'pole_emploi_connect', + }, + { openIdClient }, + ); // then expect(oidcAuthenticationService.isReady).to.be.false; @@ -26,39 +34,37 @@ describe('Unit | Identity Access Management | Domain | Services | pole-emploi-oi }); }); - describe('#createClient', function () { - it('creates an openid client with extra metadata', async function () { + describe('#initializeClientConfig', function () { + it('creates an openid client config with extra metadata', async function () { // given - const Client = sinon.spy(); - - sinon.stub(Issuer, 'discover').resolves({ Client }); sinon.stub(settings, 'poleEmploi').value(settings.oidcExampleNet); - const poleEmploiOidcAuthenticationService = new PoleEmploiOidcAuthenticationService({ - ...settings.oidcExampleNet, - additionalRequiredProperties: { logoutUrl: '', afterLogoutUrl: '', sendingUrl: '' }, - openidClientExtraMetadata: { token_endpoint_auth_method: 'client_secret_post' }, - identityProvider: 'POLE_EMPLOI', - organizationName: 'France Travail', - shouldCloseSession: true, - slug: 'pole-emploi', - source: 'pole_emploi_connect', - }); + const poleEmploiOidcAuthenticationService = new PoleEmploiOidcAuthenticationService( + { + ...settings.oidcExampleNet, + additionalRequiredProperties: { logoutUrl: '', afterLogoutUrl: '', sendingUrl: '' }, + openidClientExtraMetadata: { token_endpoint_auth_method: 'client_secret_post' }, + identityProvider: 'POLE_EMPLOI', + organizationName: 'France Travail', + shouldCloseSession: true, + slug: 'pole-emploi', + source: 'pole_emploi_connect', + }, + { openIdClient }, + ); // when - await poleEmploiOidcAuthenticationService.createClient(); + await poleEmploiOidcAuthenticationService.initializeClientConfig(); // then - expect(Issuer.discover).to.have.been.calledWithExactly( - 'https://oidc.example.net/.well-known/openid-configuration', + expect(openIdClient.discovery).to.have.been.calledWithExactly( + new URL('https://oidc.example.net/.well-known/openid-configuration'), + 'client', + { + client_secret: 'secret', + token_endpoint_auth_method: 'client_secret_post', + }, ); - expect(Client).to.have.been.calledWithNew; - expect(Client).to.have.been.calledWithExactly({ - client_id: 'client', - client_secret: 'secret', - redirect_uris: ['https://app.dev.pix.local/connexion/oidc-example-net'], - token_endpoint_auth_method: 'client_secret_post', - }); }); }); @@ -71,16 +77,19 @@ describe('Unit | Identity Access Management | Domain | Services | pole-emploi-oi expiresIn: 10, refreshToken: 'refreshToken', }; - const poleEmploiOidcAuthenticationService = new PoleEmploiOidcAuthenticationService({ - ...settings.oidcExampleNet, - additionalRequiredProperties: { logoutUrl: '', afterLogoutUrl: '', sendingUrl: '' }, - openidClientExtraMetadata: { token_endpoint_auth_method: 'client_secret_post' }, - identityProvider: 'POLE_EMPLOI', - organizationName: 'France Travail', - shouldCloseSession: true, - slug: 'pole-emploi', - source: 'pole_emploi_connect', - }); + const poleEmploiOidcAuthenticationService = new PoleEmploiOidcAuthenticationService( + { + ...settings.oidcExampleNet, + additionalRequiredProperties: { logoutUrl: '', afterLogoutUrl: '', sendingUrl: '' }, + openidClientExtraMetadata: { token_endpoint_auth_method: 'client_secret_post' }, + identityProvider: 'POLE_EMPLOI', + organizationName: 'France Travail', + shouldCloseSession: true, + slug: 'pole-emploi', + source: 'pole_emploi_connect', + }, + { openIdClient }, + ); // when const result = poleEmploiOidcAuthenticationService.createAuthenticationComplement({ sessionContent }); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index 18755c44f22..bc99a2ae94f 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -21,6 +21,7 @@ import { knex as datamartKnex } from '../datamart/knex-database-connection.js'; import { knex as datawarehouseKnex } from '../datawarehouse/knex-database-connection.js'; import { DatabaseBuilder } from '../db/database-builder/database-builder.js'; import { disconnect, knex } from '../db/knex-database-connection.js'; +import { createServer } from '../server.js'; import { createMaddoServer } from '../server.maddo.js'; import { PIX_ADMIN } from '../src/authorization/domain/constants.js'; import * as tutorialRepository from '../src/devcomp/infrastructure/repositories/tutorial-repository.js'; @@ -42,7 +43,6 @@ import * as domainBuilder from './tooling/domain-builder/factory/index.js'; import { jobChai } from './tooling/jobs/expect-job.js'; import { buildLearningContent as learningContentBuilder } from './tooling/learning-content-builder/index.js'; import { increaseCurrentTestTimeout } from './tooling/mocha-tools.js'; -import { createServerWithTestOidcProvider } from './tooling/server/hapi-server-with-test-oidc-provider.js'; import { HttpTestServer } from './tooling/server/http-test-server.js'; import { createTempFile, removeTempFile } from './tooling/temporary-file.js'; @@ -334,7 +334,7 @@ export { catchErr, catchErrSync, createMaddoServer, - createServerWithTestOidcProvider as createServer, + createServer, createTempFile, databaseBuilder, datamartBuilder, diff --git a/api/tests/tooling/server/hapi-server-with-test-oidc-provider.js b/api/tests/tooling/openid-client/openid-client-mocks.js similarity index 50% rename from api/tests/tooling/server/hapi-server-with-test-oidc-provider.js rename to api/tests/tooling/openid-client/openid-client-mocks.js index e527ee5b32a..831238f485b 100644 --- a/api/tests/tooling/server/hapi-server-with-test-oidc-provider.js +++ b/api/tests/tooling/openid-client/openid-client-mocks.js @@ -1,9 +1,10 @@ -import nock from 'nock'; - import { oidcAuthenticationServiceRegistry } from '../../../lib/domain/usecases/index.js'; -import { createServer } from '../../../server.js'; import { OidcAuthenticationService } from '../../../src/identity-access-management/domain/services/oidc-authentication-service.js'; +import { sinon } from '../../test-helper.js'; +const clientId = 'client'; +const redirectUri = 'https://app.dev.pix.org/connexion/oidc-example-net'; +const scope = 'openid profile'; const openIdConfigurationResponse = { token_endpoint: 'https://oidc.example.net/ea5ac20c-5076-4806-860a-b0aeb01645d4/oauth2/v2.0/token', token_endpoint_auth_methods_supported: ['client_secret_post', 'private_key_jwt', 'client_secret_basic'], @@ -40,28 +41,49 @@ const openIdConfigurationResponse = { ], }; -async function createServerWithTestOidcProvider() { - nock('https://oidc.example.net').get('/.well-known/openid-configuration').reply(200, openIdConfigurationResponse); +function createOpenIdClientMock(oidcProviderConfig = Symbol('oidcProviderConfig')) { + return { + discovery: sinon.stub().resolves(oidcProviderConfig), + authorizationCodeGrant: sinon.stub(), + buildAuthorizationUrl: sinon.stub(), + buildEndSessionUrl: sinon.stub(), + fetchUserInfo: sinon.stub(), + }; +} + +async function createMockedTestOidcProvider() { + oidcAuthenticationServiceRegistry.testOnly_reset(); + + const openIdClientMock = createOpenIdClientMock(openIdConfigurationResponse); + + const authorizationUrl = `${openIdConfigurationResponse.authorization_endpoint}?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=state&nonce=nonce`; + openIdClientMock.buildAuthorizationUrl.returns(authorizationUrl); + + const endSessionUrl = `${openIdConfigurationResponse.end_session_endpoint}?client_id=${clientId}`; + openIdClientMock.buildEndSessionUrl.returns(endSessionUrl); await oidcAuthenticationServiceRegistry.loadOidcProviderServices([ - new OidcAuthenticationService({ - accessTokenLifespanMs: 60000, - clientId: 'client', - clientSecret: 'secret', - enabled: true, - enabledForPixAdmin: true, - configKey: 'oidcExampleNet', - shouldCloseSession: true, - identityProvider: 'OIDC_EXAMPLE_NET', - openidConfigurationUrl: 'https://oidc.example.net/.well-known/openid-configuration', - organizationName: 'OIDC Example', - redirectUri: 'https://app.dev.pix.org/connexion/oidc-example-net', - slug: 'oidc-example-net', - source: 'oidcexamplenet', - }), + new OidcAuthenticationService( + { + accessTokenLifespanMs: 60000, + clientId, + clientSecret: 'secret', + enabled: true, + enabledForPixAdmin: true, + configKey: 'oidcExampleNet', + shouldCloseSession: true, + identityProvider: 'OIDC_EXAMPLE_NET', + openidConfigurationUrl: 'https://oidc.example.net/.well-known/openid-configuration', + organizationName: 'OIDC Example', + redirectUri, + slug: 'oidc-example-net', + source: 'oidcexamplenet', + }, + { openIdClient: openIdClientMock }, + ), ]); - return createServer(); + return openIdClientMock; } -export { createServerWithTestOidcProvider }; +export { createMockedTestOidcProvider, createOpenIdClientMock };