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] Rendre le processus d'authentifcation OIDC plus résistant aux retours arrière (PIX-13945) #11905

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
Open
3 changes: 1 addition & 2 deletions admin/app/services/oidc-identity-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export default class OidcIdentityProviders extends Service {
}

isProviderEnabled(identityProviderSlug) {
const oidcIdentityProvider = this.list.find((provider) => provider.id === identityProviderSlug);
return oidcIdentityProvider !== undefined;
return this.list.some((provider) => provider.id === identityProviderSlug);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async function authenticateOidcUser(request, h) {
await request.yar.commit(h);

if (sessionState === null) {
throw new BadRequestError('Required cookie "state" is missing');
throw new BadRequestError('Required "state" is missing in session');
}

const result = await usecases.authenticateOidcUser({
Expand Down
3 changes: 0 additions & 3 deletions api/src/shared/application/error-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,6 @@ function _mapToHttpError(error) {
if (error instanceof SharedDomainErrors.UnexpectedUserAccountError) {
return new HttpErrors.ConflictError(error.message, error.code, error.meta);
}
if (error instanceof SharedDomainErrors.AlreadyExistingEntityError) {
return new HttpErrors.PreconditionFailedError(error.message);
}
if (error instanceof SharedDomainErrors.AlreadyExistingMembershipError) {
return new HttpErrors.PreconditionFailedError(error.message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('Unit | Identity Access Management | Application | Controller | oidc-pr
expect(request.yar.commit).to.have.been.calledOnce;
});

context('when state cookie is missing', function () {
context('when state is missing in session', function () {
it('returns a BadRequestError', async function () {
// given
request.yar.get.returns(null);
Expand All @@ -75,7 +75,7 @@ describe('Unit | Identity Access Management | Application | Controller | oidc-pr

// then
expect(error).to.be.an.instanceOf(BadRequestError);
expect(error.message).to.equal('Required cookie "state" is missing');
expect(error.message).to.equal('Required "state" is missing in session');
});
});

Expand Down
20 changes: 15 additions & 5 deletions mon-pix/app/routes/authentication/login-oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,32 @@ export default class LoginOidcRoute extends Route {
throw error;
}

const identityProviderSlug = transition.to.params.identity_provider_slug.toString();

// Preventing OIDC authentication replay errors when doing history back and reload
// when the user is already authenticated with the same OIDC Provider.
if (this.session.isAuthenticated) {
const identityProvider = this.oidcIdentityProviders[identityProviderSlug];
if (identityProvider.code == this.session.data.identityProviderCode) {
return this.router.replaceWith('authenticated');
}
}

if (!queryParams.code) {
this._cleanSession();

const identityProviderSlug = transition.to.params.identity_provider_slug.toString();
const isSupportedIdentityProvider = this.oidcIdentityProviders[identityProviderSlug] ?? null;
if (isSupportedIdentityProvider) return this._handleRedirectRequest(identityProviderSlug);
if (this.oidcIdentityProviders.isProviderEnabled(identityProviderSlug)) {
return this._handleRedirectRequest(identityProviderSlug);
}

return this.router.replaceWith('authentication.login');
}
}

async model(params, transition) {
const queryParams = transition.to.queryParams;

const identityProviderSlug = params.identity_provider_slug;
if (queryParams.code) {
const identityProviderSlug = params.identity_provider_slug;
return this._handleCallbackRequest(queryParams.code, queryParams.state, queryParams.iss, identityProviderSlug);
}
}
Expand Down
4 changes: 4 additions & 0 deletions mon-pix/app/services/oidc-identity-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export default class OidcIdentityProviders extends Service {
return this.list.some((identityProvider) => !FEATURED_IDENTITY_PROVIDER_CODES.includes(identityProvider.code));
}

isProviderEnabled(identityProviderSlug) {
return this.list.some((provider) => provider.id === identityProviderSlug);
}

async load() {
const oidcIdentityProviders = await this.store.findAll('oidc-identity-provider');
oidcIdentityProviders.map((oidcIdentityProvider) => (this[oidcIdentityProvider.id] = oidcIdentityProvider));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ module('Acceptance | Campaigns | Start Campaigns workflow | OIDC', function (hoo
await clickByLabel('Je commence');

// then
sinon.assert.called(replaceLocationStub);
assert.strictEqual(currentURL(), '/connexion/oidc-partner');
assert.ok(true);
});

test('should redirect to login or register oidc page', async function (assert) {
Expand Down Expand Up @@ -180,9 +178,7 @@ module('Acceptance | Campaigns | Start Campaigns workflow | OIDC', function (hoo
await clickByLabel('Je commence');

// then
sinon.assert.called(replaceLocationStub);
assert.strictEqual(currentURL(), '/connexion/oidc-partner');
assert.ok(true);
});
});
});
Expand Down
5 changes: 5 additions & 0 deletions mon-pix/tests/helpers/service-stubs.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function stubSessionService(owner, sessionData = {}) {
const userIdForLearnerAssociation = sessionData.userIdForLearnerAssociation;
const userId = isAuthenticated ? sessionData.userId || 123 : null;
const source = isAuthenticated ? sessionData.source || 'pix' : null;
const identityProviderCode = sessionData.identityProviderCode;

class SessionStub extends Service {
constructor() {
Expand All @@ -34,6 +35,10 @@ export function stubSessionService(owner, sessionData = {}) {
this.data = {
authenticated: { user_id: userId, source, access_token: 'access_token!' },
};

if (identityProviderCode) {
this.data.identityProviderCode = identityProviderCode;
}
} else {
this.data = {};
}
Expand Down
35 changes: 35 additions & 0 deletions mon-pix/tests/unit/routes/authentication/login-oidc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApplicationError } from 'mon-pix/errors/application-error';
import { module, test } from 'qunit';
import sinon from 'sinon';

import { stubSessionService } from '../../../helpers/service-stubs';
import setupIntl from '../../../helpers/setup-intl';

module('Unit | Route | login-oidc', function (hooks) {
Expand Down Expand Up @@ -36,6 +37,37 @@ module('Unit | Route | login-oidc', function (hooks) {
});
});

module('when the user is already authenticated', function () {
test('redirects the user to the authenticated route', async function (assert) {
// given
const someOidcPartner = {
id: 'some-oidc-provider',
code: 'SOME_OIDC_PARTNER',
};
class OidcIdentityProvidersStub extends Service {
'some-oidc-provider' = someOidcPartner;
list = [someOidcPartner];
isProviderEnabled = (identityProviderSlug) => {
return Boolean(identityProviderSlug == 'some-oidc-provider');
};
}
this.owner.register('service:oidcIdentityProviders', OidcIdentityProvidersStub);

const route = this.owner.lookup('route:authentication/login-oidc');
const replaceWithStub = sinon.stub();
route.router = {
replaceWith: replaceWithStub,
};
stubSessionService(this.owner, { isAuthenticated: true, identityProviderCode: 'SOME_OIDC_PARTNER' });

// when
await route.beforeModel({ to: { queryParams: {}, params: { identity_provider_slug: 'some-oidc-provider' } } });

// then
assert.ok(replaceWithStub.calledWith('authenticated'));
});
});

module('when no code exists in queryParams', function (hooks) {
hooks.beforeEach(function () {
sinon.stub(fetch, 'default').resolves({
Expand All @@ -49,6 +81,9 @@ module('Unit | Route | login-oidc', function (hooks) {
class OidcIdentityProvidersStub extends Service {
'oidc-partner' = oidcPartner;
list = [oidcPartner];
isProviderEnabled = (identityProviderSlug) => {
return Boolean(identityProviderSlug == 'oidc-partner');
};
}

this.owner.register('service:oidcIdentityProviders', OidcIdentityProvidersStub);
Expand Down
38 changes: 38 additions & 0 deletions mon-pix/tests/unit/services/oidc-identity-providers-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,42 @@ module('Unit | Service | oidc-identity-providers', function (hooks) {
});
});
});

module('isProviderEnabled', function (hooks) {
let oidcIdentityProvidersService;

hooks.beforeEach(function () {
const oidcPartner = {
id: 'oidc-partner',
code: 'OIDC_PARTNER',
};

class OidcIdentityProvidersStub extends Service {
'oidc-partner' = oidcPartner;
list = [oidcPartner];
isProviderEnabled = (identityProviderSlug) => {
return Boolean(identityProviderSlug == 'oidc-partner');
};
}

this.owner.register('service:oidcIdentityProviders', OidcIdentityProvidersStub);
oidcIdentityProvidersService = this.owner.lookup('service:oidcIdentityProviders');
});

test('returns true if given provider is in the list', async function (assert) {
// when
const isProviderEnabled = await oidcIdentityProvidersService.isProviderEnabled('oidc-partner');

// then
assert.true(isProviderEnabled);
});

test('returns false if given provider is in not the list', async function (assert) {
// when
const isProviderEnabled = await oidcIdentityProvidersService.isProviderEnabled('disabled-provider');

// then
assert.false(isProviderEnabled);
});
});
});