diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 313caddca..155dc5cff 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -1195,6 +1195,7 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_GetRegularSubscriptionForUser), container.get(TYPES.Auth_GetSubscriptionSetting), container.get(TYPES.Auth_SharedVaultUserRepository), + container.get(TYPES.Auth_GetActiveSessionsForUser), ), ) container.bind(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest) diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts index 5e4404ca3..2228f9848 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts @@ -22,6 +22,7 @@ import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/ import { UserSubscription } from '../../Subscription/UserSubscription' import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { GetActiveSessionsForUser } from '../GetActiveSessionsForUser' describe('CreateCrossServiceToken', () => { let userProjector: ProjectorInterface @@ -32,6 +33,7 @@ describe('CreateCrossServiceToken', () => { let getRegularSubscription: GetRegularSubscriptionForUser let getSubscriptionSetting: GetSubscriptionSetting let sharedVaultUserRepository: SharedVaultUserRepositoryInterface + let getActiveSessionsForUser: GetActiveSessionsForUser const jwtTTL = 60 let session: Session @@ -49,11 +51,15 @@ describe('CreateCrossServiceToken', () => { getRegularSubscription, getSubscriptionSetting, sharedVaultUserRepository, + getActiveSessionsForUser, ) beforeEach(() => { session = {} as jest.Mocked + getActiveSessionsForUser = {} as jest.Mocked + getActiveSessionsForUser.execute = jest.fn().mockReturnValue({ sessions: [session] }) + user = { uuid: '00000000-0000-0000-0000-000000000000', email: 'test@test.te', @@ -195,6 +201,69 @@ describe('CreateCrossServiceToken', () => { ) }) + it('should create a cross service token for a user and a specific session', async () => { + await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sessionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( + { + roles: [ + { + name: 'role1', + uuid: '1-3-4', + }, + ], + belongs_to_shared_vaults: [ + { + shared_vault_uuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + }, + ], + session: { + test: 'test', + }, + user: { + email: 'test@test.te', + uuid: '00000000-0000-0000-0000-000000000000', + }, + }, + 60, + ) + }) + + it('should create a cross service token for a user and specific session if the session is missing', async () => { + getActiveSessionsForUser.execute = jest.fn().mockReturnValue({ sessions: [] }) + + await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sessionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( + { + roles: [ + { + name: 'role1', + uuid: '1-3-4', + }, + ], + belongs_to_shared_vaults: [ + { + shared_vault_uuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + }, + ], + user: { + email: 'test@test.te', + uuid: '00000000-0000-0000-0000-000000000000', + }, + }, + 60, + ) + }) + it('should throw an error if user does not exist', async () => { userRepository.findOneByUuid = jest.fn().mockReturnValue(null) diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts index 8e3c1873b..7b3dcafed 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts @@ -11,6 +11,7 @@ import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO' import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetActiveSessionsForUser } from '../GetActiveSessionsForUser' export class CreateCrossServiceToken implements UseCaseInterface { constructor( @@ -23,6 +24,7 @@ export class CreateCrossServiceToken implements UseCaseInterface { private getRegularSubscription: GetRegularSubscriptionForUser, private getSubscriptionSettingUseCase: GetSubscriptionSetting, private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, + private getActiveSessions: GetActiveSessionsForUser, ) {} async execute(dto: CreateCrossServiceTokenDTO): Promise> { @@ -84,6 +86,14 @@ export class CreateCrossServiceToken implements UseCaseInterface { if (dto.session !== undefined) { authTokenData.session = this.projectSession(dto.session) + } else if (dto.sessionUuid !== undefined) { + const activeSessionsResponse = await this.getActiveSessions.execute({ + userUuid: user.uuid, + sessionUuid: dto.sessionUuid, + }) + if (activeSessionsResponse.sessions.length) { + authTokenData.session = this.projectSession(activeSessionsResponse.sessions[0]) + } } return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL)) diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceTokenDTO.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceTokenDTO.ts index 346fdb773..25c88cd73 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceTokenDTO.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceTokenDTO.ts @@ -10,5 +10,6 @@ export type CreateCrossServiceTokenDTO = Either< }, { userUuid: string + sessionUuid?: string } > diff --git a/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.spec.ts b/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.spec.ts index 6619f903c..2d18814a3 100644 --- a/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.spec.ts +++ b/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.spec.ts @@ -65,4 +65,10 @@ describe('GetActiveSessionsForUser', () => { expect(sessionRepository.findAllByRefreshExpirationAndUserUuid).toHaveBeenCalledWith('1-2-3') }) + + it('should get a single session for a user', async () => { + expect(await createUseCase().execute({ userUuid: '1-2-3', sessionUuid: '2-3-4' })).toEqual({ + sessions: [session2], + }) + }) }) diff --git a/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.ts b/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.ts index 8647a2c86..a9798a9e2 100644 --- a/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.ts +++ b/packages/auth/src/Domain/UseCase/GetActiveSessionsForUser.ts @@ -5,6 +5,7 @@ import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterfac import { GetActiveSessionsForUserDTO } from './GetActiveSessionsForUserDTO' import { GetActiveSessionsForUserResponse } from './GetActiveSessionsForUserResponse' import { UseCaseInterface } from './UseCaseInterface' +import { Session } from '../Session/Session' @injectable() export class GetActiveSessionsForUser implements UseCaseInterface { @@ -18,13 +19,26 @@ export class GetActiveSessionsForUser implements UseCaseInterface { const ephemeralSessions = await this.ephemeralSessionRepository.findAllByUserUuid(dto.userUuid) const sessions = await this.sessionRepository.findAllByRefreshExpirationAndUserUuid(dto.userUuid) - return { - sessions: sessions.concat(ephemeralSessions).sort((a, b) => { - const dateA = a.refreshExpiration instanceof Date ? a.refreshExpiration : new Date(a.refreshExpiration) - const dateB = b.refreshExpiration instanceof Date ? b.refreshExpiration : new Date(b.refreshExpiration) + const activeSessions = sessions.concat(ephemeralSessions).sort((a, b) => { + const dateA = a.refreshExpiration instanceof Date ? a.refreshExpiration : new Date(a.refreshExpiration) + const dateB = b.refreshExpiration instanceof Date ? b.refreshExpiration : new Date(b.refreshExpiration) - return dateB.getTime() - dateA.getTime() - }), + return dateB.getTime() - dateA.getTime() + }) + + if (dto.sessionUuid) { + let sessions: Session[] = [] + const session = activeSessions.find((session) => session.uuid === dto.sessionUuid) + if (session) { + sessions = [session] + } + return { + sessions, + } + } + + return { + sessions: activeSessions, } } } diff --git a/packages/auth/src/Domain/UseCase/GetActiveSessionsForUserDTO.ts b/packages/auth/src/Domain/UseCase/GetActiveSessionsForUserDTO.ts index 9a3100b10..14d3acbf6 100644 --- a/packages/auth/src/Domain/UseCase/GetActiveSessionsForUserDTO.ts +++ b/packages/auth/src/Domain/UseCase/GetActiveSessionsForUserDTO.ts @@ -1,3 +1,4 @@ export type GetActiveSessionsForUserDTO = { userUuid: string + sessionUuid?: string } diff --git a/packages/security/src/Domain/Token/WebSocketConnectionToken.ts b/packages/security/src/Domain/Token/WebSocketConnectionToken.ts index 53a3afc7e..2803476d1 100644 --- a/packages/security/src/Domain/Token/WebSocketConnectionToken.ts +++ b/packages/security/src/Domain/Token/WebSocketConnectionToken.ts @@ -1,3 +1,4 @@ export type WebSocketConnectionTokenData = { userUuid: string + sessionUuid: string } diff --git a/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnection.spec.ts b/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnection.spec.ts index f896c2478..09e47e17f 100644 --- a/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnection.spec.ts +++ b/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnection.spec.ts @@ -16,10 +16,10 @@ describe('CreateWebSocketConnection', () => { }) it('should create a web socket connection token', async () => { - const result = await createUseCase().execute({ userUuid: '1-2-3' }) + const result = await createUseCase().execute({ userUuid: '1-2-3', sessionUuid: '4-5-6' }) expect(result.token).toEqual('foobar') - expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith({ userUuid: '1-2-3' }, 30) + expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith({ userUuid: '1-2-3', sessionUuid: '4-5-6' }, 30) }) }) diff --git a/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionDTO.ts b/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionDTO.ts index 6731d7faf..8eaeba0a3 100644 --- a/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionDTO.ts +++ b/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionDTO.ts @@ -1,3 +1,4 @@ export type CreateWebSocketConnectionDTO = { userUuid: string + sessionUuid: string } diff --git a/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken.ts b/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken.ts index f2923d55b..e118c8898 100644 --- a/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken.ts +++ b/packages/websockets/src/Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken.ts @@ -17,6 +17,7 @@ export class CreateWebSocketConnectionToken implements UseCaseInterface { async execute(dto: CreateWebSocketConnectionDTO): Promise { const data: WebSocketConnectionTokenData = { userUuid: dto.userUuid, + sessionUuid: dto.sessionUuid, } return { diff --git a/packages/websockets/src/Infra/InversifyExpressUtils/AnnotatedWebSocketsController.ts b/packages/websockets/src/Infra/InversifyExpressUtils/AnnotatedWebSocketsController.ts index a7e7131dd..39888af9f 100644 --- a/packages/websockets/src/Infra/InversifyExpressUtils/AnnotatedWebSocketsController.ts +++ b/packages/websockets/src/Infra/InversifyExpressUtils/AnnotatedWebSocketsController.ts @@ -28,6 +28,7 @@ export class AnnotatedWebSocketsController extends BaseHttpController { async createConnectionToken(_request: Request, response: Response): Promise { const result = await this.createWebSocketConnectionToken.execute({ userUuid: response.locals.user.uuid, + sessionUuid: response.locals.session.uuid, }) return this.json(result)