Skip to content

Commit

Permalink
[FEATURE] Ajouter le paramètre audience aux routes API 'oidc/token' e…
Browse files Browse the repository at this point in the history
…t 'oidc/authentication-url' pour le besoin SSO Google (PIX-10966).

 #7970
  • Loading branch information
pix-service-auto-merge authored Feb 16, 2024
2 parents 25f1910 + cf5db91 commit 3915377
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 40 deletions.
2 changes: 2 additions & 0 deletions api/lib/application/authentication/oidc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -89,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(),
},
},
}),
Expand Down
31 changes: 19 additions & 12 deletions api/lib/application/authentication/oidc/oidc-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -79,9 +81,11 @@ const getAuthenticationUrl = async function (
authenticationServiceRegistry,
},
) {
const { identity_provider: identityProvider } = request.query;
const oidcAuthenticationService =
dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode(identityProvider);
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'],
});
Expand All @@ -99,14 +103,16 @@ 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
const nonce = request.yar.get('nonce', true);

const oidcAuthenticationService =
dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode(identityProvider);
const oidcAuthenticationService = dependencies.authenticationServiceRegistry.getOidcProviderServiceByCode({
identityProviderCode: identityProvider,
audience,
});

const result = await usecases.authenticateOidcUser({
code,
Expand Down Expand Up @@ -137,8 +143,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function ()
};

authenticationServiceRegistryStub.getOidcProviderServiceByCode
.withArgs(identityProvider)
.withArgs({ identityProviderCode: identityProvider })
.returns(oidcAuthenticationService);

const dependencies = {
Expand Down Expand Up @@ -150,7 +150,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function ()
};

authenticationServiceRegistryStub.getOidcProviderServiceByCode
.withArgs(identityProvider)
.withArgs({ identityProviderCode: identityProvider, audience: undefined })
.returns(oidcAuthenticationService);

const dependencies = {
Expand All @@ -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 () {
Expand Down Expand Up @@ -203,7 +240,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function ()
};

authenticationServiceRegistryStub.getOidcProviderServiceByCode
.withArgs(identityProvider)
.withArgs({ identityProviderCode: identityProvider, audience: undefined })
.returns(oidcAuthenticationService);

const dependencies = {
Expand Down Expand Up @@ -232,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 = {};
Expand All @@ -240,7 +320,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function ()
};

authenticationServiceRegistryStub.getOidcProviderServiceByCode
.withArgs(identityProvider)
.withArgs({ identityProviderCode: identityProvider })
.returns(oidcAuthenticationService);

const dependencies = {
Expand Down Expand Up @@ -271,7 +351,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function ()
};

authenticationServiceRegistryStub.getOidcProviderServiceByCode
.withArgs(identityProvider)
.withArgs({ identityProviderCode: identityProvider })
.returns(oidcAuthenticationService);

const dependencies = {
Expand Down Expand Up @@ -309,7 +389,7 @@ describe('Unit | Application | Controller | Authentication | OIDC', function ()
};

authenticationServiceRegistryStub.getOidcProviderServiceByCode
.withArgs(identityProvider)
.withArgs({ identityProviderCode: identityProvider })
.returns(oidcAuthenticationService);

const dependencies = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -110,7 +140,7 @@ describe('Unit | Domain | Services | authentication registry', function () {
const error = catchErrSync(
oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode,
oidcAuthenticationServiceRegistry,
)(identityProviderCode);
)({ identityProviderCode });

// then
expect(error).to.be.an.instanceOf(InvalidIdentityProviderError);
Expand Down

0 comments on commit 3915377

Please sign in to comment.