diff --git a/api/lib/infrastructure/authentication.js b/api/lib/infrastructure/authentication.js index cd4ac21f616..105811b76c2 100644 --- a/api/lib/infrastructure/authentication.js +++ b/api/lib/infrastructure/authentication.js @@ -1,6 +1,7 @@ import boom from '@hapi/boom'; import lodash from 'lodash'; +import { revokedUserAccessRepository } from '../../src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js'; import { getForwardedOrigin } from '../../src/identity-access-management/infrastructure/utils/network.js'; import { config } from '../../src/shared/config.js'; import { tokenService } from '../../src/shared/domain/services/token-service.js'; @@ -90,7 +91,13 @@ async function _checkIsAuthenticated(request, h, { key, validate }) { // Only tokens including user_id are User Access Tokens. // This is why applications Access Tokens are not subject to audience validation for now. - if (decodedAccessToken.user_id && config.featureToggles.isUserTokenAudConfinementEnabled) { + const userId = decodedAccessToken.user_id; + if (config.featureToggles.isUserTokenAudConfinementEnabled && userId) { + const revokedUserAccess = await revokedUserAccessRepository.findByUserId(userId); + if (revokedUserAccess.isAccessTokenRevoked(decodedAccessToken)) { + return boom.unauthorized(); + } + const audience = getForwardedOrigin(request.headers); if (decodedAccessToken.aud !== audience) { return boom.unauthorized(); diff --git a/api/tests/unit/infrastructure/authentication_test.js b/api/tests/unit/infrastructure/authentication_test.js index 623c3c51d48..6ea34e8f77f 100644 --- a/api/tests/unit/infrastructure/authentication_test.js +++ b/api/tests/unit/infrastructure/authentication_test.js @@ -1,4 +1,6 @@ import { authentication } from '../../../lib/infrastructure/authentication.js'; +import { RevokedUserAccess } from '../../../src/identity-access-management/domain/models/RevokedUserAccess.js'; +import { revokedUserAccessRepository } from '../../../src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js'; import { tokenService } from '../../../src/shared/domain/services/token-service.js'; import { expect, sinon } from '../../test-helper.js'; @@ -115,6 +117,45 @@ describe('Unit | Infrastructure | Authentication', function () { }); }); + describe('when the user access is revoked', function () { + it('should throw an error', async function () { + // given + const date = new Date(); + const revokedUserAccess = new RevokedUserAccess(date.getTime() / 1000); + sinon.stub(revokedUserAccessRepository, 'findByUserId').resolves(revokedUserAccess); + sinon.stub(revokedUserAccess, 'isAccessTokenRevoked').returns(true); + + const request = { + headers: { + authorization: 'Bearer token', + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'app.pix.fr', + }, + }; + const h = { authenticated: sinon.stub() }; + tokenService.extractTokenFromAuthChain.withArgs('Bearer token').returns('token'); + tokenService.getDecodedToken.withArgs('token', 'dummy-secret').returns({ + user_id: 'user_id', + aud: 'https://app.pix.fr', + }); + + // when + const { authenticate } = authentication.scheme(undefined, { + key: 'dummy-secret', + validate: sinon.stub().returns({ isValid: true, credentials: {}, errorCode: null }), + }); + const response = await authenticate(request, h); + + // then + expect(h.authenticated).to.not.have.been.called; + expect(response.output.payload).to.include({ + statusCode: 401, + error: 'Unauthorized', + message: 'Unauthorized', + }); + }); + }); + describe('when there is no forwarded origin in the request', function () { it('should throw an error', async function () { // given