Skip to content

Commit

Permalink
feat(api): demonstrate audience setting with origin
Browse files Browse the repository at this point in the history
  • Loading branch information
lego-technix committed Dec 17, 2024
1 parent 965ce2e commit 5bdfacf
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 41 deletions.
22 changes: 21 additions & 1 deletion api/lib/infrastructure/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.warn('\n\ndecodedAccessToken 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 });
Expand Down Expand Up @@ -93,4 +113,4 @@ const authentication = {
defaultStrategy: 'jwt-user',
};

export { authentication };
export { authentication, getOriginFromHeaders };
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getOriginFromHeaders } from '../../../../lib/infrastructure/authentication.js';
import { BadRequestError, UnauthorizedError } from '../../../shared/application/http-errors.js';
import { requestResponseUtils } from '../../../shared/infrastructure/utils/request-response-utils.js';
import { usecases } from '../../domain/usecases/index.js';
Expand All @@ -11,7 +12,9 @@ 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 audience = getOriginFromHeaders(request.headers);
console.warn('oidcProviderController.authenticateOidcUser audience:', audience);
const { code, state, iss, identityProvider: identityProviderCode } = request.deserializedPayload;

const sessionState = request.yar.get('state', true);
const nonce = request.yar.get('nonce', true);
Expand All @@ -32,6 +35,8 @@ async function authenticateOidcUser(request, h) {
});

if (result.isAuthenticationComplete) {
console.warn('oidcProviderController.authenticateOidcUser pixAccessToken:', result.pixAccessToken);

return h.response({ access_token: result.pixAccessToken, logout_url_uuid: result.logoutUrlUUID }).code(200);
}

Expand All @@ -55,13 +60,17 @@ async function authenticateOidcUser(request, h) {
* @return {Promise<{access_token: string, logout_url_uuid: string}>}
*/
async function createUser(request, h, dependencies = { requestResponseUtils }) {
const audience = getOriginFromHeaders(request.headers);
console.warn('oidcProviderController.createUser audience:', audience);

const { identityProvider, authenticationKey } = request.deserializedPayload;
const localeFromCookie = request.state?.locale;
const language = dependencies.requestResponseUtils.extractLocaleFromRequest(request);

const { accessToken: access_token, logoutUrlUUID: logout_url_uuid } = await usecases.createOidcUser({
authenticationKey,
identityProvider,
audience,
localeFromCookie,
language,
});
Expand Down Expand Up @@ -144,11 +153,15 @@ async function getRedirectLogoutUrl(request, h) {
* @return {Promise<{access_token: string, logout_url_uuid: string}>}
*/
async function reconcileUser(request, h) {
const audience = getOriginFromHeaders(request.headers);
console.warn('oidcProviderController.reconcileUser audience:', audience);

const { identityProvider, authenticationKey } = request.deserializedPayload;

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

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
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ export class OidcAuthenticationServiceRegistry {
return this.#readyOidcProviderServicesForPixAdmin;
}

getOidcProviderServiceByCode({ identityProviderCode, audience = 'app' }) {
const services =
audience === 'admin' ? this.#readyOidcProviderServicesForPixAdmin : this.#readyOidcProviderServices;
getOidcProviderServiceByCode({ identityProviderCode, audience }) {
// const audienceUrl = new URL(audience)
// const services =
// audience === 'admin' ? this.#readyOidcProviderServicesForPixAdmin : this.#readyOidcProviderServices;
// TODO: Do a clean separation of the different OIDC providers by application
const services = [...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,16 @@ export class OidcAuthenticationService {
}
}

createAccessToken(userId) {
return jsonwebtoken.sign({ user_id: userId }, config.authentication.secret, this.accessTokenJwtOptions);
// TODO: Use an object for arguments instead
// TODO: Use the tokenService to read-write tokens
createAccessToken(userId, audience) {
console.warn('OidcAuthenticationService.createAccessToken audience:', audience);

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

async saveIdToken({ idToken, userId }) {
Expand Down Expand Up @@ -198,6 +206,7 @@ export class OidcAuthenticationService {
}

async getUserInfo({ idToken, accessToken }) {
// TODO: Use the tokenService to read-write tokens
let userInfo = jsonwebtoken.decode(idToken);

if (this.claimManager.hasMissingClaims(userInfo)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ async function authenticateOidcUser({
userLoginRepository,
userRepository,
}) {
console.warn('usecases.authenticateOidcUser audience:', audience);

await oidcAuthenticationServiceRegistry.loadOidcProviderServices();
await oidcAuthenticationServiceRegistry.configureReadyOidcProviderServiceByCode(identityProviderCode);

const oidcAuthenticationService = oidcAuthenticationServiceRegistry.getOidcProviderServiceByCode({
identityProviderCode,
audience,
});
console.warn('usecases.authenticateOidcUser oidcAuthenticationService:', oidcAuthenticationService);

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

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

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

const pixAccessToken = oidcAuthenticationService.createAccessToken(user.id);
const pixAccessToken = oidcAuthenticationService.createAccessToken(user.id, audience);

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

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

const authenticateUser = async function ({
password,
audience,
scope,
source,
username,
Expand All @@ -16,6 +17,8 @@ const authenticateUser = async function ({
userLoginRepository,
adminMemberRepository,
}) {
console.warn('usecases.authenticateUser audience:', audience);

try {
const foundUser = await pixAuthenticationService.getUserByUsernameAndPassword({
username,
Expand All @@ -28,12 +31,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.warn('authenticateUser accessToken:', accessToken);

foundUser.setLocaleIfNotAlreadySet(localeFromCookie);
if (foundUser.hasBeenModified) {
Expand All @@ -56,17 +64,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 };
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ async function createOidcUser({
authenticationKey,
localeFromCookie,
language,
audience,
authenticationSessionService,
oidcAuthenticationServiceRegistry,
authenticationMethodRepository,
userToCreateRepository,
userLoginRepository,
}) {
console.warn('usecases.createOidcUser audience:', audience);

const sessionContentAndUserInfo = await authenticationSessionService.getByKey(authenticationKey);
if (!sessionContentAndUserInfo) {
throw new AuthenticationKeyExpired();
Expand Down Expand Up @@ -68,7 +71,7 @@ async function createOidcUser({
authenticationMethodRepository,
});

const accessToken = oidcAuthenticationService.createAccessToken(userId);
const accessToken = oidcAuthenticationService.createAccessToken(userId, audience);

let logoutUrlUUID;
if (oidcAuthenticationService.shouldCloseSession) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import { AuthenticationMethod } from '../models/AuthenticationMethod.js';
export const reconcileOidcUser = async function ({
authenticationKey,
identityProvider,
audience,
authenticationSessionService,
authenticationMethodRepository,
oidcAuthenticationServiceRegistry,
userLoginRepository,
}) {
console.warn('usecases.createOidcUser audience:', audience);

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

Expand Down Expand Up @@ -52,7 +55,7 @@ export const reconcileOidcUser = async function ({
}),
});

const accessToken = await oidcAuthenticationService.createAccessToken(userId);
const accessToken = await oidcAuthenticationService.createAccessToken(userId, audience);

let logoutUrlUUID;
if (oidcAuthenticationService.shouldCloseSession) {
Expand Down
9 changes: 5 additions & 4 deletions api/src/shared/domain/services/token-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.warn('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 };
}

Expand Down
Loading

0 comments on commit 5bdfacf

Please sign in to comment.