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

[FEATURE] Ajouter la forwarded origin HTTP dans un nouvel attribut aud des AC utilisateurs lors du login (par mot de passe et SSO OIDC) (PIX-15928) #11137

Merged
merged 17 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions admin/app/adapters/oidc-identity-provider.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import ApplicationAdapter from './application';

const PIX_ADMIN_AUDIENCE = 'admin';
const PIX_ADMIN_TARGET = 'admin';

export default class OidcIdentityProviderAdapter extends ApplicationAdapter {
urlForFindAll(_, snapshot) {
if (snapshot.adapterOptions?.readyIdentityProviders) {
return `${this.host}/${this.namespace}/oidc/identity-providers?audience=${PIX_ADMIN_AUDIENCE}`;
return `${this.host}/${this.namespace}/oidc/identity-providers?target=${PIX_ADMIN_TARGET}`;
}
return `${this.host}/${this.namespace}/admin/oidc/identity-providers`;
}
Expand Down
2 changes: 1 addition & 1 deletion admin/app/authenticators/oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default class OidcAuthenticator extends BaseAuthenticator {
identity_provider: identityProvider.code,
code,
state: state,
audience: 'admin',
target: 'admin',
};

if (this.session.isAuthenticated) {
Expand Down
2 changes: 1 addition & 1 deletion admin/app/routes/authentication/login-oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default class LoginOidcRoute extends Route {

async _handleRedirectRequest(identityProvider) {
const response = await fetch(
`${ENV.APP.API_HOST}/api/oidc/authorization-url?identity_provider=${identityProvider.code}&audience=admin`,
`${ENV.APP.API_HOST}/api/oidc/authorization-url?identity_provider=${identityProvider.code}&target=admin`,
);
const { redirectTarget } = await response.json();
this.location.replace(redirectTarget);
Expand Down
4 changes: 2 additions & 2 deletions admin/tests/unit/adapters/oidc-identity-provider-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { module, test } from 'qunit';
module('Unit | Adapter | OidcIdentityProvider', function (hooks) {
setupTest(hooks);
module('#urlForFindAll', function () {
test('returns correct url for ready providers including audience parameter', function (assert) {
test('returns correct url for ready providers including target parameter', function (assert) {
// given
const adapter = this.owner.lookup('adapter:oidc-identity-provider');

Expand All @@ -16,7 +16,7 @@ module('Unit | Adapter | OidcIdentityProvider', function (hooks) {
});

// then
assert.ok(url.endsWith('/oidc/identity-providers?audience=admin'));
assert.ok(url.endsWith('/oidc/identity-providers?target=admin'));
});

test('returns correct url for all available providers', function (assert) {
Expand Down
2 changes: 1 addition & 1 deletion admin/tests/unit/authenticators/oidc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module('Unit | Authenticator | oidc', function (hooks) {
identity_provider: identityProviderCode,
code: code,
state,
audience: 'admin',
target: 'admin',
},
},
});
Expand Down
2 changes: 1 addition & 1 deletion admin/tests/unit/routes/authentication/login-oidc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ module('Unit | Route | login-oidc', function (hooks) {
// then
sinon.assert.calledWith(
fetchStub,
'http://localhost:3000/api/oidc/authorization-url?identity_provider=OIDC_PARTNER&audience=admin',
'http://localhost:3000/api/oidc/authorization-url?identity_provider=OIDC_PARTNER&target=admin',
);
sinon.assert.calledWith(route.location.replace, 'https://oidc.example.net/connexion');
assert.ok(true);
Expand Down
2 changes: 1 addition & 1 deletion api/src/authorization/domain/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const PIX_ADMIN = {
SUPPORT: 'SUPPORT',
},
SCOPE: 'pix-admin',
AUDIENCE: 'admin',
TARGET: 'admin',
};

const PIX_ORGA = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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';

/**
* @param request
Expand Down Expand Up @@ -43,20 +44,22 @@ async function reconcileUserForAdmin(
},
) {
const { email, identityProvider, authenticationKey } = request.deserializedPayload;
const origin = getForwardedOrigin(request.headers);

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

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

const accessToken = await usecases.reconcileOidcUserForAdmin({
email,
identityProvider,
authenticationKey,
oidcAuthenticationService,
audience: origin,
});

return h.response({ access_token: accessToken }).code(200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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';

/**
* @typedef {function} authenticateOidcUser
Expand All @@ -11,7 +12,8 @@ import * as oidcSerializer from '../../infrastructure/serializers/jsonapi/oidc-s
* @return {Promise<*>}
*/
async function authenticateOidcUser(request, h) {
const { code, state, iss, identityProvider: identityProviderCode, audience } = request.deserializedPayload;
const { code, state, iss, identityProvider: identityProviderCode, target } = request.deserializedPayload;
const origin = getForwardedOrigin(request.headers);

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

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

if (result.isAuthenticationComplete) {
Expand Down Expand Up @@ -58,12 +61,14 @@ async function createUser(request, h, dependencies = { requestResponseUtils }) {
const { identityProvider, authenticationKey } = request.deserializedPayload;
const localeFromCookie = request.state?.locale;
const language = dependencies.requestResponseUtils.extractLocaleFromRequest(request);
const origin = getForwardedOrigin(request.headers);

const { accessToken: access_token, logoutUrlUUID: logout_url_uuid } = await usecases.createOidcUser({
authenticationKey,
identityProvider,
localeFromCookie,
language,
audience: origin,
});

return h.response({ access_token, logout_url_uuid }).code(200);
Expand Down Expand Up @@ -95,9 +100,9 @@ async function findUserForReconciliation(request, h, dependencies = { oidcSerial
* @return {Promise<Object>}
*/
async function getAuthorizationUrl(request, h) {
const { identity_provider: identityProvider, audience } = request.query;
const { identity_provider: identityProvider, target } = request.query;

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

request.yar.set('state', state);
request.yar.set('nonce', nonce);
Expand All @@ -113,8 +118,8 @@ async function getAuthorizationUrl(request, h) {
* @return {Promise<*>}
*/
async function getIdentityProviders(request, h) {
const audience = request.query.audience;
const identityProviders = await usecases.getReadyIdentityProviders({ audience });
const target = request.query.target;
const identityProviders = await usecases.getReadyIdentityProviders({ target });
return h.response(oidcProviderSerializer.serialize(identityProviders)).code(200);
}

Expand Down Expand Up @@ -146,9 +151,12 @@ async function getRedirectLogoutUrl(request, h) {
async function reconcileUser(request, h) {
const { identityProvider, authenticationKey } = request.deserializedPayload;

const origin = getForwardedOrigin(request.headers);

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

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 = [
config: {
validate: {
query: Joi.object({
audience: Joi.string().optional().default('app'),
target: Joi.string().optional().default('app'),
}),
},
auth: false,
Expand Down Expand Up @@ -48,7 +48,7 @@ export const oidcProviderRoutes = [
validate: {
query: Joi.object({
identity_provider: Joi.string().required(),
audience: Joi.string().valid('app', 'admin').optional(),
target: Joi.string().valid('app', 'admin').optional(),
}),
},
handler: (request, h) => oidcProviderController.getAuthorizationUrl(request, h),
Expand All @@ -72,7 +72,7 @@ export const oidcProviderRoutes = [
code: Joi.string().required(),
state: Joi.string().required(),
iss: Joi.string().optional(),
audience: Joi.string().valid('app', 'admin').optional(),
target: Joi.string().valid('app', 'admin').optional(),
},
},
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +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';

const authenticateAnonymousUser = async function (request, h) {
const { campaign_code: campaignCode, lang } = request.payload;
Expand All @@ -25,6 +26,8 @@ const createToken = async function (request, h, dependencies = { tokenService })
let accessToken, refreshToken;
let expirationDelaySeconds;

const origin = getForwardedOrigin(request.headers);
bpetetot marked this conversation as resolved.
Show resolved Hide resolved

const grantType = request.payload.grant_type;
const scope = request.payload.scope;

Expand All @@ -33,15 +36,22 @@ const createToken = async function (request, h, dependencies = { tokenService })
const localeFromCookie = request.state?.locale;
const source = 'pix';

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

accessToken = tokensInfo.accessToken;
refreshToken = tokensInfo.refreshToken;
expirationDelaySeconds = tokensInfo.expirationDelaySeconds;
} else if (grantType === 'refresh_token') {
refreshToken = request.payload.refresh_token;

const tokensInfo = await usecases.createAccessTokenFromRefreshToken({ refreshToken, scope });
const tokensInfo = await usecases.createAccessTokenFromRefreshToken({ refreshToken, scope, audience: origin });

accessToken = tokensInfo.accessToken;
expirationDelaySeconds = tokensInfo.expirationDelaySeconds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import { config } from '../../../shared/config.js';
const SEPARATOR = ':';

export class RefreshToken {
constructor({ userId, scope, source, value }) {
constructor({ userId, scope, source, value, audience }) {
this.userId = userId;
this.scope = scope;
this.source = source;
this.value = value;
this.audience = audience;
}

static generate({ userId, scope, source }) {
static generate({ userId, scope, source, audience }) {
const uuid = crypto.randomUUID();
const value = [userId, scope, uuid].filter(Boolean).join(SEPARATOR);
return new RefreshToken({ userId, scope, source, value });
return new RefreshToken({ userId, scope, source, value, audience });
}

get expirationDelaySeconds() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ export class OidcAuthenticationServiceRegistry {
return this.#readyOidcProviderServicesForPixAdmin;
}

getOidcProviderServiceByCode({ identityProviderCode, audience = 'app' }) {
const services =
audience === 'admin' ? this.#readyOidcProviderServicesForPixAdmin : this.#readyOidcProviderServices;
getOidcProviderServiceByCode({ identityProviderCode, target = 'app' }) {
const services = target === 'admin' ? 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
Expand Up @@ -122,8 +122,12 @@ export class OidcAuthenticationService {
}
}

createAccessToken(userId) {
return jsonwebtoken.sign({ user_id: userId }, config.authentication.secret, this.accessTokenJwtOptions);
createAccessToken({ userId, audience }) {
return jsonwebtoken.sign(
{ user_id: userId, aud: audience },
config.authentication.secret,
this.accessTokenJwtOptions,
);
}

async saveIdToken({ idToken, userId }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { ForbiddenAccess } from '../../../shared/domain/errors.js';
/**
* @typedef {function} authenticateOidcUser
* @param {Object} params
* @param {string} params.audience
* @param {string} params.target
* @param {string} params.code
* @param {string} params.identityProviderCode
* @param {string} params.nonce
* @param {string} params.sessionState
* @param {string} params.state
* @param {string} params.audience
* @param {AuthenticationSessionService} params.authenticationSessionService
* @param {OidcAuthenticationServiceRegistry} params.oidcAuthenticationServiceRegistry
* @param {AdminMemberRepository} params.adminMemberRepository
Expand All @@ -19,13 +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({
audience,
target,
code,
state,
iss,
identityProviderCode,
nonce,
sessionState,
audience,
authenticationSessionService,
oidcAuthenticationServiceRegistry,
adminMemberRepository,
Expand All @@ -38,7 +40,7 @@ async function authenticateOidcUser({

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

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

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

await _updateAuthenticationMethodWithComplement({
userInfo,
Expand All @@ -73,7 +75,7 @@ async function authenticateOidcUser({
authenticationMethodRepository,
});

const pixAccessToken = oidcAuthenticationService.createAccessToken(user.id);
const pixAccessToken = oidcAuthenticationService.createAccessToken({ userId: user.id, audience });

let logoutUrlUUID;
if (oidcAuthenticationService.shouldCloseSession) {
Expand Down Expand Up @@ -109,8 +111,8 @@ async function _updateAuthenticationMethodWithComplement({
});
}

async function _assertUserWithPixAdminAccess({ audience, userId, adminMemberRepository }) {
if (audience === PIX_ADMIN.AUDIENCE) {
async function _assertUserWithPixAdminAccess({ target, userId, adminMemberRepository }) {
if (target === PIX_ADMIN.TARGET) {
const adminMember = await adminMemberRepository.get({ userId });
if (!adminMember?.hasAccessToAdminScope) {
throw new ForbiddenAccess(
Expand Down
Loading
Loading