Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 10 additions & 8 deletions .vite/helpers/create-defra-id-test-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const VALID_DEFRA_AUDIENCE = 'test-defra'
const USER_EMAIL = '[email protected]'

// Generate key pair once at module load time
// @ts-ignore - @types/node is missing generateKeyPairSync overloads for jwk format (incomplete fix in PR #63492)
// @ts-ignore
const keyPair = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
Expand All @@ -20,9 +20,10 @@ const keyPair = generateKeyPairSync('rsa', {
})

const privateKey = keyPair.privateKey
/** @type {import('crypto').JsonWebKey & {kid: string, use: string, alg: string}} */

/** @type {import('crypto').JsonWebKey & {kid: string, use: string, alg: string}} */
export const publicKey = {
// @ts-ignore - keyPair.publicKey is JsonWebKey but spread causes type issues
...keyPair.publicKey,

// Add JWKS-required fields to the public key
Expand All @@ -31,18 +32,19 @@ export const publicKey = {
alg: 'RS256'
}

/** @type {import('../../src/common/helpers/auth/types.js').DefraIdTokenPayload} */
export const baseDefraIdTokenPayload = {
name: 'John Doe',
id: 'test-contact-id',
email: USER_EMAIL,
aud: VALID_DEFRA_AUDIENCE,
iss: `https://dcidmtest.b2clogin.com/DCIDMTest.onmicrosoft.com/v2.0`,
firstName: 'John',
lastName: 'Doe',
currentRelationshipId: 'rel-1',
relationships: ['rel-1'],
nbf: new Date().getTime() / 1000,
iss: `https://dcidmtest.b2clogin.com/DCIDMTest.onmicrosoft.com/v2.0`,
aud: VALID_DEFRA_AUDIENCE,
exp: new Date().getTime() / 1000 + 3600,
maxAgeSec: 3600, // 60 minutes
timeSkewSec: 15
iat: new Date().getTime() / 1000,
nbf: new Date().getTime() / 1000
}

/** @type {{key: string, algorithm: 'RS256'}} */
Expand Down
35 changes: 35 additions & 0 deletions src/common/helpers/auth/add-user-if-not-initial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ROLES } from '#common/helpers/auth/constants.js'

/** @typedef {import('./types.js').DefraIdTokenPayload} DefraIdTokenPayload */

/**
* Adds a user to an organisation if they are not the initial user
* @param {Object} request - The Hapi request object
* @param {DefraIdTokenPayload} tokenPayload - The Defra ID token payload containing user information
* @param {Object} organisationById - The organisation object
* @returns {Promise<void>}
*/
export const addUserIfNotInitial = async (
request,
tokenPayload,
organisationById
) => {
const { organisationsRepository } = request
const { email, firstName, lastName } = tokenPayload

await organisationsRepository.update(
organisationById.id,
organisationById.version,
{
users: [
...organisationById.users,
{
email,
fullName: `${firstName} ${lastName}`,
isInitialUser: false,
roles: [ROLES.standardUser]
}
]
}
)
}
17 changes: 11 additions & 6 deletions src/common/helpers/auth/get-defra-user-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { getUsersOrganisationInfo } from './get-users-org-info.js'
import { getRolesForOrganisationAccess } from './get-roles-for-org-access.js'

/** @typedef {import('#repositories/organisations/port.js').OrganisationsRepository} OrganisationsRepository */
/** @typedef {import('./types.js').DefraIdTokenPayload} DefraIdTokenPayload */

/**
* @param {Object} tokenPayload
* @param {string} tokenPayload.id
* @param {string} tokenPayload.email
* @param {import('#common/hapi-types.js').HapiRequest & {organisationsRepository: OrganisationsRepository}} request
* @returns {Promise<string[]>}
* Determines the roles for a Defra ID user based on their token and request context
* @param {DefraIdTokenPayload} tokenPayload - The Defra ID token payload
* @param {import('#common/hapi-types.js').HapiRequest & {organisationsRepository: OrganisationsRepository}} request - The Hapi request object
* @returns {Promise<string[]>} Array of role strings
*/
export async function getDefraUserRoles(tokenPayload, request) {
const { email } = tokenPayload
Expand Down Expand Up @@ -46,7 +46,12 @@ export async function getDefraUserRoles(tokenPayload, request) {
// - the request does not have an organisationId param
// - or if the linkedEprOrg does not match the organisationId param
// - or if the organisation status is not accessible
const roles = await getRolesForOrganisationAccess(request, linkedEprOrg)
// Adds the user to the organisation if they are not already present
const roles = await getRolesForOrganisationAccess(
request,
linkedEprOrg,
tokenPayload
)

return roles
}
6 changes: 4 additions & 2 deletions src/common/helpers/auth/get-defra-user-roles.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ describe('#getDefraUserRoles', () => {
)
expect(mockGetRolesForOrganisationAccess).toHaveBeenCalledWith(
mockRequest,
mockLinkedEprOrg
mockLinkedEprOrg,
tokenPayload
)
})

Expand Down Expand Up @@ -231,7 +232,8 @@ describe('#getDefraUserRoles', () => {

expect(mockGetRolesForOrganisationAccess).toHaveBeenCalledWith(
customRequest,
mockLinkedEprOrg
mockLinkedEprOrg,
tokenPayload
)
expect(mockGetRolesForOrganisationAccess).toHaveBeenCalledTimes(1)
})
Expand Down
7 changes: 7 additions & 0 deletions src/common/helpers/auth/get-entra-user-roles.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { ROLES } from '#common/helpers/auth/constants.js'
import { getConfig } from '#root/config.js'

/** @typedef {import('./types.js').EntraIdTokenPayload} EntraIdTokenPayload */

/**
* Determines the roles for an Entra ID user based on their token
* @param {EntraIdTokenPayload} tokenPayload - The Entra ID token payload
* @returns {Promise<string[]>} Array of role strings
*/
export async function getEntraUserRoles(tokenPayload) {
const userEmail = tokenPayload.email || tokenPayload.preferred_username

Expand Down
11 changes: 11 additions & 0 deletions src/common/helpers/auth/get-jwt-strategy-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import Boom from '@hapi/boom'
import { getDefraUserRoles } from './get-defra-user-roles.js'
import { getEntraUserRoles } from './get-entra-user-roles.js'

/** @typedef {import('./types.js').TokenPayload} TokenPayload */
/** @typedef {import('./types.js').EntraIdTokenPayload} EntraIdTokenPayload */
/** @typedef {import('./types.js').DefraIdTokenPayload} DefraIdTokenPayload */

/**
* Configures JWT authentication strategy for both Entra ID and Defra ID
* @param {Object} oidcConfigs - OIDC configuration for both identity providers
* @param {Object} oidcConfigs.entraIdOidcConfig - Entra ID OIDC configuration
* @param {Object} oidcConfigs.defraIdOidcConfig - Defra ID OIDC configuration
* @returns {Object} JWT strategy configuration object
*/
export function getJwtStrategyConfig(oidcConfigs) {
const { entraIdOidcConfig, defraIdOidcConfig } = oidcConfigs

Expand Down
18 changes: 13 additions & 5 deletions src/common/helpers/auth/get-roles-for-org-access.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import Boom from '@hapi/boom'
import { STATUS } from '#domain/organisations/model.js'
import { ROLES } from '#common/helpers/auth/constants.js'
import { addUserIfNotInitial } from './add-user-if-not-initial.js'

/** @typedef {import('#repositories/organisations/port.js').OrganisationsRepository} OrganisationsRepository */
/** @typedef {import('./types.js').DefraIdTokenPayload} DefraIdTokenPayload */

/**
* @param {import('#common/hapi-types.js').HapiRequest & {organisationsRepository: OrganisationsRepository}} request
* @param {string} linkedEprOg
* @returns {Promise<string[]>}
* Determines roles for organization access based on token and organization status
* @param {import('#common/hapi-types.js').HapiRequest & {organisationsRepository: OrganisationsRepository}} request - The Hapi request object
* @param {string} linkedEprOg - The linked EPR organization ID
* @param {DefraIdTokenPayload} tokenPayload - The Defra ID token payload
* @returns {Promise<string[]>} Array of role strings
*/
export const getRolesForOrganisationAccess = async (request, linkedEprOg) => {
export const getRolesForOrganisationAccess = async (
request,
linkedEprOg,
tokenPayload
) => {
const { organisationId } = request.params

if (!organisationId) {
Expand All @@ -32,7 +40,7 @@ export const getRolesForOrganisationAccess = async (request, linkedEprOg) => {
throw Boom.forbidden('Access denied: organisation status not accessible')
}

// Placeholder for checking whether the user is part of the EPROrganisation
addUserIfNotInitial(request, tokenPayload, organisationById)

return [ROLES.standardUser]
}
Loading