From 5ccdf097bc8f88f3a566c182f07e0d94c4219216 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Mon, 6 Jul 2026 12:23:32 -0400 Subject: [PATCH 1/7] KMS-690: Added logging to debug auth issue --- .../edlAuthorizer/__tests__/handler.test.js | 36 ++++++- serverless/src/edlAuthorizer/handler.js | 101 +++++++++++++++++- serverless/src/shared/fetchEdlProfile.js | 83 +++++++++++++- 3 files changed, 210 insertions(+), 10 deletions(-) diff --git a/serverless/src/edlAuthorizer/__tests__/handler.test.js b/serverless/src/edlAuthorizer/__tests__/handler.test.js index 14743511..ef06d552 100644 --- a/serverless/src/edlAuthorizer/__tests__/handler.test.js +++ b/serverless/src/edlAuthorizer/__tests__/handler.test.js @@ -8,9 +8,11 @@ vi.mock('@/shared/fetchEdlProfile') describe('edlAuthorizer', () => { const OLD_ENV = process.env let loggerErrorSpy + let loggerInfoSpy beforeEach(() => { loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}) + loggerInfoSpy = vi.spyOn(logger, 'info').mockImplementation(() => {}) process.env = { ...OLD_ENV } fetchEdlProfile.mockReset() fetchEdlProfile.mockResolvedValue({ @@ -28,6 +30,7 @@ describe('edlAuthorizer', () => { afterAll(() => { loggerErrorSpy?.mockRestore() + loggerInfoSpy?.mockRestore() }) describe('when the token is for a valid user', () => { @@ -117,7 +120,13 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('Authorization failed: No uid found in profile') + expect(logger.error).toHaveBeenCalledWith('Authorization failed: No uid found in profile', expect.objectContaining({ + profile: expect.objectContaining({ + uid: undefined + }), + tokenPresent: false, + tokenType: 'missing' + })) }) }) @@ -147,7 +156,12 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('EDL Authorizer error:', unauthorizedError) + expect(logger.error).toHaveBeenCalledWith('EDL Authorizer error:', unauthorizedError, expect.objectContaining({ + errorMessage: 'Unauthorized', + errorName: 'Error', + tokenPresent: false, + tokenType: 'missing' + })) }) }) @@ -179,7 +193,14 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level 3 below required 5') + expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level 3 below required 5', expect.objectContaining({ + parsedAssuranceLevel: 3, + requiredAssuranceLevel: 5, + profile: expect.objectContaining({ + uid: 'mock_user', + assuranceLevel: 3 + }) + })) }) test('returns a deny policy when assurance level missing', async () => { @@ -208,7 +229,14 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level missing from profile') + expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level missing from profile', expect.objectContaining({ + rawAssuranceLevel: undefined, + rawAssuranceLevelType: 'undefined', + profile: expect.objectContaining({ + uid: 'mock_user', + assuranceLevel: undefined + }) + })) }) }) }) diff --git a/serverless/src/edlAuthorizer/handler.js b/serverless/src/edlAuthorizer/handler.js index 04a94d52..4ea2e815 100644 --- a/serverless/src/edlAuthorizer/handler.js +++ b/serverless/src/edlAuthorizer/handler.js @@ -5,8 +5,73 @@ import { logger } from '@/shared/logger' const REQUIRED_ASSURANCE_LEVEL = 5 +/** + * Builds a request summary safe for authorization logs. + * + * @param {Object} event API Gateway authorizer event. + * @returns {Object} Redacted request summary. + */ +const summarizeEventForLogs = (event = {}) => { + const { + headers = {}, + authorizationToken, + methodArn + } = event + const normalizedHeaders = downcaseKeys(headers) + const headerToken = normalizedHeaders.authorization || '' + const eventToken = authorizationToken || '' + + return { + methodArnPresent: Boolean(methodArn), + headerAuthorizationPresent: Boolean(headerToken), + headerAuthorizationLength: headerToken.length, + eventAuthorizationTokenPresent: Boolean(eventToken), + eventAuthorizationTokenLength: eventToken.length + } +} + +/** + * Summarizes the selected request token without exposing its raw value. + * + * @param {string} token Request authorization token. + * @returns {{tokenPresent: boolean, tokenType: string, tokenLength: number}} + * Redacted token summary. + */ +const summarizeTokenForLogs = (token) => { + const normalizedToken = token || '' + const bearerMatch = normalizedToken.match(/^\s*bearer\s+(.*)$/i) + const tokenValue = bearerMatch ? bearerMatch[1].trim() : normalizedToken.trim() + let tokenType = 'missing' + + if (bearerMatch) { + tokenType = 'bearer' + } else if (tokenValue) { + tokenType = 'launchpad' + } + + return { + tokenPresent: tokenValue.length > 0, + tokenType, + tokenLength: tokenValue.length + } +} + +/** + * Summarizes the normalized EDL profile fields used by the authorizer. + * + * @param {Object} profile Normalized EDL profile. + * @returns {Object} Redacted profile summary. + */ +const summarizeProfileForLogs = (profile = {}) => ({ + keys: Object.keys(profile).sort(), + uid: profile.uid, + auid: profile.auid, + assuranceLevel: profile.assuranceLevel, + namePresent: Boolean(profile.name) +}) + export const edlAuthorizer = async (event) => { - logger.debug('EDL Authorizer called with event:', JSON.stringify(event, null, 2)) + logger.info('[edl-authorizer] Authorization request received', summarizeEventForLogs(event)) const { headers = {}, methodArn, @@ -24,17 +89,22 @@ export const edlAuthorizer = async (event) => { // If still not found, default to an empty string token = token || '' logger.debug('Launchpad token:', token ? 'Present' : 'Not present') + logger.info('[edl-authorizer] Resolved request token', summarizeTokenForLogs(token)) try { const profile = await fetchEdlProfile(token) logger.debug('Fetched EDL profile:', JSON.stringify(profile, null, 2)) + logger.info('[edl-authorizer] Retrieved normalized EDL profile', summarizeProfileForLogs(profile)) const { uid, assuranceLevel } = profile || {} if (!uid) { - logger.error('Authorization failed: No uid found in profile') + logger.error('Authorization failed: No uid found in profile', { + ...summarizeTokenForLogs(token), + profile: summarizeProfileForLogs(profile) + }) return generatePolicy('user', 'Deny', methodArn) } @@ -42,25 +112,46 @@ export const edlAuthorizer = async (event) => { const parsedAssuranceLevel = Number(assuranceLevel) if (Number.isNaN(parsedAssuranceLevel)) { - logger.error('Authorization failed: Assurance level missing from profile') + logger.error('Authorization failed: Assurance level missing from profile', { + ...summarizeTokenForLogs(token), + profile: summarizeProfileForLogs(profile), + rawAssuranceLevel: assuranceLevel, + rawAssuranceLevelType: typeof assuranceLevel + }) return generatePolicy('user', 'Deny', methodArn) } if (parsedAssuranceLevel < REQUIRED_ASSURANCE_LEVEL) { - logger.error(`Authorization failed: Assurance level ${parsedAssuranceLevel} below required ${REQUIRED_ASSURANCE_LEVEL}`) + logger.error(`Authorization failed: Assurance level ${parsedAssuranceLevel} below required ${REQUIRED_ASSURANCE_LEVEL}`, { + ...summarizeTokenForLogs(token), + profile: summarizeProfileForLogs(profile), + parsedAssuranceLevel, + requiredAssuranceLevel: REQUIRED_ASSURANCE_LEVEL + }) return generatePolicy('user', 'Deny', methodArn) } logger.debug('Authorization successful for uid:', uid) + logger.info('[edl-authorizer] Authorization successful', { + uid, + parsedAssuranceLevel, + requiredAssuranceLevel: REQUIRED_ASSURANCE_LEVEL, + tokenType: summarizeTokenForLogs(token).tokenType + }) const policy = generatePolicy(uid, 'Allow', methodArn) logger.debug('Returning policy:', JSON.stringify(policy, null, 2)) return policy } catch (error) { - logger.error('EDL Authorizer error:', error) + logger.error('EDL Authorizer error:', error, { + ...summarizeEventForLogs(event), + ...summarizeTokenForLogs(token), + errorMessage: error?.message, + errorName: error?.name + }) // Return a "Deny" policy for any caught errors const denyPolicy = generatePolicy('user', 'Deny', methodArn) diff --git a/serverless/src/shared/fetchEdlProfile.js b/serverless/src/shared/fetchEdlProfile.js index 0fe9aa02..e372611e 100644 --- a/serverless/src/shared/fetchEdlProfile.js +++ b/serverless/src/shared/fetchEdlProfile.js @@ -3,6 +3,61 @@ import { logger } from '@/shared/logger' import fetchEdlClientToken from './fetchEdlClientToken' +/** + * Builds a token summary safe for logs without exposing the raw token value. + * + * @param {string} token Authorization token value from the request. + * @returns {{tokenPresent: boolean, tokenType: string, tokenLength: number}} + * Redacted token summary. + */ +const summarizeTokenForLogs = (token) => { + const normalizedToken = token || '' + const bearerMatch = normalizedToken.match(/^\s*bearer\s+(.*)$/i) + const tokenValue = bearerMatch ? bearerMatch[1].trim() : normalizedToken.trim() + let tokenType = 'missing' + + if (bearerMatch) { + tokenType = 'bearer' + } else if (tokenValue) { + tokenType = 'launchpad' + } + + return { + tokenPresent: tokenValue.length > 0, + tokenType, + tokenLength: tokenValue.length + } +} + +/** + * Extracts only the raw profile fields useful for authorization debugging. + * + * @param {Object} profile Raw profile payload from EDL. + * @returns {Object} Redacted profile summary for logs. + */ +const summarizeRawProfileForLogs = (profile = {}) => ({ + keys: Object.keys(profile).sort(), + uid: profile.uid, + auid: profile.nams_auid, + assuranceLevel: profile.assurance_level ?? profile.assuranceLevel, + firstNamePresent: Boolean(profile.first_name), + lastNamePresent: Boolean(profile.last_name) +}) + +/** + * Extracts normalized profile fields relevant to authorizer decisions. + * + * @param {Object} profile Normalized profile payload. + * @returns {Object} Redacted normalized profile summary for logs. + */ +const summarizeNormalizedProfileForLogs = (profile = {}) => ({ + keys: Object.keys(profile).sort(), + uid: profile.uid, + auid: profile.auid, + assuranceLevel: profile.assuranceLevel, + namePresent: Boolean(profile.name) +}) + /** * Builds an EDL profile into the format expected by consumers * @param {Object} profile Raw EDL profile response @@ -52,6 +107,10 @@ const buildEdlError = (response, source) => { const fetchProfileWithLaunchpadToken = async (host, launchpadToken) => { const clientToken = await fetchEdlClientToken() logger.debug('Fetched client token:', clientToken ? 'Present' : 'Not present') + logger.info('[edl-profile] Fetching Launchpad profile via EDL gateway', { + host, + ...summarizeTokenForLogs(launchpadToken) + }) const response = await fetch(`${host}/api/nams/edl_user`, { body: `token=${launchpadToken}`, @@ -71,8 +130,10 @@ const fetchProfileWithLaunchpadToken = async (host, launchpadToken) => { const profile = await response.json() logger.debug('Received EDL profile:', JSON.stringify(profile, null, 2)) + logger.info('[edl-profile] Received raw Launchpad profile summary', summarizeRawProfileForLogs(profile)) const normalizedProfile = buildProfile(profile) + logger.info('[edl-profile] Normalized Launchpad profile summary', summarizeNormalizedProfileForLogs(normalizedProfile)) return { ...normalizedProfile, @@ -87,6 +148,11 @@ const fetchProfileWithLaunchpadToken = async (host, launchpadToken) => { * @returns {Promise} normalized profile from the oauth endpoint */ const fetchProfileWithEdlAccessToken = async (host, edlToken) => { + logger.info('[edl-profile] Fetching direct EDL bearer profile', { + host, + ...summarizeTokenForLogs(`Bearer ${edlToken}`) + }) + const response = await fetch(`${host}/oauth/userInfo`, { method: 'GET', headers: { @@ -104,8 +170,12 @@ const fetchProfileWithEdlAccessToken = async (host, edlToken) => { const profile = await response.json() logger.debug('Received EDL oauth profile:', JSON.stringify(profile, null, 2)) + logger.info('[edl-profile] Received raw bearer profile summary', summarizeRawProfileForLogs(profile)) - return buildProfile(profile) + const normalizedProfile = buildProfile(profile) + logger.info('[edl-profile] Normalized bearer profile summary', summarizeNormalizedProfileForLogs(normalizedProfile)) + + return normalizedProfile } /** @@ -130,6 +200,11 @@ const fetchEdlProfile = async (token) => { const { host } = getEdlConfig() logger.debug('EDL host:', host) + logger.info('[edl-profile] Starting profile lookup', { + host, + isOffline: Boolean(IS_OFFLINE), + ...summarizeTokenForLogs(token) + }) try { const normalizedToken = token || '' @@ -143,12 +218,18 @@ const fetchEdlProfile = async (token) => { } logger.debug('Using EDL access token for profile lookup') + logger.info('[edl-profile] Resolved token path', { + tokenType: 'bearer' + }) return fetchProfileWithEdlAccessToken(host, edlToken) } const trimmedToken = normalizedToken.trim() logger.debug('Using Launchpad token for profile lookup') + logger.info('[edl-profile] Resolved token path', { + tokenType: 'launchpad' + }) return fetchProfileWithLaunchpadToken(host, trimmedToken) } catch (error) { From 69432f0d250af13d4e1d40fbf96ae028fdf195bf Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Tue, 7 Jul 2026 13:25:55 -0400 Subject: [PATCH 2/7] KMS-690: Updated to use /oauth/user for token validation. --- .../shared/__tests__/fetchEdlProfile.test.js | 120 ++++++++++--- serverless/src/shared/fetchEdlProfile.js | 159 ++++++++---------- 2 files changed, 164 insertions(+), 115 deletions(-) diff --git a/serverless/src/shared/__tests__/fetchEdlProfile.test.js b/serverless/src/shared/__tests__/fetchEdlProfile.test.js index 5ec11cdf..96747f87 100644 --- a/serverless/src/shared/__tests__/fetchEdlProfile.test.js +++ b/serverless/src/shared/__tests__/fetchEdlProfile.test.js @@ -1,3 +1,4 @@ +import { getEdlConfig } from '@/shared/getConfig' import { logger } from '@/shared/logger' import fetchEdlClientToken from '../fetchEdlClientToken' @@ -5,12 +6,29 @@ import fetchEdlProfile from '../fetchEdlProfile' vi.mock('../fetchEdlClientToken', () => ({ default: vi.fn() })) +const encodeBase64Url = (value) => Buffer + .from(JSON.stringify(value)) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, '') + +const createJwt = (payload) => [ + encodeBase64Url({ + alg: 'RS256', + typ: 'JWT' + }), + encodeBase64Url(payload), + 'signature' +].join('.') + const originalConsoleLog = console.log let loggerErrorSpy beforeEach(() => { vi.resetAllMocks() console.log = vi.fn() + process.env.EDL_PASSWORD = 'kms-client-password' fetchEdlClientToken.mockImplementation(() => ('mock_token')) loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}) }) @@ -20,47 +38,57 @@ afterEach(() => { console.log = originalConsoleLog loggerErrorSpy?.mockRestore() loggerErrorSpy = undefined + delete process.env.EDL_PASSWORD + delete process.env.IS_OFFLINE }) describe('fetchEdlProfile', () => { describe('when provided a Bearer access token', () => { describe('when the token is valid', () => { - test('calls the oauth endpoint and returns the profile', async () => { + test('validates the token and returns the normalized profile', async () => { + const { uid: edlUid } = getEdlConfig() + const rawToken = createJwt({ + uid: 'user.name', + assurance_level: 3 + }) + global.fetch = vi.fn(() => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({ - nams_auid: 'user.name', - uid: 'user.name', - first_name: 'User', - last_name: 'Name', - assurance_level: 3 + uid: 'user.name' }) })) - const profile = await fetchEdlProfile('Bearer bearer-token-value') + const profile = await fetchEdlProfile(`Bearer ${rawToken}`) expect(profile).toEqual({ - auid: 'user.name', assuranceLevel: 3, - name: 'User Name', + name: 'user.name', uid: 'user.name' }) expect(fetchEdlClientToken).not.toHaveBeenCalled() expect(fetch).toHaveBeenCalledTimes(1) - expect(fetch).toHaveBeenCalledWith('https://sit.urs.earthdata.nasa.gov/oauth/userInfo', { - headers: { - Authorization: 'Bearer bearer-token-value', - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' - }, - method: 'GET' - }) + expect(fetch).toHaveBeenCalledWith( + `https://sit.urs.earthdata.nasa.gov/oauth/tokens/user?client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(rawToken)}`, + { + headers: { + Accept: 'application/json', + Authorization: `Basic ${Buffer.from(`${edlUid}:kms-client-password`).toString('base64')}` + }, + method: 'GET' + } + ) }) }) - describe('when the oauth endpoint returns an unauthorized error', () => { + describe('when the token validation endpoint returns an unauthorized error', () => { test('throws unauthorized', async () => { + const rawToken = createJwt({ + uid: 'user.name', + assurance_level: 3 + }) const mockResponse = { ok: false, status: 401, @@ -68,15 +96,19 @@ describe('fetchEdlProfile', () => { } global.fetch = vi.fn(() => Promise.resolve(mockResponse)) - await expect(fetchEdlProfile('Bearer bearer-token-value')).rejects.toThrow('Unauthorized') + await expect(fetchEdlProfile(`Bearer ${rawToken}`)).rejects.toThrow('Unauthorized') expect(fetchEdlClientToken).not.toHaveBeenCalled() expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('EDL oauth error response:', mockResponse) + expect(logger.error).toHaveBeenCalledWith('EDL token validation error response:', mockResponse) }) }) - describe('when the oauth endpoint returns a non-auth error', () => { + describe('when the token validation endpoint returns a non-auth error', () => { test('throws an error describing the failure', async () => { + const rawToken = createJwt({ + uid: 'user.name', + assurance_level: 3 + }) const mockResponse = { ok: false, status: 500, @@ -84,9 +116,9 @@ describe('fetchEdlProfile', () => { } global.fetch = vi.fn(() => Promise.resolve(mockResponse)) - await expect(fetchEdlProfile('Bearer bearer-token-value')).rejects.toThrow('EDL oauth request failed with status 500') + await expect(fetchEdlProfile(`Bearer ${rawToken}`)).rejects.toThrow('EDL token validation request failed with status 500') expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('EDL oauth error response:', mockResponse) + expect(logger.error).toHaveBeenCalledWith('EDL token validation error response:', mockResponse) }) }) @@ -101,6 +133,49 @@ describe('fetchEdlProfile', () => { expect(logger.error).toHaveBeenCalledWith('#fetchEdlProfile fetchEdlProfile Error:', expect.any(Error)) }) }) + + describe('when bearer validation configuration is missing', () => { + test('throws when EDL_PASSWORD is missing', async () => { + delete process.env.EDL_PASSWORD + global.fetch = vi.fn() + + await expect(fetchEdlProfile(`Bearer ${createJwt({ + uid: 'user.name', + assurance_level: 3 + })}`)).rejects.toThrow('Missing EDL_PASSWORD configuration') + + expect(fetch).not.toHaveBeenCalled() + }) + }) + + describe('when the token validation payload is malformed', () => { + test('throws an error when uid is missing', async () => { + const rawToken = createJwt({ + uid: 'user.name', + assurance_level: 3 + }) + + global.fetch = vi.fn(() => Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({}) + })) + + await expect(fetchEdlProfile(`Bearer ${rawToken}`)).rejects.toThrow('EDL token validation response missing uid') + }) + + test('throws an error when the jwt payload cannot be decoded', async () => { + global.fetch = vi.fn(() => Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ + uid: 'user.name' + }) + })) + + await expect(fetchEdlProfile('Bearer not-a-jwt')).rejects.toThrow('Invalid EDL token format') + }) + }) }) describe('when provided a Launchpad access token', () => { @@ -216,7 +291,6 @@ describe('fetchEdlProfile', () => { }) expect(fetch).toHaveBeenCalledTimes(0) - process.env.IS_OFFLINE = false }) }) }) diff --git a/serverless/src/shared/fetchEdlProfile.js b/serverless/src/shared/fetchEdlProfile.js index e372611e..5b28d8e1 100644 --- a/serverless/src/shared/fetchEdlProfile.js +++ b/serverless/src/shared/fetchEdlProfile.js @@ -3,61 +3,6 @@ import { logger } from '@/shared/logger' import fetchEdlClientToken from './fetchEdlClientToken' -/** - * Builds a token summary safe for logs without exposing the raw token value. - * - * @param {string} token Authorization token value from the request. - * @returns {{tokenPresent: boolean, tokenType: string, tokenLength: number}} - * Redacted token summary. - */ -const summarizeTokenForLogs = (token) => { - const normalizedToken = token || '' - const bearerMatch = normalizedToken.match(/^\s*bearer\s+(.*)$/i) - const tokenValue = bearerMatch ? bearerMatch[1].trim() : normalizedToken.trim() - let tokenType = 'missing' - - if (bearerMatch) { - tokenType = 'bearer' - } else if (tokenValue) { - tokenType = 'launchpad' - } - - return { - tokenPresent: tokenValue.length > 0, - tokenType, - tokenLength: tokenValue.length - } -} - -/** - * Extracts only the raw profile fields useful for authorization debugging. - * - * @param {Object} profile Raw profile payload from EDL. - * @returns {Object} Redacted profile summary for logs. - */ -const summarizeRawProfileForLogs = (profile = {}) => ({ - keys: Object.keys(profile).sort(), - uid: profile.uid, - auid: profile.nams_auid, - assuranceLevel: profile.assurance_level ?? profile.assuranceLevel, - firstNamePresent: Boolean(profile.first_name), - lastNamePresent: Boolean(profile.last_name) -}) - -/** - * Extracts normalized profile fields relevant to authorizer decisions. - * - * @param {Object} profile Normalized profile payload. - * @returns {Object} Redacted normalized profile summary for logs. - */ -const summarizeNormalizedProfileForLogs = (profile = {}) => ({ - keys: Object.keys(profile).sort(), - uid: profile.uid, - auid: profile.auid, - assuranceLevel: profile.assuranceLevel, - namePresent: Boolean(profile.name) -}) - /** * Builds an EDL profile into the format expected by consumers * @param {Object} profile Raw EDL profile response @@ -98,6 +43,36 @@ const buildEdlError = (response, source) => { return new Error(`${source} failed with status ${status}`) } +/** + * Builds the Basic authorization header value for app-authenticated EDL calls. + * @param {string} clientId Registered EDL application client id + * @param {string} password Registered EDL application password + * @returns {string} HTTP Basic authorization header value + */ +const buildBasicAuthorizationHeader = (clientId, password) => ( + `Basic ${Buffer.from(`${clientId}:${password}`).toString('base64')}` +) + +/** + * Decodes JWT claims from the token payload. + * @param {string} edlToken Direct EDL access token + * @returns {Object} Parsed JWT claims + */ +const decodeJwtClaims = (edlToken) => { + const payload = edlToken.split('.')[1] + + if (!payload) { + throw new Error('Invalid EDL token format') + } + + const normalizedPayload = payload + .replace(/-/g, '+') + .replace(/_/g, '/') + const paddedPayload = normalizedPayload.padEnd(Math.ceil(normalizedPayload.length / 4) * 4, '=') + + return JSON.parse(Buffer.from(paddedPayload, 'base64').toString('utf8')) +} + /** * Fetches the user profile using a Launchpad token via the Launchpad gateway * @param {string} host EDL host base URL @@ -107,10 +82,6 @@ const buildEdlError = (response, source) => { const fetchProfileWithLaunchpadToken = async (host, launchpadToken) => { const clientToken = await fetchEdlClientToken() logger.debug('Fetched client token:', clientToken ? 'Present' : 'Not present') - logger.info('[edl-profile] Fetching Launchpad profile via EDL gateway', { - host, - ...summarizeTokenForLogs(launchpadToken) - }) const response = await fetch(`${host}/api/nams/edl_user`, { body: `token=${launchpadToken}`, @@ -130,10 +101,8 @@ const fetchProfileWithLaunchpadToken = async (host, launchpadToken) => { const profile = await response.json() logger.debug('Received EDL profile:', JSON.stringify(profile, null, 2)) - logger.info('[edl-profile] Received raw Launchpad profile summary', summarizeRawProfileForLogs(profile)) const normalizedProfile = buildProfile(profile) - logger.info('[edl-profile] Normalized Launchpad profile summary', summarizeNormalizedProfileForLogs(normalizedProfile)) return { ...normalizedProfile, @@ -142,40 +111,57 @@ const fetchProfileWithLaunchpadToken = async (host, launchpadToken) => { } /** - * Fetches the user profile directly from EDL using an access token + * Validates the bearer token with EDL and derives assurance level from the JWT. * @param {string} host EDL host base URL * @param {string} edlToken Direct EDL access token (Bearer token) - * @returns {Promise} normalized profile from the oauth endpoint + * @returns {Promise} normalized profile from validated bearer token data */ const fetchProfileWithEdlAccessToken = async (host, edlToken) => { - logger.info('[edl-profile] Fetching direct EDL bearer profile', { - host, - ...summarizeTokenForLogs(`Bearer ${edlToken}`) - }) + const { uid: edlUid } = getEdlConfig() + const { + EDL_PASSWORD: password + } = process.env - const response = await fetch(`${host}/oauth/userInfo`, { - method: 'GET', - headers: { - Authorization: `Bearer ${edlToken}`, - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + if (!edlUid) { + throw new Error('Missing EDL UID configuration') + } + + if (!password) { + throw new Error('Missing EDL_PASSWORD configuration') + } + + const response = await fetch( + `${host}/oauth/tokens/user?client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(edlToken)}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: buildBasicAuthorizationHeader(edlUid, password) + } } - }) + ) - logger.debug('EDL oauth response status:', response.status) + logger.debug('EDL token validation response status:', response.status) if (!response.ok) { - logger.error('EDL oauth error response:', response) - throw buildEdlError(response, 'EDL oauth request') + logger.error('EDL token validation error response:', response) + throw buildEdlError(response, 'EDL token validation request') } const profile = await response.json() - logger.debug('Received EDL oauth profile:', JSON.stringify(profile, null, 2)) - logger.info('[edl-profile] Received raw bearer profile summary', summarizeRawProfileForLogs(profile)) + logger.debug('Received EDL token validation payload:', JSON.stringify(profile, null, 2)) - const normalizedProfile = buildProfile(profile) - logger.info('[edl-profile] Normalized bearer profile summary', summarizeNormalizedProfileForLogs(normalizedProfile)) + if (!profile.uid) { + throw new Error('EDL token validation response missing uid') + } + + const claims = decodeJwtClaims(edlToken) - return normalizedProfile + return { + name: profile.uid, + uid: profile.uid, + assuranceLevel: claims.assurance_level + } } /** @@ -200,11 +186,6 @@ const fetchEdlProfile = async (token) => { const { host } = getEdlConfig() logger.debug('EDL host:', host) - logger.info('[edl-profile] Starting profile lookup', { - host, - isOffline: Boolean(IS_OFFLINE), - ...summarizeTokenForLogs(token) - }) try { const normalizedToken = token || '' @@ -218,18 +199,12 @@ const fetchEdlProfile = async (token) => { } logger.debug('Using EDL access token for profile lookup') - logger.info('[edl-profile] Resolved token path', { - tokenType: 'bearer' - }) return fetchProfileWithEdlAccessToken(host, edlToken) } const trimmedToken = normalizedToken.trim() logger.debug('Using Launchpad token for profile lookup') - logger.info('[edl-profile] Resolved token path', { - tokenType: 'launchpad' - }) return fetchProfileWithLaunchpadToken(host, trimmedToken) } catch (error) { From 5ba016405285744414f01f1963ecda55e74d6ff2 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Tue, 7 Jul 2026 13:47:37 -0400 Subject: [PATCH 3/7] KMS-690: Changed to POST --- .../shared/__tests__/fetchEdlProfile.test.js | 8 +++++--- serverless/src/shared/fetchEdlProfile.js | 19 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/serverless/src/shared/__tests__/fetchEdlProfile.test.js b/serverless/src/shared/__tests__/fetchEdlProfile.test.js index 96747f87..e96b337e 100644 --- a/serverless/src/shared/__tests__/fetchEdlProfile.test.js +++ b/serverless/src/shared/__tests__/fetchEdlProfile.test.js @@ -71,13 +71,15 @@ describe('fetchEdlProfile', () => { expect(fetchEdlClientToken).not.toHaveBeenCalled() expect(fetch).toHaveBeenCalledTimes(1) expect(fetch).toHaveBeenCalledWith( - `https://sit.urs.earthdata.nasa.gov/oauth/tokens/user?client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(rawToken)}`, + 'https://sit.urs.earthdata.nasa.gov/oauth/tokens/user', { + body: `client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(rawToken)}`, headers: { Accept: 'application/json', - Authorization: `Basic ${Buffer.from(`${edlUid}:kms-client-password`).toString('base64')}` + Authorization: `Basic ${Buffer.from(`${edlUid}:kms-client-password`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, - method: 'GET' + method: 'POST' } ) }) diff --git a/serverless/src/shared/fetchEdlProfile.js b/serverless/src/shared/fetchEdlProfile.js index 5b28d8e1..85fd280f 100644 --- a/serverless/src/shared/fetchEdlProfile.js +++ b/serverless/src/shared/fetchEdlProfile.js @@ -130,16 +130,15 @@ const fetchProfileWithEdlAccessToken = async (host, edlToken) => { throw new Error('Missing EDL_PASSWORD configuration') } - const response = await fetch( - `${host}/oauth/tokens/user?client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(edlToken)}`, - { - method: 'GET', - headers: { - Accept: 'application/json', - Authorization: buildBasicAuthorizationHeader(edlUid, password) - } - } - ) + const response = await fetch(`${host}/oauth/tokens/user`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: buildBasicAuthorizationHeader(edlUid, password), + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + body: `client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(edlToken)}` + }) logger.debug('EDL token validation response status:', response.status) From a43159a549e210a7de431255bb974df4e4398a6f Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Tue, 7 Jul 2026 15:21:54 -0400 Subject: [PATCH 4/7] KMS-690: Now uses EDL_CLIENT_ID --- README.md | 1 + bin/deploy-bamboo.sh | 1 + cdk/app/lib/KmsStack.ts | 1 + cdk/app/lib/helper/KmsLambdaFunctions.ts | 1 + cdk/bin/main.ts | 1 + .../shared/__tests__/fetchEdlProfile.test.js | 20 +++++++++++++++---- serverless/src/shared/fetchEdlProfile.js | 10 +++++----- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8ac26715..0d48a9ee 100644 --- a/README.md +++ b/README.md @@ -478,6 +478,7 @@ export bamboo_RDF4J_PASSWORD=[your rdfdb password] export bamboo_EDL_HOST=[edl host name] export bamboo_EDL_UID=[edl user id] export bamboo_EDL_PASSWORD=[edl password] +export bamboo_EDL_CLIENT_ID=[edl oauth client id] export bamboo_CMR_BASE_URL=[cmr base url] export bamboo_CMR_WRITER_TOKEN=[optional bearer token for CMR metadata writeback] export bamboo_CMR_WRITEBACK_PROVIDERS=[optional provider id, comma-separated list, or ALL] diff --git a/bin/deploy-bamboo.sh b/bin/deploy-bamboo.sh index 556ad322..d4454d5f 100755 --- a/bin/deploy-bamboo.sh +++ b/bin/deploy-bamboo.sh @@ -62,6 +62,7 @@ dockerRun() { --env "RDF4J_USER_NAME=$bamboo_RDF4J_USER_NAME" \ --env "RDF4J_PASSWORD=$bamboo_RDF4J_PASSWORD" \ --env "EDL_PASSWORD=$bamboo_EDL_PASSWORD" \ + --env "EDL_CLIENT_ID=$bamboo_EDL_CLIENT_ID" \ --env "CMR_BASE_URL=$bamboo_CMR_BASE_URL" \ --env "CMR_WRITER_TOKEN=${bamboo_CMR_WRITER_TOKEN:-}" \ --env "CMR_WRITEBACK_PROVIDERS=${bamboo_CMR_WRITEBACK_PROVIDERS:-}" \ diff --git a/cdk/app/lib/KmsStack.ts b/cdk/app/lib/KmsStack.ts index 60609043..fdc6e6d1 100644 --- a/cdk/app/lib/KmsStack.ts +++ b/cdk/app/lib/KmsStack.ts @@ -29,6 +29,7 @@ export interface KmsStackProps extends cdk.StackProps { logDestinationArn: string environment: { CMR_BASE_URL: string + EDL_CLIENT_ID: string EDL_PASSWORD: string BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE?: string LOG_LEVEL: string diff --git a/cdk/app/lib/helper/KmsLambdaFunctions.ts b/cdk/app/lib/helper/KmsLambdaFunctions.ts index 05477e64..ce2484f7 100644 --- a/cdk/app/lib/helper/KmsLambdaFunctions.ts +++ b/cdk/app/lib/helper/KmsLambdaFunctions.ts @@ -31,6 +31,7 @@ interface LambdaFunctionsProps { useLocalstack: boolean; environment: { CMR_BASE_URL: string; + EDL_CLIENT_ID: string; EDL_PASSWORD: string; BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE?: string; REDIS_ENABLED?: string; diff --git a/cdk/bin/main.ts b/cdk/bin/main.ts index c62ff5ea..da797a10 100644 --- a/cdk/bin/main.ts +++ b/cdk/bin/main.ts @@ -200,6 +200,7 @@ async function main() { RDF_BUCKET_NAME: process.env.RDF_BUCKET_NAME || 'kms-rdf-backup', CMR_BASE_URL: cmrBaseUrl, EDL_PASSWORD: process.env.EDL_PASSWORD || '', + EDL_CLIENT_ID: process.env.EDL_CLIENT_ID || '', BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE: process.env.BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE, LOG_LEVEL: process.env.LOG_LEVEL || 'INFO', REDIS_ENABLED: redisEnabledValue, diff --git a/serverless/src/shared/__tests__/fetchEdlProfile.test.js b/serverless/src/shared/__tests__/fetchEdlProfile.test.js index e96b337e..3adeb6f3 100644 --- a/serverless/src/shared/__tests__/fetchEdlProfile.test.js +++ b/serverless/src/shared/__tests__/fetchEdlProfile.test.js @@ -1,4 +1,3 @@ -import { getEdlConfig } from '@/shared/getConfig' import { logger } from '@/shared/logger' import fetchEdlClientToken from '../fetchEdlClientToken' @@ -28,6 +27,7 @@ let loggerErrorSpy beforeEach(() => { vi.resetAllMocks() console.log = vi.fn() + process.env.EDL_CLIENT_ID = 'kms-client-id' process.env.EDL_PASSWORD = 'kms-client-password' fetchEdlClientToken.mockImplementation(() => ('mock_token')) loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}) @@ -38,6 +38,7 @@ afterEach(() => { console.log = originalConsoleLog loggerErrorSpy?.mockRestore() loggerErrorSpy = undefined + delete process.env.EDL_CLIENT_ID delete process.env.EDL_PASSWORD delete process.env.IS_OFFLINE }) @@ -46,7 +47,6 @@ describe('fetchEdlProfile', () => { describe('when provided a Bearer access token', () => { describe('when the token is valid', () => { test('validates the token and returns the normalized profile', async () => { - const { uid: edlUid } = getEdlConfig() const rawToken = createJwt({ uid: 'user.name', assurance_level: 3 @@ -73,10 +73,10 @@ describe('fetchEdlProfile', () => { expect(fetch).toHaveBeenCalledWith( 'https://sit.urs.earthdata.nasa.gov/oauth/tokens/user', { - body: `client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(rawToken)}`, + body: `client_id=${encodeURIComponent('kms-client-id')}&token=${encodeURIComponent(rawToken)}`, headers: { Accept: 'application/json', - Authorization: `Basic ${Buffer.from(`${edlUid}:kms-client-password`).toString('base64')}`, + Authorization: `Basic ${Buffer.from('kms-client-id:kms-client-password').toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, method: 'POST' @@ -137,6 +137,18 @@ describe('fetchEdlProfile', () => { }) describe('when bearer validation configuration is missing', () => { + test('throws when EDL_CLIENT_ID is missing', async () => { + delete process.env.EDL_CLIENT_ID + global.fetch = vi.fn() + + await expect(fetchEdlProfile(`Bearer ${createJwt({ + uid: 'user.name', + assurance_level: 3 + })}`)).rejects.toThrow('Missing EDL_CLIENT_ID configuration') + + expect(fetch).not.toHaveBeenCalled() + }) + test('throws when EDL_PASSWORD is missing', async () => { delete process.env.EDL_PASSWORD global.fetch = vi.fn() diff --git a/serverless/src/shared/fetchEdlProfile.js b/serverless/src/shared/fetchEdlProfile.js index 85fd280f..7f56b48b 100644 --- a/serverless/src/shared/fetchEdlProfile.js +++ b/serverless/src/shared/fetchEdlProfile.js @@ -117,13 +117,13 @@ const fetchProfileWithLaunchpadToken = async (host, launchpadToken) => { * @returns {Promise} normalized profile from validated bearer token data */ const fetchProfileWithEdlAccessToken = async (host, edlToken) => { - const { uid: edlUid } = getEdlConfig() const { + EDL_CLIENT_ID: clientId, EDL_PASSWORD: password } = process.env - if (!edlUid) { - throw new Error('Missing EDL UID configuration') + if (!clientId) { + throw new Error('Missing EDL_CLIENT_ID configuration') } if (!password) { @@ -134,10 +134,10 @@ const fetchProfileWithEdlAccessToken = async (host, edlToken) => { method: 'POST', headers: { Accept: 'application/json', - Authorization: buildBasicAuthorizationHeader(edlUid, password), + Authorization: buildBasicAuthorizationHeader(clientId, password), 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, - body: `client_id=${encodeURIComponent(edlUid)}&token=${encodeURIComponent(edlToken)}` + body: `client_id=${encodeURIComponent(clientId)}&token=${encodeURIComponent(edlToken)}` }) logger.debug('EDL token validation response status:', response.status) From 757d889c8da6c1a683bf47e62d44b4be314cd8a9 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Tue, 7 Jul 2026 15:44:36 -0400 Subject: [PATCH 5/7] KMS-690: Reverted authorizer logging --- .../edlAuthorizer/__tests__/handler.test.js | 33 +----- serverless/src/edlAuthorizer/handler.js | 101 +----------------- 2 files changed, 9 insertions(+), 125 deletions(-) diff --git a/serverless/src/edlAuthorizer/__tests__/handler.test.js b/serverless/src/edlAuthorizer/__tests__/handler.test.js index ef06d552..8b7507d3 100644 --- a/serverless/src/edlAuthorizer/__tests__/handler.test.js +++ b/serverless/src/edlAuthorizer/__tests__/handler.test.js @@ -120,13 +120,7 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('Authorization failed: No uid found in profile', expect.objectContaining({ - profile: expect.objectContaining({ - uid: undefined - }), - tokenPresent: false, - tokenType: 'missing' - })) + expect(logger.error).toHaveBeenCalledWith('Authorization failed: No uid found in profile') }) }) @@ -156,12 +150,7 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('EDL Authorizer error:', unauthorizedError, expect.objectContaining({ - errorMessage: 'Unauthorized', - errorName: 'Error', - tokenPresent: false, - tokenType: 'missing' - })) + expect(logger.error).toHaveBeenCalledWith('EDL Authorizer error:', unauthorizedError) }) }) @@ -193,14 +182,7 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level 3 below required 5', expect.objectContaining({ - parsedAssuranceLevel: 3, - requiredAssuranceLevel: 5, - profile: expect.objectContaining({ - uid: 'mock_user', - assuranceLevel: 3 - }) - })) + expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level 3 below required 5') }) test('returns a deny policy when assurance level missing', async () => { @@ -229,14 +211,7 @@ describe('edlAuthorizer', () => { }) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level missing from profile', expect.objectContaining({ - rawAssuranceLevel: undefined, - rawAssuranceLevelType: 'undefined', - profile: expect.objectContaining({ - uid: 'mock_user', - assuranceLevel: undefined - }) - })) + expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level missing from profile') }) }) }) diff --git a/serverless/src/edlAuthorizer/handler.js b/serverless/src/edlAuthorizer/handler.js index 4ea2e815..04a94d52 100644 --- a/serverless/src/edlAuthorizer/handler.js +++ b/serverless/src/edlAuthorizer/handler.js @@ -5,73 +5,8 @@ import { logger } from '@/shared/logger' const REQUIRED_ASSURANCE_LEVEL = 5 -/** - * Builds a request summary safe for authorization logs. - * - * @param {Object} event API Gateway authorizer event. - * @returns {Object} Redacted request summary. - */ -const summarizeEventForLogs = (event = {}) => { - const { - headers = {}, - authorizationToken, - methodArn - } = event - const normalizedHeaders = downcaseKeys(headers) - const headerToken = normalizedHeaders.authorization || '' - const eventToken = authorizationToken || '' - - return { - methodArnPresent: Boolean(methodArn), - headerAuthorizationPresent: Boolean(headerToken), - headerAuthorizationLength: headerToken.length, - eventAuthorizationTokenPresent: Boolean(eventToken), - eventAuthorizationTokenLength: eventToken.length - } -} - -/** - * Summarizes the selected request token without exposing its raw value. - * - * @param {string} token Request authorization token. - * @returns {{tokenPresent: boolean, tokenType: string, tokenLength: number}} - * Redacted token summary. - */ -const summarizeTokenForLogs = (token) => { - const normalizedToken = token || '' - const bearerMatch = normalizedToken.match(/^\s*bearer\s+(.*)$/i) - const tokenValue = bearerMatch ? bearerMatch[1].trim() : normalizedToken.trim() - let tokenType = 'missing' - - if (bearerMatch) { - tokenType = 'bearer' - } else if (tokenValue) { - tokenType = 'launchpad' - } - - return { - tokenPresent: tokenValue.length > 0, - tokenType, - tokenLength: tokenValue.length - } -} - -/** - * Summarizes the normalized EDL profile fields used by the authorizer. - * - * @param {Object} profile Normalized EDL profile. - * @returns {Object} Redacted profile summary. - */ -const summarizeProfileForLogs = (profile = {}) => ({ - keys: Object.keys(profile).sort(), - uid: profile.uid, - auid: profile.auid, - assuranceLevel: profile.assuranceLevel, - namePresent: Boolean(profile.name) -}) - export const edlAuthorizer = async (event) => { - logger.info('[edl-authorizer] Authorization request received', summarizeEventForLogs(event)) + logger.debug('EDL Authorizer called with event:', JSON.stringify(event, null, 2)) const { headers = {}, methodArn, @@ -89,22 +24,17 @@ export const edlAuthorizer = async (event) => { // If still not found, default to an empty string token = token || '' logger.debug('Launchpad token:', token ? 'Present' : 'Not present') - logger.info('[edl-authorizer] Resolved request token', summarizeTokenForLogs(token)) try { const profile = await fetchEdlProfile(token) logger.debug('Fetched EDL profile:', JSON.stringify(profile, null, 2)) - logger.info('[edl-authorizer] Retrieved normalized EDL profile', summarizeProfileForLogs(profile)) const { uid, assuranceLevel } = profile || {} if (!uid) { - logger.error('Authorization failed: No uid found in profile', { - ...summarizeTokenForLogs(token), - profile: summarizeProfileForLogs(profile) - }) + logger.error('Authorization failed: No uid found in profile') return generatePolicy('user', 'Deny', methodArn) } @@ -112,46 +42,25 @@ export const edlAuthorizer = async (event) => { const parsedAssuranceLevel = Number(assuranceLevel) if (Number.isNaN(parsedAssuranceLevel)) { - logger.error('Authorization failed: Assurance level missing from profile', { - ...summarizeTokenForLogs(token), - profile: summarizeProfileForLogs(profile), - rawAssuranceLevel: assuranceLevel, - rawAssuranceLevelType: typeof assuranceLevel - }) + logger.error('Authorization failed: Assurance level missing from profile') return generatePolicy('user', 'Deny', methodArn) } if (parsedAssuranceLevel < REQUIRED_ASSURANCE_LEVEL) { - logger.error(`Authorization failed: Assurance level ${parsedAssuranceLevel} below required ${REQUIRED_ASSURANCE_LEVEL}`, { - ...summarizeTokenForLogs(token), - profile: summarizeProfileForLogs(profile), - parsedAssuranceLevel, - requiredAssuranceLevel: REQUIRED_ASSURANCE_LEVEL - }) + logger.error(`Authorization failed: Assurance level ${parsedAssuranceLevel} below required ${REQUIRED_ASSURANCE_LEVEL}`) return generatePolicy('user', 'Deny', methodArn) } logger.debug('Authorization successful for uid:', uid) - logger.info('[edl-authorizer] Authorization successful', { - uid, - parsedAssuranceLevel, - requiredAssuranceLevel: REQUIRED_ASSURANCE_LEVEL, - tokenType: summarizeTokenForLogs(token).tokenType - }) const policy = generatePolicy(uid, 'Allow', methodArn) logger.debug('Returning policy:', JSON.stringify(policy, null, 2)) return policy } catch (error) { - logger.error('EDL Authorizer error:', error, { - ...summarizeEventForLogs(event), - ...summarizeTokenForLogs(token), - errorMessage: error?.message, - errorName: error?.name - }) + logger.error('EDL Authorizer error:', error) // Return a "Deny" policy for any caught errors const denyPolicy = generatePolicy('user', 'Deny', methodArn) From 456fa2b4adffa848c1ebe4f6b74f4c35a5a655fe Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Wed, 8 Jul 2026 14:10:48 -0400 Subject: [PATCH 6/7] KMS-690: Updates based on PR feedback. --- .../src/shared/__tests__/fetchEdlProfile.test.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/serverless/src/shared/__tests__/fetchEdlProfile.test.js b/serverless/src/shared/__tests__/fetchEdlProfile.test.js index 3adeb6f3..ef6535fd 100644 --- a/serverless/src/shared/__tests__/fetchEdlProfile.test.js +++ b/serverless/src/shared/__tests__/fetchEdlProfile.test.js @@ -1,10 +1,13 @@ import { logger } from '@/shared/logger' +import * as getConfig from '@/shared/getConfig' import fetchEdlClientToken from '../fetchEdlClientToken' import fetchEdlProfile from '../fetchEdlProfile' vi.mock('../fetchEdlClientToken', () => ({ default: vi.fn() })) +const TEST_EDL_HOST = 'https://edl.example.test' + const encodeBase64Url = (value) => Buffer .from(JSON.stringify(value)) .toString('base64') @@ -22,6 +25,7 @@ const createJwt = (payload) => [ ].join('.') const originalConsoleLog = console.log +let getEdlConfigSpy let loggerErrorSpy beforeEach(() => { @@ -30,12 +34,17 @@ beforeEach(() => { process.env.EDL_CLIENT_ID = 'kms-client-id' process.env.EDL_PASSWORD = 'kms-client-password' fetchEdlClientToken.mockImplementation(() => ('mock_token')) + getEdlConfigSpy = vi.spyOn(getConfig, 'getEdlConfig').mockImplementation(() => ({ + host: TEST_EDL_HOST + })) loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}) }) afterEach(() => { vi.clearAllMocks() console.log = originalConsoleLog + getEdlConfigSpy?.mockRestore() + getEdlConfigSpy = undefined loggerErrorSpy?.mockRestore() loggerErrorSpy = undefined delete process.env.EDL_CLIENT_ID @@ -71,7 +80,7 @@ describe('fetchEdlProfile', () => { expect(fetchEdlClientToken).not.toHaveBeenCalled() expect(fetch).toHaveBeenCalledTimes(1) expect(fetch).toHaveBeenCalledWith( - 'https://sit.urs.earthdata.nasa.gov/oauth/tokens/user', + `${TEST_EDL_HOST}/oauth/tokens/user`, { body: `client_id=${encodeURIComponent('kms-client-id')}&token=${encodeURIComponent(rawToken)}`, headers: { @@ -216,7 +225,7 @@ describe('fetchEdlProfile', () => { }) expect(fetch).toHaveBeenCalledTimes(1) - expect(fetch).toHaveBeenCalledWith('https://sit.urs.earthdata.nasa.gov/api/nams/edl_user', { + expect(fetch).toHaveBeenCalledWith(`${TEST_EDL_HOST}/api/nams/edl_user`, { body: 'token=mock-token', headers: { Authorization: 'Bearer mock_token', @@ -250,7 +259,7 @@ describe('fetchEdlProfile', () => { }) expect(fetch).toHaveBeenCalledTimes(1) - expect(fetch).toHaveBeenCalledWith('https://sit.urs.earthdata.nasa.gov/api/nams/edl_user', { + expect(fetch).toHaveBeenCalledWith(`${TEST_EDL_HOST}/api/nams/edl_user`, { body: 'token=mock-token', headers: { Authorization: 'Bearer mock_token', From c6ca69791f2ae6148b372e04f539fe89054e38a1 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Wed, 8 Jul 2026 14:51:07 -0400 Subject: [PATCH 7/7] KMS-690: Fixed lint issues --- serverless/src/shared/__tests__/fetchEdlProfile.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/serverless/src/shared/__tests__/fetchEdlProfile.test.js b/serverless/src/shared/__tests__/fetchEdlProfile.test.js index ef6535fd..a18dc498 100644 --- a/serverless/src/shared/__tests__/fetchEdlProfile.test.js +++ b/serverless/src/shared/__tests__/fetchEdlProfile.test.js @@ -1,5 +1,5 @@ -import { logger } from '@/shared/logger' import * as getConfig from '@/shared/getConfig' +import { logger } from '@/shared/logger' import fetchEdlClientToken from '../fetchEdlClientToken' import fetchEdlProfile from '../fetchEdlProfile' @@ -37,14 +37,17 @@ beforeEach(() => { getEdlConfigSpy = vi.spyOn(getConfig, 'getEdlConfig').mockImplementation(() => ({ host: TEST_EDL_HOST })) + loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}) }) afterEach(() => { vi.clearAllMocks() console.log = originalConsoleLog + getEdlConfigSpy?.mockRestore() getEdlConfigSpy = undefined + loggerErrorSpy?.mockRestore() loggerErrorSpy = undefined delete process.env.EDL_CLIENT_ID