Skip to content

Commit

Permalink
refactor(api): make authenticateOidcUser usecase use requestedApplica…
Browse files Browse the repository at this point in the history
…tion instead of target
  • Loading branch information
lego-technix committed Feb 25, 2025
1 parent 41720fe commit f6bc8a3
Show file tree
Hide file tree
Showing 14 changed files with 98 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { oidcAuthenticationServiceRegistry } from '../../../../lib/domain/usecases/index.js';
import { PIX_ADMIN } from '../../../authorization/domain/constants.js';
import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js';
import { usecases } from '../../domain/usecases/index.js';
import * as oidcProviderSerializer from '../../infrastructure/serializers/jsonapi/oidc-identity-providers.serializer.js';
import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
import { getForwardedOrigin, getRequestedApplication } from '../../infrastructure/utils/network.js';

/**
* @param request
Expand Down Expand Up @@ -45,13 +44,14 @@ async function reconcileUserForAdmin(
) {
const { email, identityProvider, authenticationKey } = request.deserializedPayload;
const origin = getForwardedOrigin(request.headers);
const requestedApplication = getRequestedApplication(origin);

await dependencies.oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await dependencies.oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProvider);

const oidcAuthenticationService = dependencies.oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({
identityProviderCode: identityProvider,
target: PIX_ADMIN.TARGET,
requestedApplication,
});

const accessToken = await usecases.reconcileOidcUserForAdmin({
Expand All @@ -60,6 +60,7 @@ async function reconcileUserForAdmin(
authenticationKey,
oidcAuthenticationService,
audience: origin,
requestedApplication,
});

return h.response({ access_token: accessToken }).code(200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { requestResponseUtils } from '../../../shared/infrastructure/utils/reque
import { usecases } from '../../domain/usecases/index.js';
import * as oidcProviderSerializer from '../../infrastructure/serializers/jsonapi/oidc-identity-providers.serializer.js';
import * as oidcSerializer from '../../infrastructure/serializers/jsonapi/oidc-serializer.js';
import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
import { getForwardedOrigin, getRequestedApplication } from '../../infrastructure/utils/network.js';

/**
* @typedef {function} authenticateOidcUser
Expand All @@ -12,8 +12,9 @@ import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
* @return {Promise<*>}
*/
async function authenticateOidcUser(request, h) {
const { code, state, iss, identityProvider: identityProviderCode, target } = request.deserializedPayload;
const { code, state, iss, identityProvider: identityProviderCode } = request.deserializedPayload;
const origin = getForwardedOrigin(request.headers);
const requestedApplication = getRequestedApplication(origin);

const sessionState = request.yar.get('state', true);
const nonce = request.yar.get('nonce', true);
Expand All @@ -24,13 +25,13 @@ async function authenticateOidcUser(request, h) {
}

const result = await usecases.authenticateOidcUser({
target,
code,
state,
iss,
identityProviderCode,
nonce,
sessionState,
requestedApplication,
audience: origin,
});

Expand Down Expand Up @@ -100,9 +101,11 @@ async function findUserForReconciliation(request, h, dependencies = { oidcSerial
* @return {Promise<Object>}
*/
async function getAuthorizationUrl(request, h) {
const { identity_provider: identityProvider, target } = request.query;
const { identity_provider: identityProvider } = request.query;
const origin = getForwardedOrigin(request.headers);
const requestedApplication = getRequestedApplication(origin);

const { nonce, state, ...payload } = await usecases.getAuthorizationUrl({ target, identityProvider });
const { nonce, state, ...payload } = await usecases.getAuthorizationUrl({ identityProvider, requestedApplication });

request.yar.set('state', state);
request.yar.set('nonce', nonce);
Expand All @@ -118,8 +121,11 @@ async function getAuthorizationUrl(request, h) {
* @return {Promise<*>}
*/
async function getIdentityProviders(request, h) {
const target = request.query.target;
const identityProviders = await usecases.getReadyIdentityProviders({ target });
const origin = getForwardedOrigin(request.headers);
const requestedApplication = getRequestedApplication(origin);

const identityProviders = await usecases.getReadyIdentityProviders({ requestedApplication });

return h.response(oidcProviderSerializer.serialize(identityProviders)).code(200);
}

Expand Down Expand Up @@ -152,11 +158,13 @@ async function reconcileUser(request, h) {
const { identityProvider, authenticationKey } = request.deserializedPayload;

const origin = getForwardedOrigin(request.headers);
const requestedApplication = getRequestedApplication(origin);

const result = await usecases.reconcileOidcUser({
authenticationKey,
identityProvider,
audience: origin,
requestedApplication,
});

return h.response({ access_token: result.accessToken, logout_url_uuid: result.logoutUrlUUID }).code(200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const oidcProviderRoutes = [
options: {
validate: {
query: Joi.object({
target: Joi.string().optional().default('app'),
target: Joi.string().optional().default('app'), // Now useless, will soon be removed
}),
},
auth: false,
Expand Down Expand Up @@ -49,7 +49,7 @@ export const oidcProviderRoutes = [
validate: {
query: Joi.object({
identity_provider: Joi.string().required(),
target: Joi.string().valid('app', 'admin').optional(),
target: Joi.string().valid('app', 'admin').optional(), // Now useless, will soon be removed
}),
},
handler: (request, h) => oidcProviderController.getAuthorizationUrl(request, h),
Expand All @@ -73,7 +73,7 @@ export const oidcProviderRoutes = [
code: Joi.string().required(),
state: Joi.string().required(),
iss: Joi.string().optional(),
target: Joi.string().valid('app', 'admin').optional(),
target: Joi.string().valid('app', 'admin').optional(), // Now useless, will soon be removed
},
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ export class OidcAuthenticationServiceRegistry {
return this.#readyOidcProviderServicesForPixAdmin;
}

getOidcProviderServiceByCode({ identityProviderCode, target = 'app' }) {
const services = target === 'admin' ? this.#readyOidcProviderServicesForPixAdmin : this.#readyOidcProviderServices;
getOidcProviderServiceByCode({ identityProviderCode, requestedApplication }) {
const services = requestedApplication?.isPixAdmin
? this.#readyOidcProviderServicesForPixAdmin
: this.#readyOidcProviderServices;
const oidcProviderService = services.find((service) => identityProviderCode === service.code);

if (!oidcProviderService) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { PIX_ADMIN } from '../../../authorization/domain/constants.js';
import { ForbiddenAccess } from '../../../shared/domain/errors.js';

/**
Expand All @@ -11,6 +10,7 @@ import { ForbiddenAccess } from '../../../shared/domain/errors.js';
* @param {string} params.sessionState
* @param {string} params.state
* @param {string} params.audience
* @param {RequestedApplication} params.requestedApplication
* @param {AuthenticationSessionService} params.authenticationSessionService
* @param {OidcAuthenticationServiceRegistry} params.oidcAuthenticationServiceRegistry
* @param {AdminMemberRepository} params.adminMemberRepository
Expand All @@ -20,14 +20,14 @@ import { ForbiddenAccess } from '../../../shared/domain/errors.js';
* @return {Promise<{isAuthenticationComplete: boolean, givenName: string, familyName: string, authenticationKey: string, email: string}|{isAuthenticationComplete: boolean, pixAccessToken: string, logoutUrlUUID: string}>}
*/
async function authenticateOidcUser({
target,
code,
state,
iss,
identityProviderCode,
nonce,
sessionState,
audience,
requestedApplication,
authenticationSessionService,
oidcAuthenticationServiceRegistry,
adminMemberRepository,
Expand All @@ -40,7 +40,7 @@ async function authenticateOidcUser({

const oidcAuthenticationService = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({
identityProviderCode,
target,
requestedApplication,
});

const sessionContent = await oidcAuthenticationService.exchangeCodeForTokens({
Expand All @@ -65,7 +65,7 @@ async function authenticateOidcUser({
return { authenticationKey, givenName, familyName, email, isAuthenticationComplete: false };
}

await _assertUserHasAccessToApplication({ target, user, adminMemberRepository });
await _assertUserHasAccessToApplication({ requestedApplication, user, adminMemberRepository });

await _updateAuthenticationMethodWithComplement({
userInfo,
Expand Down Expand Up @@ -111,8 +111,8 @@ async function _updateAuthenticationMethodWithComplement({
});
}

async function _assertUserHasAccessToApplication({ target, user, adminMemberRepository }) {
if (target === PIX_ADMIN.TARGET) {
async function _assertUserHasAccessToApplication({ requestedApplication, user, adminMemberRepository }) {
if (requestedApplication.isPixAdmin) {
const adminMember = await adminMemberRepository.get({ userId: user.id });
if (!adminMember?.hasAccessToAdminScope) {
throw new ForbiddenAccess(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
/**
* @typedef {function} getAuthorizationUrl
* @param {Object} params
* @param {string} params.audience
* @param {string} params.identityProvider
* @param {RequestedApplication} params.requestedApplication
* @param {OidcAuthenticationServiceRegistry} params.oidcAuthenticationServiceRegistry
* @return {Promise<string>}
*/
async function getAuthorizationUrl({ target, identityProvider, oidcAuthenticationServiceRegistry }) {
async function getAuthorizationUrl({ identityProvider, requestedApplication, oidcAuthenticationServiceRegistry }) {
await oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProvider);

const oidcAuthenticationService = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({
identityProviderCode: identityProvider,
target,
requestedApplication,
});

return oidcAuthenticationService.getAuthorizationUrl();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* @typedef {function} getReadyIdentityProviders
* @param {Object} params
* @param {string} [params.audience=app]
* @param {RequestedApplication} params.requestedApplication
* @param {OidcAuthenticationServiceRegistry} params.oidcAuthenticationServiceRegistry
* @return {Promise<OidcAuthenticationService[]|null>}
*/
const getReadyIdentityProviders = async function ({ target = 'app', oidcAuthenticationServiceRegistry }) {
const getReadyIdentityProviders = async function ({ requestedApplication, oidcAuthenticationServiceRegistry }) {
await oidcAuthenticationServiceRegistry.loadOidcProviderServices();

if (target === 'admin') {
if (requestedApplication?.isPixAdmin) {
return oidcAuthenticationServiceRegistry.getReadyOidcProviderServicesForPixAdmin();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AuthenticationMethod } from '../models/AuthenticationMethod.js';
* @param {string} params.authenticationKey
* @param {string} params.identityProvider
* @param {string} params.audience
* @param {RequestedApplication} params.requestedApplication
* @param {AuthenticationSessionService} params.authenticationSessionService
* @param {AuthenticationMethodRepository} params.authenticationMethodRepository
* @param {OidcAuthenticationServiceRegistry} params.oidcAuthenticationServiceRegistry
Expand All @@ -21,12 +22,14 @@ export const reconcileOidcUser = async function ({
oidcAuthenticationServiceRegistry,
userLoginRepository,
audience,
requestedApplication,
}) {
await oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProvider);

const oidcAuthenticationService = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({
identityProviderCode: identityProvider,
requestedApplication,
});

const sessionContentAndUserInfo = await authenticationSessionService.getByKey(authenticationKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
const options = {
method: 'GET',
url: '/api/oidc/identity-providers',
headers: {
'x-forwarded-proto': 'https',
'x-forwarded-host': 'app.pix.fr',
},
};

// when
Expand Down Expand Up @@ -79,13 +83,16 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
// given
const query = querystring.stringify({
identity_provider: 'OIDC_EXAMPLE_NET',
target: 'app',
});

// when
const response = await server.inject({
method: 'GET',
url: `/api/oidc/authorization-url?${query}`,
headers: {
'x-forwarded-proto': 'https',
'x-forwarded-host': 'app.pix.fr',
},
});

// then
Expand Down Expand Up @@ -119,6 +126,10 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
const authUrlResponse = await server.inject({
method: 'GET',
url: `/api/oidc/authorization-url?${query}`,
headers: {
'x-forwarded-proto': 'https',
'x-forwarded-host': 'app.pix.fr',
},
});
cookies = authUrlResponse.headers['set-cookie'];

Expand Down Expand Up @@ -190,7 +201,11 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
const response = await server.inject({
method: 'POST',
url: '/api/oidc/token',
headers: { cookie: cookies[0] },
headers: {
cookie: cookies[0],
'x-forwarded-proto': 'https',
'x-forwarded-host': 'app.pix.fr',
},
payload,
});

Expand Down Expand Up @@ -283,16 +298,14 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
});
});

context('when target is admin', function () {
context('when the requestedApplication is admin', function () {
context('when user does not have an admin role', function () {
it('returns 403', async function () {
// given
const firstName = 'John';
const lastName = 'Doe';
const externalIdentifier = 'sub';

payload.data.attributes.target = 'admin';

const userId = databaseBuilder.factory.buildUser({
firstName,
lastName,
Expand Down Expand Up @@ -335,7 +348,11 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
const response = await server.inject({
method: 'POST',
url: '/api/oidc/token',
headers: { cookie: cookies[0] },
headers: {
cookie: cookies[0],
'x-forwarded-proto': 'https',
'x-forwarded-host': 'admin.pix.fr',
},
payload,
});

Expand All @@ -359,8 +376,6 @@ describe('Acceptance | Identity Access Management | Application | Route | oidc-p
const lastName = 'Doe';
const externalIdentifier = 'sub';

payload.data.attributes.target = 'admin';

const userId = databaseBuilder.factory.buildUser.withRole({
firstName,
lastName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PIX_ADMIN } from '../../../../../src/authorization/domain/constants.js';
import { OidcAuthenticationServiceRegistry } from '../../../../../src/identity-access-management/domain/services/oidc-authentication-service-registry.js';
import { oidcProviderRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/oidc-provider-repository.js';
import { RequestedApplication } from '../../../../../src/identity-access-management/infrastructure/utils/network.js';
import { InvalidIdentityProviderError } from '../../../../../src/shared/domain/errors.js';
import { catchErrSync, databaseBuilder, expect } from '../../../../test-helper.js';

Expand Down Expand Up @@ -164,15 +164,16 @@ describe('Integration | Identity Access Management | Domain | Service | oidc-aut
expect(service.code).to.equal('OIDC_EXAMPLE');
});

describe('when the target is admin', function () {
describe('when the requestedApplication is admin', function () {
it('returns a ready OIDC provider for Pix Admin', async function () {
// given
const requestedApplication = new RequestedApplication('admin');
await oidcAuthenticationServiceRegistry.loadOidcProviderServices();

// when
const service = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({
identityProviderCode: 'OIDC_EXAMPLE_FOR_PIX_ADMIN',
target: PIX_ADMIN.TARGET,
requestedApplication,
});

// then
Expand Down
Loading

0 comments on commit f6bc8a3

Please sign in to comment.