Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TECH] Dans Pix API supprimer les notions erronées ou obsolètes de scope et de target utilisées pour la vérification du droit d’accès d’un utilisateur à une application (PIX-15945) #11513

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
9 changes: 1 addition & 8 deletions api/src/authorization/domain/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,11 @@ const PIX_ADMIN = {
SUPER_ADMIN: 'SUPER_ADMIN',
SUPPORT: 'SUPPORT',
},
SCOPE: 'pix-admin',
TARGET: 'admin',
};

const PIX_ORGA = {
NOT_LINKED_ORGANIZATION_MSG:
"L'accès à Pix Orga est limité aux membres invités. Chaque espace est géré par un administrateur Pix Orga propre à l'organisation qui l'utilise. Contactez-le pour qu'il vous y invite.",
SCOPE: 'pix-orga',
};

const PIX_CERTIF = {
SCOPE: 'pix-certif',
};

export { PIX_ADMIN, PIX_CERTIF, PIX_ORGA };
export { PIX_ADMIN, PIX_ORGA };
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
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';
Expand Down Expand Up @@ -52,7 +51,7 @@ async function reconcileUserForAdmin(

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

const accessToken = await usecases.reconcileOidcUserForAdmin({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getForwardedOrigin, RequestedApplication } from '../../infrastructure/u
* @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 = RequestedApplication.fromOrigin(origin);

Expand All @@ -25,7 +25,6 @@ async function authenticateOidcUser(request, h) {
}

const result = await usecases.authenticateOidcUser({
target,
code,
state,
iss,
Expand Down Expand Up @@ -104,9 +103,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 = RequestedApplication.fromOrigin(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 @@ -122,8 +123,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 = RequestedApplication.fromOrigin(origin);

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

return h.response(oidcProviderSerializer.serialize(identityProviders)).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
@@ -1,6 +1,6 @@
import { tokenService } from '../../../shared/domain/services/token-service.js';
import { usecases } from '../../domain/usecases/index.js';
import { getForwardedOrigin } from '../../infrastructure/utils/network.js';
import { getForwardedOrigin, RequestedApplication } from '../../infrastructure/utils/network.js';

const authenticateAnonymousUser = async function (request, h) {
const { campaign_code: campaignCode, lang } = request.payload;
Expand Down Expand Up @@ -28,21 +28,22 @@ const createToken = async function (request, h, dependencies = { tokenService })
let expirationDelaySeconds;

const origin = getForwardedOrigin(request.headers);
const requestedApplication = RequestedApplication.fromOrigin(origin);

const grantType = request.payload.grant_type;

if (grantType === 'password') {
const { username, password, scope } = request.payload;
const { username, password } = request.payload;
const localeFromCookie = request.state?.locale;
const source = 'pix';

const tokensInfo = await usecases.authenticateUser({
username,
password,
scope,
source,
localeFromCookie,
audience: origin,
requestedApplication,
});

accessToken = tokensInfo.accessToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const tokenRoutes = [
grant_type: Joi.string().valid('password').required(),
username: Joi.string().required(),
password: Joi.string().required(),
scope: Joi.string(),
scope: Joi.string().optional(), // Now useless, will soon be removed
}),
Joi.object()
.required()
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,40 +10,39 @@ 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
* @param {AuthenticationMethodRepository} params.authenticationMethodRepository
* @param {UserLoginRepository} params.userLoginRepository
* @param {UserRepository} params.userRepository
* @param {LastUserApplicationConnectionsRepository} params.LastUserApplicationConnectionsRepository,
* @param {RequestedApplication} params.RequestedApplication,
* @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,
authenticationMethodRepository,
userLoginRepository,
userRepository,
lastUserApplicationConnectionsRepository,
requestedApplication,
}) {
await oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProviderCode);

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

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

await _assertUserWithPixAdminAccess({ target, userId: user.id, adminMemberRepository });
await _assertUserHasAccessToApplication({ requestedApplication, user, adminMemberRepository });

await _updateAuthenticationMethodWithComplement({
userInfo,
Expand Down Expand Up @@ -120,9 +118,9 @@ async function _updateAuthenticationMethodWithComplement({
});
}

async function _assertUserWithPixAdminAccess({ target, userId, adminMemberRepository }) {
if (target === PIX_ADMIN.TARGET) {
const adminMember = await adminMemberRepository.get({ userId });
async function _assertUserHasAccessToApplication({ requestedApplication, user, adminMemberRepository }) {
if (requestedApplication.isPixAdmin) {
const adminMember = await adminMemberRepository.get({ userId: user.id });
if (!adminMember?.hasAccessToAdminScope) {
throw new ForbiddenAccess(
'User does not have the rights to access the application',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { RefreshToken } from '../models/RefreshToken.js';

const authenticateUser = async function ({
password,
scope,
source,
username,
localeFromCookie,
Expand All @@ -21,52 +20,52 @@ const authenticateUser = async function ({
emailRepository,
emailValidationDemandRepository,
audience,
requestedApplication,
}) {
try {
const foundUser = await pixAuthenticationService.getUserByUsernameAndPassword({
const user = await pixAuthenticationService.getUserByUsernameAndPassword({
username,
password,
userRepository,
});

if (foundUser.shouldChangePassword) {
const passwordResetToken = tokenService.createPasswordResetToken(foundUser.id);
if (user.shouldChangePassword) {
const passwordResetToken = tokenService.createPasswordResetToken(user.id);
throw new UserShouldChangePasswordError(undefined, passwordResetToken);
}

await _checkUserAccessScope(scope, foundUser, adminMemberRepository);
await _assertUserHasAccessToApplication({ requestedApplication, user, adminMemberRepository });

const refreshToken = RefreshToken.generate({ userId: foundUser.id, source, audience });
const refreshToken = RefreshToken.generate({ userId: user.id, source, audience });
await refreshTokenRepository.save({ refreshToken });

const { accessToken, expirationDelaySeconds } = await tokenService.createAccessTokenFromUser({
userId: foundUser.id,
userId: user.id,
source,
audience,
});

foundUser.setLocaleIfNotAlreadySet(localeFromCookie);
if (foundUser.hasBeenModified) {
await userRepository.update({ id: foundUser.id, locale: foundUser.locale });
user.setLocaleIfNotAlreadySet(localeFromCookie);
if (user.hasBeenModified) {
await userRepository.update({ id: user.id, locale: user.locale });
}
const userLogin = await userLoginRepository.findByUserId(foundUser.id);
if (foundUser.email && userLogin?.shouldSendConnectionWarning()) {
const validationToken = !foundUser.emailConfirmedAt
? await emailValidationDemandRepository.save(foundUser.id)
: null;

const userLogin = await userLoginRepository.findByUserId(user.id);
if (user.email && userLogin?.shouldSendConnectionWarning()) {
const validationToken = !user.emailConfirmedAt ? await emailValidationDemandRepository.save(user.id) : null;
await emailRepository.sendEmailAsync(
createWarningConnectionEmail({
locale: foundUser.locale,
email: foundUser.email,
firstName: foundUser.firstName,
locale: user.locale,
email: user.email,
firstName: user.firstName,
validationToken,
}),
);
}
await userLoginRepository.updateLastLoggedAt({ userId: foundUser.id });
await userLoginRepository.updateLastLoggedAt({ userId: user.id });

await authenticationMethodRepository.updateLastLoggedAtByIdentityProvider({
userId: foundUser.id,
userId: user.id,
identityProvider: NON_OIDC_IDENTITY_PROVIDERS.PIX.code,
});

Expand All @@ -80,12 +79,12 @@ const authenticateUser = async function ({
}
};

async function _checkUserAccessScope(scope, user, adminMemberRepository) {
if (scope === PIX_ORGA.SCOPE && !user.isLinkedToOrganizations()) {
async function _assertUserHasAccessToApplication({ requestedApplication, user, adminMemberRepository }) {
if (requestedApplication.isPixOrga && !user.isLinkedToOrganizations()) {
throw new ForbiddenAccess(PIX_ORGA.NOT_LINKED_ORGANIZATION_MSG);
}

if (scope === PIX_ADMIN.SCOPE) {
if (requestedApplication.isPixAdmin) {
const adminMember = await adminMemberRepository.get({ userId: user.id });
if (!adminMember?.hasAccessToAdminScope) {
throw new ForbiddenAccess(PIX_ADMIN.NOT_ALLOWED_MSG);
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 @@ -31,6 +32,7 @@ export const reconcileOidcUser = async function ({

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

const sessionContentAndUserInfo = await authenticationSessionService.getByKey(authenticationKey);
Expand Down
Loading