From 9dca840e07ac616123a61ea2179f77a09b866510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Latzarus?= Date: Mon, 5 Feb 2024 17:39:06 +0100 Subject: [PATCH 1/4] refactor(api): use object for arguments Co-authored-by: Andreia Pena --- .../authentication/oidc/oidc-controller.js | 25 +++++++++++-------- .../authentication-service-registry.js | 2 +- .../oidc/oidc-controller_test.js | 12 ++++----- .../authentication-service-registry_test.js | 4 +-- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/api/lib/application/authentication/oidc/oidc-controller.js b/api/lib/application/authentication/oidc/oidc-controller.js index e70091b79dc..4915732b566 100644 --- a/api/lib/application/authentication/oidc/oidc-controller.js +++ b/api/lib/application/authentication/oidc/oidc-controller.js @@ -24,8 +24,9 @@ const getRedirectLogoutUrl = async function ( ) { const userId = request.auth.credentials.userId; const { identity_provider: identityProvider, logout_url_uuid: logoutUrlUUID } = request.query; - const oidcAuthenticationService = - dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode(identityProvider); + const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({ + identityProviderCode: identityProvider, + }); const redirectLogoutUrl = await oidcAuthenticationService.getRedirectLogoutUrl({ userId, logoutUrlUUID, @@ -61,8 +62,9 @@ const reconcileUser = async function ( }, ) { const { identityProvider, authenticationKey } = request.deserializedPayload; - const oidcAuthenticationService = - dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode(identityProvider); + const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({ + identityProviderCode: identityProvider, + }); const result = await usecases.reconcileOidcUser({ authenticationKey, @@ -80,8 +82,9 @@ const getAuthenticationUrl = async function ( }, ) { const { identity_provider: identityProvider } = request.query; - const oidcAuthenticationService = - dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode(identityProvider); + const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({ + identityProviderCode: identityProvider, + }); const { nonce, state, ...payload } = oidcAuthenticationService.getAuthenticationUrl({ redirectUri: request.query['redirect_uri'], }); @@ -105,8 +108,9 @@ const authenticateUser = async function ( // eslint-disable-next-line no-unused-vars const nonce = request.yar.get('nonce', true); - const oidcAuthenticationService = - dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode(identityProvider); + const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({ + identityProviderCode: identityProvider, + }); const result = await usecases.authenticateOidcUser({ code, @@ -137,8 +141,9 @@ const createUser = async function ( const { identityProvider, authenticationKey } = request.deserializedPayload; const localeFromCookie = request.state?.locale; - const oidcAuthenticationService = - dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode(identityProvider); + const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({ + identityProviderCode: identityProvider, + }); const { accessToken, logoutUrlUUID } = await usecases.createOidcUser({ authenticationKey, identityProvider, diff --git a/api/lib/domain/services/authentication/authentication-service-registry.js b/api/lib/domain/services/authentication/authentication-service-registry.js index b997e05ee34..9d2b7f52cb7 100644 --- a/api/lib/domain/services/authentication/authentication-service-registry.js +++ b/api/lib/domain/services/authentication/authentication-service-registry.js @@ -17,7 +17,7 @@ class OidcAuthenticationServiceRegistry { return this.#readyOidcProviderServicesForPixAdmin; } - getOidcProviderServiceByCode(identityProviderCode) { + getOidcProviderServiceByCode({ identityProviderCode }) { const oidcProviderService = this.#readyOidcProviderServices.find( (service) => identityProviderCode === service.code, ); diff --git a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js index b044d8ab555..d4d698b372a 100644 --- a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js +++ b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js @@ -115,7 +115,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }; authenticationServiceRegistryStub.getOidcProviderServiceByCode - .withArgs(identityProvider) + .withArgs({ identityProviderCode: identityProvider }) .returns(oidcAuthenticationService); const dependencies = { @@ -150,7 +150,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }; authenticationServiceRegistryStub.getOidcProviderServiceByCode - .withArgs(identityProvider) + .withArgs({ identityProviderCode: identityProvider }) .returns(oidcAuthenticationService); const dependencies = { @@ -203,7 +203,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }; authenticationServiceRegistryStub.getOidcProviderServiceByCode - .withArgs(identityProvider) + .withArgs({ identityProviderCode: identityProvider, audience: undefined }) .returns(oidcAuthenticationService); const dependencies = { @@ -240,7 +240,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }; authenticationServiceRegistryStub.getOidcProviderServiceByCode - .withArgs(identityProvider) + .withArgs({ identityProviderCode: identityProvider }) .returns(oidcAuthenticationService); const dependencies = { @@ -271,7 +271,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }; authenticationServiceRegistryStub.getOidcProviderServiceByCode - .withArgs(identityProvider) + .withArgs({ identityProviderCode: identityProvider }) .returns(oidcAuthenticationService); const dependencies = { @@ -309,7 +309,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }; authenticationServiceRegistryStub.getOidcProviderServiceByCode - .withArgs(identityProvider) + .withArgs({ identityProviderCode: identityProvider }) .returns(oidcAuthenticationService); const dependencies = { diff --git a/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js b/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js index 7a5f3de5bf8..462e63280e8 100644 --- a/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js +++ b/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js @@ -87,7 +87,7 @@ describe('Unit | Domain | Services | authentication registry', function () { oidcAuthenticationServiceRegistry.loadOidcProviderServices([firstOidcProviderService, secondOidcProviderService]); // when - const service = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode(identityProviderCode); + const service = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({ identityProviderCode }); // then expect(service.code).to.equal('FIRST'); @@ -110,7 +110,7 @@ describe('Unit | Domain | Services | authentication registry', function () { const error = catchErrSync( oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode, oidcAuthenticationServiceRegistry, - )(identityProviderCode); + )({ identityProviderCode }); // then expect(error).to.be.an.instanceOf(InvalidIdentityProviderError); From 748fa70188042a545703f732f763f126658f2427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Latzarus?= Date: Mon, 5 Feb 2024 17:41:56 +0100 Subject: [PATCH 2/4] feat(api): allow to filter oidc provider by audience in authentification service registry Co-authored-by: Andreia Pena --- .../authentication-service-registry.js | 8 +-- .../oidc/oidc-controller_test.js | 2 +- .../authentication-service-registry_test.js | 64 ++++++++++++++----- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/api/lib/domain/services/authentication/authentication-service-registry.js b/api/lib/domain/services/authentication/authentication-service-registry.js index 9d2b7f52cb7..120e3d660a9 100644 --- a/api/lib/domain/services/authentication/authentication-service-registry.js +++ b/api/lib/domain/services/authentication/authentication-service-registry.js @@ -17,10 +17,10 @@ class OidcAuthenticationServiceRegistry { return this.#readyOidcProviderServicesForPixAdmin; } - getOidcProviderServiceByCode({ identityProviderCode }) { - const oidcProviderService = this.#readyOidcProviderServices.find( - (service) => identityProviderCode === service.code, - ); + getOidcProviderServiceByCode({ identityProviderCode, audience = 'app' }) { + const services = + audience === 'admin' ? this.#readyOidcProviderServicesForPixAdmin : this.#readyOidcProviderServices; + const oidcProviderService = services.find((service) => identityProviderCode === service.code); if (!oidcProviderService) { throw new InvalidIdentityProviderError(identityProviderCode); diff --git a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js index d4d698b372a..2f32060d095 100644 --- a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js +++ b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js @@ -150,7 +150,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }; authenticationServiceRegistryStub.getOidcProviderServiceByCode - .withArgs({ identityProviderCode: identityProvider }) + .withArgs({ identityProviderCode: identityProvider, audience: undefined }) .returns(oidcAuthenticationService); const dependencies = { diff --git a/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js b/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js index 462e63280e8..ac81499f1fd 100644 --- a/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js +++ b/api/tests/unit/domain/services/authentication/authentication-service-registry_test.js @@ -73,24 +73,54 @@ describe('Unit | Domain | Services | authentication registry', function () { }); describe('#getOidcProviderServiceByCode', function () { - it('returns a ready OIDC Provider service', function () { - // given - const identityProviderCode = 'FIRST'; - const firstOidcProviderService = { - code: identityProviderCode, - isReady: true, - }; - const secondOidcProviderService = { - code: 'SECOND', - }; - - oidcAuthenticationServiceRegistry.loadOidcProviderServices([firstOidcProviderService, secondOidcProviderService]); - - // when - const service = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({ identityProviderCode }); + describe('when the audience is admin', function () { + it('returns a ready OIDC provider for Pix Admin', function () { + // given + const oidcProviderForPixApp = { + code: 'PROVIDER_FOR_APP', + isReady: true, + }; + const oidcProviderForPixAdmin = { + code: 'PROVIDER_FOR_ADMIN', + isReadyForPixAdmin: true, + }; + + oidcAuthenticationServiceRegistry.loadOidcProviderServices([oidcProviderForPixApp, oidcProviderForPixAdmin]); + + // when + const service = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({ + identityProviderCode: 'PROVIDER_FOR_ADMIN', + audience: 'admin', + }); + + // then + expect(service.code).to.equal('PROVIDER_FOR_ADMIN'); + }); + }); - // then - expect(service.code).to.equal('FIRST'); + describe('when audience is not provided', function () { + it('returns a ready OIDC Provider for Pix App', function () { + // given + const identityProviderCode = 'FIRST'; + const firstOidcProviderService = { + code: identityProviderCode, + isReady: true, + }; + const secondOidcProviderService = { + code: 'SECOND', + }; + + oidcAuthenticationServiceRegistry.loadOidcProviderServices([ + firstOidcProviderService, + secondOidcProviderService, + ]); + + // when + const service = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({ identityProviderCode }); + + // then + expect(service.code).to.equal('FIRST'); + }); }); it('throws an error when identity provider is not supported', function () { From 99a469539cff79ee0ec5a6a96997f0a87d3d8cf2 Mon Sep 17 00:00:00 2001 From: Andreia Pena Date: Tue, 30 Jan 2024 16:29:02 +0100 Subject: [PATCH 3/4] feat(api): get authorization url for given audience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Clément Latzarus --- .../application/authentication/oidc/index.js | 1 + .../authentication/oidc/oidc-controller.js | 3 +- .../oidc/oidc-controller_test.js | 37 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/api/lib/application/authentication/oidc/index.js b/api/lib/application/authentication/oidc/index.js index 65efc6ebf43..c5121228d76 100644 --- a/api/lib/application/authentication/oidc/index.js +++ b/api/lib/application/authentication/oidc/index.js @@ -66,6 +66,7 @@ const register = async function (server) { query: Joi.object({ identity_provider: Joi.string().required(), redirect_uri: Joi.string().required(), + audience: Joi.string().valid('app', 'admin').optional(), }), }, handler: oidcController.getAuthenticationUrl, diff --git a/api/lib/application/authentication/oidc/oidc-controller.js b/api/lib/application/authentication/oidc/oidc-controller.js index 4915732b566..81226f5112e 100644 --- a/api/lib/application/authentication/oidc/oidc-controller.js +++ b/api/lib/application/authentication/oidc/oidc-controller.js @@ -81,9 +81,10 @@ const getAuthenticationUrl = async function ( authenticationServiceRegistry, }, ) { - const { identity_provider: identityProvider } = request.query; + const { identity_provider: identityProvider, audience } = request.query; const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({ identityProviderCode: identityProvider, + audience, }); const { nonce, state, ...payload } = oidcAuthenticationService.getAuthenticationUrl({ redirectUri: request.query['redirect_uri'], diff --git a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js index 2f32060d095..6ef53d41d3d 100644 --- a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js +++ b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js @@ -167,6 +167,43 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }); expect(request.yar.set).to.have.been.calledTwice; }); + + context('when an audience is specified', function () { + it('calls oidc authentication service with audience as a parameter', async function () { + // given + const request = { + query: { + identity_provider: identityProvider, + redirect_uri: 'http:/exemple.net/', + audience: 'admin', + }, + yar: { set: sinon.stub() }, + }; + const getAuthenticationUrlStub = sinon.stub(); + const oidcAuthenticationService = { + getAuthenticationUrl: getAuthenticationUrlStub, + }; + const authenticationServiceRegistryStub = { + getOidcProviderServiceByCode: sinon.stub(), + }; + + authenticationServiceRegistryStub.getOidcProviderServiceByCode.returns(oidcAuthenticationService); + + const dependencies = { + authenticationServiceRegistry: authenticationServiceRegistryStub, + }; + getAuthenticationUrlStub.returns('an authentication url'); + + // when + await oidcController.getAuthenticationUrl(request, hFake, dependencies); + + // then + expect(authenticationServiceRegistryStub.getOidcProviderServiceByCode).to.have.been.calledWith({ + identityProviderCode: identityProvider, + audience: 'admin', + }); + }); + }); }); describe('#authenticateUser', function () { From cf5db9113799d83bb501a72d190f0f8819e4676b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Latzarus?= Date: Tue, 6 Feb 2024 18:20:57 +0100 Subject: [PATCH 4/4] feat(api): create oidc token for given audience --- .../application/authentication/oidc/index.js | 1 + .../authentication/oidc/oidc-controller.js | 3 +- .../oidc/oidc-controller_test.js | 43 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/api/lib/application/authentication/oidc/index.js b/api/lib/application/authentication/oidc/index.js index c5121228d76..e8c38928742 100644 --- a/api/lib/application/authentication/oidc/index.js +++ b/api/lib/application/authentication/oidc/index.js @@ -90,6 +90,7 @@ const register = async function (server) { code: Joi.string().required(), redirect_uri: Joi.string().required(), state: Joi.string().required(), + audience: Joi.string().valid('app', 'admin').optional(), }, }, }), diff --git a/api/lib/application/authentication/oidc/oidc-controller.js b/api/lib/application/authentication/oidc/oidc-controller.js index 81226f5112e..77925cd33b3 100644 --- a/api/lib/application/authentication/oidc/oidc-controller.js +++ b/api/lib/application/authentication/oidc/oidc-controller.js @@ -103,7 +103,7 @@ const authenticateUser = async function ( authenticationServiceRegistry, }, ) { - const { code, identityProvider, redirectUri, state: stateReceived } = request.deserializedPayload; + const { code, identityProvider, redirectUri, state: stateReceived, audience } = request.deserializedPayload; const stateSent = request.yar.get('state', true); // eslint-disable-next-line no-unused-vars @@ -111,6 +111,7 @@ const authenticateUser = async function ( const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({ identityProviderCode: identityProvider, + audience, }); const result = await usecases.authenticateOidcUser({ diff --git a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js index 6ef53d41d3d..7ce194ca467 100644 --- a/api/tests/unit/application/authentication/oidc/oidc-controller_test.js +++ b/api/tests/unit/application/authentication/oidc/oidc-controller_test.js @@ -269,6 +269,49 @@ describe('Unit | Application | Controller | Authentication | OIDC', function () }); }); + context('when audience is "admin"', function () { + it('uses only identity providers enabled in Pix Admin', async function () { + // given + request = { + ...request, + deserializedPayload: { + ...request.deserializedPayload, + audience: 'admin', + }, + }; + const oidcAuthenticationService = {}; + const authenticationServiceRegistryStub = { + getOidcProviderServiceByCode: sinon.stub(), + }; + + authenticationServiceRegistryStub.getOidcProviderServiceByCode + .withArgs({ identityProviderCode: identityProvider }) + .returns(oidcAuthenticationService); + + const dependencies = { + authenticationServiceRegistry: authenticationServiceRegistryStub, + }; + + usecases.authenticateOidcUser.resolves({ + pixAccessToken, + logoutUrlUUID: '0208f50b-f612-46aa-89a0-7cdb5fb0d312', + isAuthenticationComplete: true, + }); + + request.yar.get.onCall(0).returns(state); + request.yar.get.onCall(1).returns(nonce); + + // when + await oidcController.authenticateUser(request, hFake, dependencies); + + // then + expect(authenticationServiceRegistryStub.getOidcProviderServiceByCode).to.have.been.calledWithExactly({ + identityProviderCode: identityProvider, + audience: 'admin', + }); + }); + }); + it('should return PIX access token and logout url uuid when authentication is complete', async function () { // given const oidcAuthenticationService = {};