From e892ad01d51380ce041fb7c62115e9e2e46c1a19 Mon Sep 17 00:00:00 2001 From: Brian Posey <15091170+btposey@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:14:51 -0400 Subject: [PATCH 1/4] Refactor UserSessionGateway to UserSessionUseCase * Moved UserSessionGateway to use case directory * Added ad_groups to Okta JWT claims type * Merged ad_groups and groups claims into a single groups claim Jira ticket: CAMS-446 Co-authored-by: James Brooks <12275865+jamesobrooks@users.noreply.github.com> Co-authored-by: Brian Posey <15091170+btposey@users.noreply.github.com>, --- .../azure/application-context-creator.test.ts | 4 +-- .../azure/application-context-creator.ts | 7 +++--- .../adapters/gateways/okta/okta-gateway.ts | 25 +++++++++++++++---- .../lib/adapters/types/authorization.ts | 3 +-- .../lib/adapters/utils/session-gateway.ts | 6 ----- backend/functions/lib/factory.ts | 11 ++++---- .../mock-gateways/mock-oauth2-gateway.ts | 3 +-- ...teway.ts => mock-user-session-use-case.ts} | 5 ++-- .../user-session/user-session.test.ts} | 14 ++++++----- .../user-session/user-session.ts} | 23 ++++++----------- 10 files changed, 50 insertions(+), 51 deletions(-) delete mode 100644 backend/functions/lib/adapters/utils/session-gateway.ts rename backend/functions/lib/testing/mock-gateways/{mock-user-session-gateway.ts => mock-user-session-use-case.ts} (88%) rename backend/functions/lib/{adapters/gateways/user-session-gateway.test.ts => use-cases/user-session/user-session.test.ts} (94%) rename backend/functions/lib/{adapters/gateways/user-session-gateway.ts => use-cases/user-session/user-session.ts} (84%) diff --git a/backend/functions/azure/application-context-creator.test.ts b/backend/functions/azure/application-context-creator.test.ts index 40b073e0b..15e76a094 100644 --- a/backend/functions/azure/application-context-creator.test.ts +++ b/backend/functions/azure/application-context-creator.test.ts @@ -2,7 +2,7 @@ import MockData from '../../../common/src/cams/test-utilities/mock-data'; import { ApplicationContext } from '../lib/adapters/types/basic'; import * as FeatureFlags from '../lib/adapters/utils/feature-flag'; import { ApplicationConfiguration } from '../lib/configs/application-configuration'; -import { MockUserSessionGateway } from '../lib/testing/mock-gateways/mock-user-session-gateway'; +import { MockUserSessionUseCase } from '../lib/testing/mock-gateways/mock-user-session-use-case'; import { createMockApplicationContext } from '../lib/testing/testing-utilities'; import ContextCreator from './application-context-creator'; import { createMockAzureFunctionContext, createMockAzureFunctionRequest } from './testing-helpers'; @@ -78,7 +78,7 @@ describe('Application Context Creator', () => { const mockContext = await createMockApplicationContext(); mockContext.request = request; const lookupSpy = jest - .spyOn(MockUserSessionGateway.prototype, 'lookup') + .spyOn(MockUserSessionUseCase.prototype, 'lookup') .mockResolvedValue(MockData.getCamsSession()); await ContextCreator.getApplicationContextSession(mockContext); expect(lookupSpy).toHaveBeenCalled(); diff --git a/backend/functions/azure/application-context-creator.ts b/backend/functions/azure/application-context-creator.ts index adedcfbf5..abe589cee 100644 --- a/backend/functions/azure/application-context-creator.ts +++ b/backend/functions/azure/application-context-creator.ts @@ -6,8 +6,7 @@ import { getFeatureFlags } from '../lib/adapters/utils/feature-flag'; import { LoggerImpl } from '../lib/adapters/services/logger.service'; import { azureToCamsHttpRequest } from './functions'; import { UnauthorizedError } from '../lib/common-errors/unauthorized-error'; -import { getUserSessionGateway } from '../lib/factory'; -import { SessionGateway } from '../lib/adapters/utils/session-gateway'; +import { getUserSessionUseCase } from '../lib/factory'; const MODULE_NAME = 'APPLICATION-CONTEXT-CREATOR'; @@ -77,8 +76,8 @@ async function getApplicationContextSession(context: ApplicationContext) { }); } - const sessionGateway: SessionGateway = getUserSessionGateway(context); - const session = await sessionGateway.lookup( + const sessionUseCase = getUserSessionUseCase(context); + const session = await sessionUseCase.lookup( context, accessToken, context.config.authConfig.provider, diff --git a/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts b/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts index 9abf8ff56..9a43f8984 100644 --- a/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts +++ b/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts @@ -43,10 +43,19 @@ async function verifyToken(token: string): Promise { } } -async function getUser(accessToken: string): Promise { +async function getUser( + accessToken: string, +): Promise<{ user: CamsUser; groups: string[]; jwt: CamsJwt }> { const { userInfoUri } = getAuthorizationConfig(); try { + const jwt = await verifyToken(accessToken); + if (!jwt) { + throw new UnauthorizedError(MODULE_NAME, { + message: 'Unable to verify token.', + }); + } + const response = await fetch(userInfoUri, { method: 'GET', headers: { authorization: 'Bearer ' + accessToken }, @@ -54,13 +63,20 @@ async function getUser(accessToken: string): Promise { if (response.ok) { const oktaUser = (await response.json()) as OktaUserInfo; - // TODO: We need to decide on the claim we will map to CamsUser.id - const camsUser: CamsUser = { + const user: CamsUser = { id: oktaUser.sub, name: oktaUser.name, }; - return camsUser; + type DojLoginUnifiedGroupClaims = { + ad_groups?: string[]; + groups?: string[]; + }; + + const claims = jwt.claims as unknown as DojLoginUnifiedGroupClaims; + const groups: string[] = [].concat(claims.ad_groups, claims.groups); + + return { user, groups, jwt }; } else { throw new Error('Failed to retrieve user info from Okta.'); } @@ -70,7 +86,6 @@ async function getUser(accessToken: string): Promise { } const OktaGateway: OpenIdConnectGateway = { - verifyToken, getUser, }; diff --git a/backend/functions/lib/adapters/types/authorization.ts b/backend/functions/lib/adapters/types/authorization.ts index 263e8c3b7..644558961 100644 --- a/backend/functions/lib/adapters/types/authorization.ts +++ b/backend/functions/lib/adapters/types/authorization.ts @@ -9,8 +9,7 @@ export type AuthorizationConfig = { }; export interface OpenIdConnectGateway { - verifyToken: (accessToken: string) => Promise; - getUser: (accessToken: string) => Promise; + getUser: (accessToken: string) => Promise<{ user: CamsUser; groups: string[]; jwt: CamsJwt }>; } export interface UserGroupGateway { diff --git a/backend/functions/lib/adapters/utils/session-gateway.ts b/backend/functions/lib/adapters/utils/session-gateway.ts deleted file mode 100644 index 84d6a7365..000000000 --- a/backend/functions/lib/adapters/utils/session-gateway.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CamsSession } from '../../../../../common/src/cams/session'; -import { ApplicationContext } from '../types/basic'; - -export interface SessionGateway { - lookup: (context: ApplicationContext, token: string, provider: string) => Promise; -} diff --git a/backend/functions/lib/factory.ts b/backend/functions/lib/factory.ts index db147da6a..1d0594c5d 100644 --- a/backend/functions/lib/factory.ts +++ b/backend/functions/lib/factory.ts @@ -32,9 +32,7 @@ import { OpenIdConnectGateway, UserGroupGateway } from './adapters/types/authori import OktaGateway from './adapters/gateways/okta/okta-gateway'; import { UserSessionCacheRepository } from './adapters/gateways/user-session-cache.repository'; import { UserSessionCacheCosmosDbRepository } from './adapters/gateways/user-session-cache.cosmosdb.repository'; -import { SessionGateway } from './adapters/utils/session-gateway'; -import { UserSessionGateway } from './adapters/gateways/user-session-gateway'; -import { MockUserSessionGateway } from './testing/mock-gateways/mock-user-session-gateway'; +import { MockUserSessionUseCase } from './testing/mock-gateways/mock-user-session-use-case'; import MockOpenIdConnectGateway from './testing/mock-gateways/mock-oauth2-gateway'; import { StorageGateway } from './adapters/types/storage'; import LocalStorageGateway from './adapters/gateways/storage/local-storage-gateway'; @@ -43,6 +41,7 @@ import { MockOrdersGateway } from './testing/mock-gateways/mock.orders.gateway'; import { MockOfficesGateway } from './testing/mock-gateways/mock.offices.gateway'; import { OfficesCosmosDbRepository } from './adapters/gateways/offices.cosmosdb.repository'; import OktaUserGroupGateway from './adapters/gateways/okta/okta-user-group-gateway'; +import { UserSessionUseCase } from './use-cases/user-session/user-session'; export const getAttorneyGateway = (): AttorneyGatewayInterface => { return MockAttorneysGateway; @@ -147,11 +146,11 @@ export const getAuthorizationGateway = (context: ApplicationContext): OpenIdConn return null; }; -export const getUserSessionGateway = (context: ApplicationContext): SessionGateway => { +export const getUserSessionUseCase = (context: ApplicationContext) => { if (context.config.authConfig.provider === 'mock') { - return new MockUserSessionGateway(); + return new MockUserSessionUseCase(); } - return new UserSessionGateway(); + return new UserSessionUseCase(); }; export const getUserSessionCacheRepository = ( diff --git a/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts b/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts index 7b35c1e33..bfce19cb8 100644 --- a/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts +++ b/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts @@ -65,11 +65,10 @@ export async function getUser(accessToken: string) { const decodedToken = jwt.decode(accessToken); const mockUser = mockUsers.find((role) => role.sub === decodedToken.sub); addSuperUserOffices(mockUser.user); - return mockUser.user; + return { user: mockUser.user, groups: [], jwt: {} as CamsJwt }; } const MockOpenIdConnectGateway: OpenIdConnectGateway = { - verifyToken, getUser, }; diff --git a/backend/functions/lib/testing/mock-gateways/mock-user-session-gateway.ts b/backend/functions/lib/testing/mock-gateways/mock-user-session-use-case.ts similarity index 88% rename from backend/functions/lib/testing/mock-gateways/mock-user-session-gateway.ts rename to backend/functions/lib/testing/mock-gateways/mock-user-session-use-case.ts index d9f0fea91..36ac8212f 100644 --- a/backend/functions/lib/testing/mock-gateways/mock-user-session-gateway.ts +++ b/backend/functions/lib/testing/mock-gateways/mock-user-session-use-case.ts @@ -1,6 +1,5 @@ import * as jwt from 'jsonwebtoken'; import { ApplicationContext } from '../../adapters/types/basic'; -import { SessionGateway } from '../../adapters/utils/session-gateway'; import { getUser } from './mock-oauth2-gateway'; import { OFFICES } from '../../../../../common/src/cams/test-utilities/offices.mock'; import { CamsSession } from '../../../../../common/src/cams/session'; @@ -9,13 +8,13 @@ import { CamsJwtClaims } from '../../../../../common/src/cams/jwt'; const cache = new Map(); -export class MockUserSessionGateway implements SessionGateway { +export class MockUserSessionUseCase { async lookup( context: ApplicationContext, accessToken: string, provider: string, ): Promise { - const user = await getUser(accessToken); + const { user } = await getUser(accessToken); const parts = accessToken.split('.'); const key = parts[2]; diff --git a/backend/functions/lib/adapters/gateways/user-session-gateway.test.ts b/backend/functions/lib/use-cases/user-session/user-session.test.ts similarity index 94% rename from backend/functions/lib/adapters/gateways/user-session-gateway.test.ts rename to backend/functions/lib/use-cases/user-session/user-session.test.ts index b3cbd7baa..dea3ac522 100644 --- a/backend/functions/lib/adapters/gateways/user-session-gateway.test.ts +++ b/backend/functions/lib/use-cases/user-session/user-session.test.ts @@ -1,5 +1,5 @@ -import { ConflictError, isConflictError, UserSessionGateway } from './user-session-gateway'; -import { ApplicationContext } from '../types/basic'; +import { ConflictError, isConflictError, UserSessionUseCase } from './user-session'; +import { ApplicationContext } from '../../adapters/types/basic'; import { createMockApplicationContext } from '../../testing/testing-utilities'; import { MockHumbleItems, MockHumbleQuery } from '../../testing/mock.cosmos-client-humble'; import { MockData } from '../../../../../common/src/cams/test-utilities/mock-data'; @@ -11,7 +11,7 @@ import { CamsRole } from '../../../../../common/src/cams/roles'; import { urlRegex } from '../../../../../common/src/cams/test-utilities/regex'; import { OFFICES } from '../../../../../common/src/cams/test-utilities/offices.mock'; import { CamsJwtHeader } from '../../../../../common/src/cams/jwt'; -import { UserSessionCacheCosmosDbRepository } from './user-session-cache.cosmosdb.repository'; +import { UserSessionCacheCosmosDbRepository } from '../../adapters/gateways/user-session-cache.cosmosdb.repository'; import MockOpenIdConnectGateway from '../../testing/mock-gateways/mock-oauth2-gateway'; describe('user-session.gateway test', () => { @@ -39,10 +39,10 @@ describe('user-session.gateway test', () => { expires: Number.MAX_SAFE_INTEGER, }; let context: ApplicationContext; - let gateway: UserSessionGateway; + let gateway: UserSessionUseCase; beforeEach(async () => { - gateway = new UserSessionGateway(); + gateway = new UserSessionUseCase(); context = await createMockApplicationContext({ env: { CAMS_LOGIN_PROVIDER: 'mock', CAMS_LOGIN_PROVIDER_CONFIG: 'something' }, }); @@ -56,7 +56,9 @@ describe('user-session.gateway test', () => { claims, header: jwtHeader as CamsJwtHeader, }); - jest.spyOn(MockOpenIdConnectGateway, 'getUser').mockResolvedValue(mockUser); + jest + .spyOn(MockOpenIdConnectGateway, 'getUser') + .mockResolvedValue({ user: mockUser, groups: [] }); }); afterEach(() => { diff --git a/backend/functions/lib/adapters/gateways/user-session-gateway.ts b/backend/functions/lib/use-cases/user-session/user-session.ts similarity index 84% rename from backend/functions/lib/adapters/gateways/user-session-gateway.ts rename to backend/functions/lib/use-cases/user-session/user-session.ts index db01a293d..38b23af3f 100644 --- a/backend/functions/lib/adapters/gateways/user-session-gateway.ts +++ b/backend/functions/lib/use-cases/user-session/user-session.ts @@ -1,15 +1,14 @@ -import { SessionGateway } from '../utils/session-gateway'; import { getAuthorizationGateway, getOfficesGateway, getUserSessionCacheRepository, } from '../../factory'; -import { ApplicationContext } from '../types/basic'; +import { ApplicationContext } from '../../adapters/types/basic'; import { UnauthorizedError } from '../../common-errors/unauthorized-error'; import { isCamsError } from '../../common-errors/cams-error'; import { ServerConfigError } from '../../common-errors/server-config-error'; import { OfficeDetails } from '../../../../../common/src/cams/courts'; -import LocalStorageGateway from './storage/local-storage-gateway'; +import LocalStorageGateway from '../../adapters/gateways/storage/local-storage-gateway'; import { OFFICES } from '../../../../../common/src/cams/test-utilities/offices.mock'; import { CamsRole } from '../../../../../common/src/cams/roles'; import { CamsSession } from '../../../../../common/src/cams/session'; @@ -73,7 +72,7 @@ async function getOffices( return offices; } -export class UserSessionGateway implements SessionGateway { +export class UserSessionUseCase { async lookup(context: ApplicationContext, token: string, provider: string): Promise { const sessionCacheRepository = getUserSessionCacheRepository(context); const cached = await sessionCacheRepository.get(context, token); @@ -89,20 +88,14 @@ export class UserSessionGateway implements SessionGateway { message: 'Unsupported authentication provider.', }); } - const jwt = await authGateway.verifyToken(token); - if (!jwt) { - throw new UnauthorizedError(MODULE_NAME, { - message: 'Unable to verify token.', - }); - } - const user = await authGateway.getUser(token); + + const { user, groups, jwt } = await authGateway.getUser(token); + user.roles = getRoles(groups); + user.offices = await getOffices(context, groups); // Simulate the legacy behavior by appending roles and Manhattan office to the user // if the 'restrict-case-assignment' feature flag is not set. - if (context.featureFlags['restrict-case-assignment']) { - user.roles = getRoles(jwt.claims.groups); - user.offices = await getOffices(context, jwt.claims.groups); - } else { + if (!context.featureFlags['restrict-case-assignment']) { user.offices = [OFFICES.find((office) => office.courtDivisionCode === '081')]; user.roles = [CamsRole.CaseAssignmentManager]; } From 9366ee51ab2bf4a41a72a4b5b6e41e9525d96e02 Mon Sep 17 00:00:00 2001 From: James Brooks <12275865+jamesobrooks@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:56:35 -0500 Subject: [PATCH 2/4] Fix tests Jira ticket: CAMS-446 Co-authored-by: Brian Posey <15091170+btposey@users.noreply.github.com>, --- .../gateways/okta/okta-gateway.test.ts | 29 ++++------ .../adapters/gateways/okta/okta-gateway.ts | 5 +- .../user-session/user-session.test.ts | 58 ++++++++++++------- .../use-cases/user-session/user-session.ts | 14 ++--- 4 files changed, 57 insertions(+), 49 deletions(-) diff --git a/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts b/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts index c667b0efc..f31a95aa6 100644 --- a/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts +++ b/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts @@ -31,7 +31,7 @@ describe('Okta gateway tests', () => { userInfoUri: 'something', }; jest.spyOn(AuthorizationConfiguration, 'getAuthorizationConfig').mockReturnValue(authConfig); - await expect(gateway.verifyToken('test')).rejects.toThrow('Invalid provider.'); + await expect(gateway.getUser('test')).rejects.toThrow('Invalid provider.'); }); test('Should receive invalid issuer error', async () => { @@ -42,7 +42,7 @@ describe('Okta gateway tests', () => { userInfoUri: 'something', }; jest.spyOn(AuthorizationConfiguration, 'getAuthorizationConfig').mockReturnValue(authConfig); - await expect(gateway.verifyToken('test')).rejects.toThrow('Issuer not provided.'); + await expect(gateway.getUser('test')).rejects.toThrow('Issuer not provided.'); }); test('Should receive invalid audience error', async () => { @@ -53,10 +53,10 @@ describe('Okta gateway tests', () => { userInfoUri: 'something', }; jest.spyOn(AuthorizationConfiguration, 'getAuthorizationConfig').mockReturnValue(authConfig); - await expect(gateway.verifyToken('test')).rejects.toThrow('Audience not provided.'); + await expect(gateway.getUser('test')).rejects.toThrow('Audience not provided.'); }); - test('Should return valid Jwt when given valid token and audience', async () => { + test('Should return valid user with Jwt when given valid token and audience', async () => { const token = 'testToken'; const jwtClaims = { iss: 'https://fake.okta.com/oauth2/default', @@ -79,27 +79,20 @@ describe('Okta gateway tests', () => { isNotBefore: jest.fn(), }; jest.spyOn(Verifier, 'verifyAccessToken').mockResolvedValue(jwt); - const actual = await gateway.verifyToken(token); - expect(actual).toEqual(jwt); - }); - - test('Should throw UnauthorizedError if not given valid input ', async () => { - const token = 'testToken'; - jest.spyOn(Verifier, 'verifyAccessToken').mockRejectedValue(new Error('Test error')); - await expect(gateway.verifyToken(token)).rejects.toThrow('Unauthorized'); - }); - - test('getUser should return a valid response with user.name', async () => { const userInfo = { name: 'Test Name', testAttribute: '', }; const mockFetchResponse = MockFetch.ok(userInfo); jest.spyOn(global, 'fetch').mockImplementation(mockFetchResponse); - const actualResponse = await gateway.getUser('testAccessToken'); + const actual = await gateway.getUser(token); + expect(actual).toEqual({ user: { id: undefined, name: userInfo.name }, groups: [], jwt }); + }); - expect(actualResponse).not.toEqual(expect.objectContaining({ testAttribute: '' })); - expect(actualResponse).toEqual(expect.objectContaining({ name: 'Test Name' })); + test('Should throw UnauthorizedError if not given valid input ', async () => { + const token = 'testToken'; + jest.spyOn(Verifier, 'verifyAccessToken').mockRejectedValue(new Error('Test error')); + await expect(gateway.getUser(token)).rejects.toThrow('Unauthorized'); }); test('getUser should throw Error if call failed', async () => { diff --git a/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts b/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts index 9a43f8984..edf16d653 100644 --- a/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts +++ b/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts @@ -5,6 +5,7 @@ import { UnauthorizedError } from '../../../common-errors/unauthorized-error'; import { verifyAccessToken } from './HumbleVerifier'; import { CamsUser } from '../../../../../../common/src/cams/users'; import { CamsJwt } from '../../../../../../common/src/cams/jwt'; +import { isCamsError } from '../../../common-errors/cams-error'; const MODULE_NAME = 'OKTA-GATEWAY'; @@ -81,7 +82,9 @@ async function getUser( throw new Error('Failed to retrieve user info from Okta.'); } } catch (originalError) { - throw new UnauthorizedError(MODULE_NAME, { originalError }); + throw isCamsError(originalError) + ? originalError + : new UnauthorizedError(MODULE_NAME, { originalError }); } } diff --git a/backend/functions/lib/use-cases/user-session/user-session.test.ts b/backend/functions/lib/use-cases/user-session/user-session.test.ts index dea3ac522..f30f8c956 100644 --- a/backend/functions/lib/use-cases/user-session/user-session.test.ts +++ b/backend/functions/lib/use-cases/user-session/user-session.test.ts @@ -13,9 +13,10 @@ import { OFFICES } from '../../../../../common/src/cams/test-utilities/offices.m import { CamsJwtHeader } from '../../../../../common/src/cams/jwt'; import { UserSessionCacheCosmosDbRepository } from '../../adapters/gateways/user-session-cache.cosmosdb.repository'; import MockOpenIdConnectGateway from '../../testing/mock-gateways/mock-oauth2-gateway'; +import * as Verifier from '../../adapters/gateways/okta/HumbleVerifier'; describe('user-session.gateway test', () => { - const jwt = MockData.getJwt(); + const jwtString = MockData.getJwt(); const claims = { iss: 'https://nonsense-3wjj23473kdwh2.okta.com/oauth2/default', sub: 'user@fake.com', @@ -28,12 +29,12 @@ describe('user-session.gateway test', () => { const mockUser = MockData.getCamsUser(); const expectedSession = MockData.getCamsSession({ user: mockUser, - accessToken: jwt, + accessToken: jwtString, provider, }); const mockCamsSession: CamsSession = { user: { id: 'userId-Wrong Name', name: 'Wrong Name' }, - accessToken: jwt, + accessToken: jwtString, provider, issuer: 'http://issuer/', expires: Number.MAX_SAFE_INTEGER, @@ -44,7 +45,7 @@ describe('user-session.gateway test', () => { beforeEach(async () => { gateway = new UserSessionUseCase(); context = await createMockApplicationContext({ - env: { CAMS_LOGIN_PROVIDER: 'mock', CAMS_LOGIN_PROVIDER_CONFIG: 'something' }, + env: { CAMS_LOGIN_PROVIDER: 'okta', CAMS_LOGIN_PROVIDER_CONFIG: 'something' }, }); const jwtHeader = { @@ -52,13 +53,18 @@ describe('user-session.gateway test', () => { typ: undefined, kid: '', }; - jest.spyOn(MockOpenIdConnectGateway, 'verifyToken').mockResolvedValue({ + const camsJwt = { claims, header: jwtHeader as CamsJwtHeader, - }); + }; + jest.spyOn(Verifier, 'verifyAccessToken').mockResolvedValue(camsJwt); + // jest.spyOn(MockOpenIdConnectGateway, 'verifyToken').mockResolvedValue({ + // claims, + // header: jwtHeader as CamsJwtHeader, + // }); jest .spyOn(MockOpenIdConnectGateway, 'getUser') - .mockResolvedValue({ user: mockUser, groups: [] }); + .mockResolvedValue({ user: mockUser, groups: [], jwt: camsJwt }); }); afterEach(() => { @@ -70,7 +76,7 @@ describe('user-session.gateway test', () => { const createSpy = jest .spyOn(UserSessionCacheCosmosDbRepository.prototype, 'put') .mockResolvedValue(mockCamsSession); - const session = await gateway.lookup(context, jwt, provider); + const session = await gateway.lookup(context, jwtString, provider); expect(session).toEqual({ ...expectedSession, expires: expect.any(Number), @@ -86,7 +92,7 @@ describe('user-session.gateway test', () => { const createSpy = jest .spyOn(UserSessionCacheCosmosDbRepository.prototype, 'put') .mockRejectedValue('We should not call this function.'); - const session = await gateway.lookup(context, jwt, provider); + const session = await gateway.lookup(context, jwtString, provider); expect(session).toEqual({ ...expectedSession, expires: expect.any(Number), @@ -100,10 +106,10 @@ describe('user-session.gateway test', () => { resources: [], }); jest - .spyOn(MockOpenIdConnectGateway, 'verifyToken') - .mockRejectedValue(new UnauthorizedError('TEST_USER_SESSION_GATEWAY')); + .spyOn(MockOpenIdConnectGateway, 'getUser') + .mockRejectedValue(new UnauthorizedError('test-module')); const createSpy = jest.spyOn(MockHumbleItems.prototype, 'create'); - await expect(gateway.lookup(context, jwt, provider)).rejects.toThrow(); + await expect(gateway.lookup(context, jwtString, provider)).rejects.toThrow(); expect(createSpy).not.toHaveBeenCalled(); }); @@ -111,16 +117,24 @@ describe('user-session.gateway test', () => { jest.spyOn(MockHumbleQuery.prototype, 'fetchAll').mockResolvedValue({ resources: [], }); - jest.spyOn(MockOpenIdConnectGateway, 'verifyToken').mockResolvedValue(null); - await expect(gateway.lookup(context, jwt, provider)).rejects.toThrow(UnauthorizedError); + jest.spyOn(MockOpenIdConnectGateway, 'getUser').mockResolvedValue({ + user: mockUser, + groups: [], + jwt: null, + }); + await expect(gateway.lookup(context, jwtString, provider)).rejects.toThrow(UnauthorizedError); }); test('should handle undefined jwt from authGateway', async () => { jest.spyOn(MockHumbleQuery.prototype, 'fetchAll').mockResolvedValue({ resources: [], }); - jest.spyOn(MockOpenIdConnectGateway, 'verifyToken').mockResolvedValue(undefined); - await expect(gateway.lookup(context, jwt, provider)).rejects.toThrow(UnauthorizedError); + jest.spyOn(MockOpenIdConnectGateway, 'getUser').mockResolvedValue({ + user: mockUser, + groups: [], + jwt: undefined, + }); + await expect(gateway.lookup(context, jwtString, provider)).rejects.toThrow(UnauthorizedError); }); test('should return valid session and NOT add to cache when Conflict error is received', async () => { @@ -141,9 +155,9 @@ describe('user-session.gateway test', () => { .spyOn(UserSessionCacheCosmosDbRepository.prototype, 'get') .mockResolvedValueOnce(null) .mockResolvedValue(mockCamsSession); - jest.spyOn(MockOpenIdConnectGateway, 'verifyToken').mockRejectedValue(conflictError); + jest.spyOn(MockOpenIdConnectGateway, 'getUser').mockRejectedValue(conflictError); const createSpy = jest.spyOn(MockHumbleItems.prototype, 'create'); - const session = await gateway.lookup(context, jwt, provider); + const session = await gateway.lookup(context, jwtString, provider); expect(session).toEqual({ ...mockCamsSession, expires: expect.any(Number), @@ -173,9 +187,9 @@ describe('user-session.gateway test', () => { jest.spyOn(MockHumbleQuery.prototype, 'fetchAll').mockResolvedValue({ resources: [], }); - jest.spyOn(MockOpenIdConnectGateway, 'verifyToken').mockRejectedValue(new Error('Test error')); + jest.spyOn(MockOpenIdConnectGateway, 'getUser').mockRejectedValue(new Error('Test error')); const createSpy = jest.spyOn(MockHumbleItems.prototype, 'create'); - await expect(gateway.lookup(context, jwt, provider)).rejects.toThrow(UnauthorizedError); + await expect(gateway.lookup(context, jwtString, provider)).rejects.toThrow(UnauthorizedError); expect(createSpy).not.toHaveBeenCalled(); }); @@ -184,7 +198,7 @@ describe('user-session.gateway test', () => { resources: [], }); jest.spyOn(factoryModule, 'getAuthorizationGateway').mockReturnValue(null); - await expect(gateway.lookup(context, jwt, provider)).rejects.toThrow(ServerConfigError); + await expect(gateway.lookup(context, jwtString, provider)).rejects.toThrow(ServerConfigError); }); test('should use legacy behavior if restrict-case-assignment feature flag is not set', async () => { @@ -198,7 +212,7 @@ describe('user-session.gateway test', () => { const localContext = { ...context, featureFlags: { ...context.featureFlags } }; localContext.featureFlags['restrict-case-assignment'] = false; - const session = await gateway.lookup(localContext, jwt, provider); + const session = await gateway.lookup(localContext, jwtString, provider); expect(session.user.offices).toEqual([ OFFICES.find((office) => office.courtDivisionCode === '081'), ]); diff --git a/backend/functions/lib/use-cases/user-session/user-session.ts b/backend/functions/lib/use-cases/user-session/user-session.ts index 38b23af3f..1cb08085d 100644 --- a/backend/functions/lib/use-cases/user-session/user-session.ts +++ b/backend/functions/lib/use-cases/user-session/user-session.ts @@ -116,14 +116,12 @@ export class UserSessionUseCase { return await sessionCacheRepository.get(context, token); } - if (isCamsError(originalError)) { - throw originalError; - } - - throw new UnauthorizedError(MODULE_NAME, { - message: originalError.message, - originalError, - }); + throw isCamsError(originalError) + ? originalError + : new UnauthorizedError(MODULE_NAME, { + message: originalError.message, + originalError, + }); } } } From eec13e7e1ee13f59fa80f6ef519e7a06b3f03131 Mon Sep 17 00:00:00 2001 From: Brian Posey <15091170+btposey@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:07:50 -0400 Subject: [PATCH 3/4] Test unifying ad_group and group claims for Okta Jira ticket: CAMS-446 Co-authored-by: James Brooks <12275865+jamesobrooks@users.noreply.github.com> Co-authored-by: Brian Posey <15091170+btposey@users.noreply.github.com>, --- .../lib/adapters/gateways/okta/okta-gateway.test.ts | 9 +++++++-- .../functions/lib/adapters/gateways/okta/okta-gateway.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts b/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts index f31a95aa6..722027348 100644 --- a/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts +++ b/backend/functions/lib/adapters/gateways/okta/okta-gateway.test.ts @@ -64,7 +64,8 @@ describe('Okta gateway tests', () => { aud: 'api://default', iat: 0, exp: Math.floor(Date.now() / 1000) + 600, - groups: [], + ad_groups: ['groupA', 'groupB'], + groups: ['groupB', 'groupC'], }; const jwtHeader = { alg: 'RS256', @@ -86,7 +87,11 @@ describe('Okta gateway tests', () => { const mockFetchResponse = MockFetch.ok(userInfo); jest.spyOn(global, 'fetch').mockImplementation(mockFetchResponse); const actual = await gateway.getUser(token); - expect(actual).toEqual({ user: { id: undefined, name: userInfo.name }, groups: [], jwt }); + expect(actual).toEqual({ + user: { id: undefined, name: userInfo.name }, + groups: ['groupA', 'groupB', 'groupC'], + jwt, + }); }); test('Should throw UnauthorizedError if not given valid input ', async () => { diff --git a/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts b/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts index edf16d653..43214d2e9 100644 --- a/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts +++ b/backend/functions/lib/adapters/gateways/okta/okta-gateway.ts @@ -75,7 +75,7 @@ async function getUser( }; const claims = jwt.claims as unknown as DojLoginUnifiedGroupClaims; - const groups: string[] = [].concat(claims.ad_groups, claims.groups); + const groups = Array.from(new Set([].concat(claims.ad_groups, claims.groups))); return { user, groups, jwt }; } else { From c2c6a6ac709469477ec172d760a7b21797cd6434 Mon Sep 17 00:00:00 2001 From: Brian Posey <15091170+btposey@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:14:05 -0400 Subject: [PATCH 4/4] Remove unused code Jira ticket: CAMS-446 Co-authored-by: James Brooks <12275865+jamesobrooks@users.noreply.github.com> Co-authored-by: Brian Posey <15091170+btposey@users.noreply.github.com>, --- .../functions/lib/use-cases/user-session/user-session.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/functions/lib/use-cases/user-session/user-session.test.ts b/backend/functions/lib/use-cases/user-session/user-session.test.ts index f30f8c956..b3f6b60e6 100644 --- a/backend/functions/lib/use-cases/user-session/user-session.test.ts +++ b/backend/functions/lib/use-cases/user-session/user-session.test.ts @@ -58,10 +58,6 @@ describe('user-session.gateway test', () => { header: jwtHeader as CamsJwtHeader, }; jest.spyOn(Verifier, 'verifyAccessToken').mockResolvedValue(camsJwt); - // jest.spyOn(MockOpenIdConnectGateway, 'verifyToken').mockResolvedValue({ - // claims, - // header: jwtHeader as CamsJwtHeader, - // }); jest .spyOn(MockOpenIdConnectGateway, 'getUser') .mockResolvedValue({ user: mockUser, groups: [], jwt: camsJwt });