Skip to content
Open
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
219 changes: 205 additions & 14 deletions serverless/src/edlCallback/__tests__/handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ import * as getConfig from '../../../../sharedUtils/getConfig'
import fetchEdlProfile from '../../utils/fetchEdlProfile'
import createJwt from '../../utils/createJwt'
import * as createCookieModule from '../../utils/createCookie'
import checkNonNasaMMTAccess from '../../utils/checkNonNasaMMTAccess'

beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterAll(() => {
vi.restoreAllMocks()
})

vi.mock('../../utils/checkNonNasaMMTAccess')
const realCreateCookie = createCookieModule.default
vi.mock('simple-oauth2')
vi.mock('../../../../sharedUtils/getConfig', () => {
Expand Down Expand Up @@ -83,7 +93,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand All @@ -93,6 +106,147 @@ describe('edlCallback', () => {
})
})

describe('when handling invalid assurance levels', () => {
test('should redirect to unauthorizedMMTAccess when assurance level is not defined', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

AuthorizationCode.mockImplementation(() => ({
getToken: vi.fn().mockResolvedValue({
token: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_at: '2023-01-01T00:00:00Z'
}
})
}))

fetchEdlProfile.mockResolvedValue({
uid: 'test-user' // No assurance level
})

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=deniedAccessMMT')
})
})

describe('when handling assurance levels less than 4', () => {
test('should redirect to unauthorizedMMTAccess when assurance level is less than 4', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

AuthorizationCode.mockImplementation(() => ({
getToken: vi.fn().mockResolvedValue({
token: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_at: '2023-01-01T00:00:00Z'
}
})
}))

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '3' // Set assurance level to 3
})

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=deniedAccessMMT')
})
})

describe('when handling assurance level 4', () => {
beforeEach(() => {
AuthorizationCode.mockImplementation(() => ({
getToken: vi.fn().mockResolvedValue({
token: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_at: '2023-01-01T00:00:00Z'
}
})
}))
})

test('should redirect to unauthorizedNonNasaMMTAccess when checkNonNasaMMTAccess returns false', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '4'
})

checkNonNasaMMTAccess.mockResolvedValue(false)

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=deniedNonNasaAccessMMT')
expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token')
expect(checkNonNasaMMTAccess).toHaveBeenCalledTimes(1)
})

test('should redirect to auth-callback with target when checkNonNasaMMTAccess returns true for assurance level 4', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '4'
})

checkNonNasaMMTAccess.mockResolvedValue(true)

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/auth-callback?target=%2F')
expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token')
expect(checkNonNasaMMTAccess).toHaveBeenCalledTimes(1)
})

test('should throw an error when checkNonNasaMMTAccess fails', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '4'
})

checkNonNasaMMTAccess.mockRejectedValue(new Error('Failed to check access'))

await expect(edlCallback(mockEvent)).rejects.toThrow('Failed to check access')
expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token')
expect(checkNonNasaMMTAccess).toHaveBeenCalledTimes(1)
})
})

describe('when running in offline mode', () => {
test('should issue mock tokens without calling EDL', async () => {
process.env.IS_OFFLINE = 'true'
Expand Down Expand Up @@ -191,7 +345,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand All @@ -216,7 +373,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand All @@ -243,11 +403,16 @@ describe('edlCallback', () => {
getToken: vi.fn().mockResolvedValue({ token: mockToken })
}))

const mockEdlProfile = { uid: 'test-user' }
const mockEdlProfile = {
uid: 'test-user',
assuranceLevel: 5
}
fetchEdlProfile.mockResolvedValue(mockEdlProfile)

await edlCallback(mockEvent)
const expectedExpirationSeconds = Math.floor(new Date(mockToken.expires_at).getTime() / 1000)
const expectedExpirationSeconds = Math.floor(
new Date(mockToken.expires_at).getTime() / 1000
)

expect(createJwt).toHaveBeenCalledWith(
mockToken.access_token,
Expand Down Expand Up @@ -304,7 +469,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -334,7 +502,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -363,7 +534,10 @@ describe('edlCallback', () => {
getToken: mockGetToken
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

await edlCallback(mockEvent)

Expand Down Expand Up @@ -433,7 +607,7 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({})
fetchEdlProfile.mockResolvedValue({ assuranceLevel: 5 })

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -461,7 +635,10 @@ describe('edlCallback', () => {
})
}))

const mockEdlProfile = { uid: 'test-user' }
const mockEdlProfile = {
uid: 'test-user',
assuranceLevel: 5
}
fetchEdlProfile.mockResolvedValue(mockEdlProfile)

await edlCallback(mockEvent)
Expand Down Expand Up @@ -505,7 +682,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -538,7 +718,11 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

createJwt.mockImplementation(() => {
throw new Error('JWT creation failed')
})
Expand All @@ -564,7 +748,11 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

createJwt.mockReturnValue('test-jwt')
createCookieSpy.mockImplementation(() => {
throw new Error('Cookie creation failed')
Expand Down Expand Up @@ -593,7 +781,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down
43 changes: 43 additions & 0 deletions serverless/src/edlCallback/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getApplicationConfig, getEdlConfig } from '../../../sharedUtils/getConf
import fetchEdlProfile from '../utils/fetchEdlProfile'
import createJwt from '../utils/createJwt'
import createCookie from '../utils/createCookie'
import checkNonNasaMMTAccess from '../utils/checkNonNasaMMTAccess'

/**
* Handles the EDL callback during authentication
Expand Down Expand Up @@ -72,6 +73,48 @@ const edlCallback = async (event) => {
expiresAt = token.expires_at

edlProfile = await fetchEdlProfile(oauthToken)

let { assuranceLevel } = edlProfile
// Check if assuranceLevel is not already a number
if (typeof assuranceLevel !== 'number') {
assuranceLevel = Number(assuranceLevel)
}

// Define the minimum required assurance level
const MINIMUM_ASSURANCE_LEVEL = 4

// If assuranceLevel is undefined, not a valid number or a number smaller than MINIMUM_ASSURANCE_LEVEL
// then show access denied error page
if (!Number.isFinite(assuranceLevel) || assuranceLevel < MINIMUM_ASSURANCE_LEVEL) {
console.log(`Invalid or insufficient assurance level: ${assuranceLevel}`)

return {
statusCode: 303,
headers: {
Location: `${mmtHost}/unauthorizedAccess?errorType=deniedAccessMMT`
}
}
}

// If assuranceLevel is MINIMUM_ASSURANCE_LEVEL then check for non NASA access role
if (assuranceLevel === MINIMUM_ASSURANCE_LEVEL) {
try {
const hasNonNasaMMTAccess = await checkNonNasaMMTAccess(edlProfile.uid, accessToken)
if (!hasNonNasaMMTAccess) {
console.log('User does not have Non-NASA MMT access')

return {
statusCode: 303,
headers: {
Location: `${mmtHost}/unauthorizedAccess?errorType=deniedNonNasaAccessMMT`
}
}
}
} catch (error) {
console.error('Error checking Non-NASA MMT access:', error)
throw error
}
}
}

// Create JWT with EDL token and edl profile
Expand Down
Loading