From c992002e6e5eca735a9ee17161a83cb7de5fc46d Mon Sep 17 00:00:00 2001 From: LEGO Technix <109212476+lego-technix@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:41:07 +0100 Subject: [PATCH] feat(api): demonstrate audience setting with origin --- api/lib/infrastructure/authentication.js | 22 +++++- .../application/token/token.controller.js | 6 +- .../domain/usecases/authenticate-user.js | 32 ++++---- .../shared/domain/services/token-service.js | 9 ++- .../infrastructure/authentication_test.js | 74 +++++++++++++++++++ 5 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 api/tests/unit/infrastructure/authentication_test.js diff --git a/api/lib/infrastructure/authentication.js b/api/lib/infrastructure/authentication.js index c36f6f690b8..155b538bdbc 100644 --- a/api/lib/infrastructure/authentication.js +++ b/api/lib/infrastructure/authentication.js @@ -7,6 +7,20 @@ import boom from '@hapi/boom'; import { config } from '../../src/shared/config.js'; import { tokenService } from '../../src/shared/domain/services/token-service.js'; +function getOriginFromHeaders(headers) { + const urlParts = []; + + urlParts.push(_getHeaderFirstValue(headers['x-forwarded-proto'])); + urlParts.push('://'); + urlParts.push(_getHeaderFirstValue(headers['x-forwarded-host'])); + + return urlParts.join(''); +} + +function _getHeaderFirstValue(headerValue) { + return headerValue.split(',')[0]; +} + async function _checkIsAuthenticated(request, h, { key, validate }) { if (!request.headers.authorization) { return boom.unauthorized(null, 'jwt'); @@ -21,6 +35,12 @@ async function _checkIsAuthenticated(request, h, { key, validate }) { const decodedAccessToken = tokenService.getDecodedToken(accessToken, key); if (decodedAccessToken) { + const audience = getOriginFromHeaders(request.headers); + console.log('\n\n\n audience:', audience, 'decodedAccessToken.aud:', decodedAccessToken.aud); + if (decodedAccessToken.aud != audience) { + return { isValid: false, errorCode: 403 }; + } + const { isValid, credentials, errorCode } = validate(decodedAccessToken, request, h); if (isValid) { return h.authenticated({ credentials }); @@ -93,4 +113,4 @@ const authentication = { defaultStrategy: 'jwt-user', }; -export { authentication }; +export { authentication, getOriginFromHeaders }; diff --git a/api/src/identity-access-management/application/token/token.controller.js b/api/src/identity-access-management/application/token/token.controller.js index 86950535bb7..09c7ff82057 100644 --- a/api/src/identity-access-management/application/token/token.controller.js +++ b/api/src/identity-access-management/application/token/token.controller.js @@ -1,3 +1,4 @@ +import { getOriginFromHeaders } from '../../../../lib/infrastructure/authentication.js'; import { BadRequestError } from '../../../shared/application/http-errors.js'; import { tokenService } from '../../../shared/domain/services/token-service.js'; import { usecases } from '../../domain/usecases/index.js'; @@ -30,11 +31,12 @@ const createToken = async function (request, h, dependencies = { tokenService }) const scope = request.payload.scope; if (grantType === 'password') { - const { username, password, scope } = request.payload; + const { username, password } = request.payload; const localeFromCookie = request.state?.locale; + const audience = getOriginFromHeaders(request.headers); const source = 'pix'; - const tokensInfo = await usecases.authenticateUser({ username, password, scope, source, localeFromCookie }); + const tokensInfo = await usecases.authenticateUser({ username, password, audience, source, localeFromCookie }); accessToken = tokensInfo.accessToken; refreshToken = tokensInfo.refreshToken; diff --git a/api/src/identity-access-management/domain/usecases/authenticate-user.js b/api/src/identity-access-management/domain/usecases/authenticate-user.js index 091c3ed872f..5eb8fb29412 100644 --- a/api/src/identity-access-management/domain/usecases/authenticate-user.js +++ b/api/src/identity-access-management/domain/usecases/authenticate-user.js @@ -5,6 +5,7 @@ import { RefreshToken } from '../models/RefreshToken.js'; const authenticateUser = async function ({ password, + audience, scope, source, username, @@ -28,12 +29,17 @@ const authenticateUser = async function ({ throw new UserShouldChangePasswordError(undefined, passwordResetToken); } - await _checkUserAccessScope(scope, foundUser, adminMemberRepository); + // await _checkUserAccessScope(scope, foundUser, adminMemberRepository); const refreshToken = RefreshToken.generate({ userId: foundUser.id, scope, source }); await refreshTokenRepository.save({ refreshToken }); - const { accessToken, expirationDelaySeconds } = await tokenService.createAccessTokenFromUser(foundUser.id, source); + const { accessToken, expirationDelaySeconds } = await tokenService.createAccessTokenFromUser({ + userId: foundUser.id, + audience, + source, + }); + console.log('authenticateUser accessToken:', accessToken); foundUser.setLocaleIfNotAlreadySet(localeFromCookie); if (foundUser.hasBeenModified) { @@ -56,17 +62,17 @@ const authenticateUser = async function ({ } }; -async function _checkUserAccessScope(scope, user, adminMemberRepository) { - if (scope === PIX_ORGA.SCOPE && !user.isLinkedToOrganizations()) { - throw new ForbiddenAccess(PIX_ORGA.NOT_LINKED_ORGANIZATION_MSG); - } +// async function _checkUserAccessScope(scope, user, adminMemberRepository) { +// if (scope === PIX_ORGA.SCOPE && !user.isLinkedToOrganizations()) { +// throw new ForbiddenAccess(PIX_ORGA.NOT_LINKED_ORGANIZATION_MSG); +// } - if (scope === PIX_ADMIN.SCOPE) { - const adminMember = await adminMemberRepository.get({ userId: user.id }); - if (!adminMember?.hasAccessToAdminScope) { - throw new ForbiddenAccess(PIX_ADMIN.NOT_ALLOWED_MSG); - } - } -} +// if (scope === PIX_ADMIN.SCOPE) { +// const adminMember = await adminMemberRepository.get({ userId: user.id }); +// if (!adminMember?.hasAccessToAdminScope) { +// throw new ForbiddenAccess(PIX_ADMIN.NOT_ALLOWED_MSG); +// } +// } +// } export { authenticateUser }; diff --git a/api/src/shared/domain/services/token-service.js b/api/src/shared/domain/services/token-service.js index 086c6cd7f8e..2235c8b415e 100644 --- a/api/src/shared/domain/services/token-service.js +++ b/api/src/shared/domain/services/token-service.js @@ -11,15 +11,16 @@ import { const CERTIFICATION_RESULTS_BY_RECIPIENT_EMAIL_LINK_SCOPE = 'certificationResultsByRecipientEmailLink'; -function _createAccessToken({ userId, source, expirationDelaySeconds }) { - return jsonwebtoken.sign({ user_id: userId, source }, config.authentication.secret, { +function _createAccessToken({ userId, audience, source, expirationDelaySeconds }) { + return jsonwebtoken.sign({ user_id: userId, aud: audience, source }, config.authentication.secret, { expiresIn: expirationDelaySeconds, }); } -function createAccessTokenFromUser(userId, source) { +function createAccessTokenFromUser({ userId, audience, source }) { + console.log('createAccessTokenFromUser audience:', audience); const expirationDelaySeconds = config.authentication.accessTokenLifespanMs / 1000; - const accessToken = _createAccessToken({ userId, source, expirationDelaySeconds }); + const accessToken = _createAccessToken({ userId, audience, source, expirationDelaySeconds }); return { accessToken, expirationDelaySeconds }; } diff --git a/api/tests/unit/infrastructure/authentication_test.js b/api/tests/unit/infrastructure/authentication_test.js new file mode 100644 index 00000000000..755cfd88438 --- /dev/null +++ b/api/tests/unit/infrastructure/authentication_test.js @@ -0,0 +1,74 @@ +import { getOriginFromHeaders } from '../../../lib/infrastructure/authentication.js'; +import { expect } from '../../../tests/test-helper.js'; + +describe('Unit | Infrastructure | Authentication', function () { + describe('#getOriginFromHeaders', function () { + context('when port is HTTP standard port 80', function () { + it('returns an HTTP URL', async function () { + // given + const headers = { + 'x-forwarded-proto': 'http', + 'x-forwarded-port': '80', + 'x-forwarded-host': 'localhost', + }; + + // when + const origin = getOriginFromHeaders(headers); + + // then + expect(origin).to.equal('http://localhost'); + }); + }); + + context('when port is HTTPS standard port 443', function () { + it('returns an HTTPS URL', async function () { + // given + const headers = { + 'x-forwarded-proto': 'https', + 'x-forwarded-port': '443', + 'x-forwarded-host': 'app-pr10823.review.pix.fr', + }; + + // when + const origin = getOriginFromHeaders(headers); + + // then + expect(origin).to.equal('https://app-pr10823.review.pix.fr'); + }); + }); + + context('when port is neither HTTP nor HTTPS standard ports', function () { + it('returns an URL with a specific port', async function () { + // given + const headers = { + 'x-forwarded-proto': 'http', + 'x-forwarded-port': '4200', + 'x-forwarded-host': 'localhost:4200', + }; + + // when + const origin = getOriginFromHeaders(headers); + + // then + expect(origin).to.equal('http://localhost:4200'); + }); + }); + }); + + context('when x-forwarded-proto and x-forwarded-port have multiple values (ember serve --proxy)', function () { + it('returns an URL corresponding to the first HTTP proxy facing the user', async function () { + // given + const headers = { + 'x-forwarded-proto': 'https,http', + 'x-forwarded-port': '80', + 'x-forwarded-host': 'app.dev.pix.org', + }; + + // when + const origin = getOriginFromHeaders(headers); + + // then + expect(origin).to.equal('https://app.dev.pix.org'); + }); + }); +});