From 0b46eff16ea0c32cac91ead04474303500359f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 9 Jan 2024 09:57:02 +0100 Subject: [PATCH] fix(auth): check for user agent persisting on session during a session refresh (#1016) --- packages/auth/src/Bootstrap/Container.ts | 14 ++++++- .../UseCase/RefreshSessionToken.spec.ts | 39 ++++++++++++++++++- .../src/Domain/UseCase/RefreshSessionToken.ts | 36 ++++++++++++----- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 23fd0bcf7..0c23af087 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -284,6 +284,7 @@ import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions' import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser' import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler' +import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface' export class ContainerConfigLoader { constructor(private mode: 'server' | 'worker' = 'server') {} @@ -986,7 +987,18 @@ export class ContainerConfigLoader { .toConstantValue(new CleanupExpiredSessions(container.get(TYPES.Auth_SessionRepository))) container.bind(TYPES.Auth_AuthenticateUser).to(AuthenticateUser) container.bind(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest) - container.bind(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken) + container + .bind(TYPES.Auth_RefreshSessionToken) + .toConstantValue( + new RefreshSessionToken( + container.get(TYPES.Auth_SessionService), + container.get(TYPES.Auth_DomainEventFactory), + container.get(TYPES.Auth_DomainEventPublisher), + container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_Logger), + ), + ) container.bind(TYPES.Auth_SignIn).to(SignIn) container .bind(TYPES.Auth_VerifyMFA) diff --git a/packages/auth/src/Domain/UseCase/RefreshSessionToken.spec.ts b/packages/auth/src/Domain/UseCase/RefreshSessionToken.spec.ts index a64755a7f..6f94de520 100644 --- a/packages/auth/src/Domain/UseCase/RefreshSessionToken.spec.ts +++ b/packages/auth/src/Domain/UseCase/RefreshSessionToken.spec.ts @@ -7,6 +7,10 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' import { Logger } from 'winston' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' +import { GetSetting } from './GetSetting/GetSetting' +import { Result } from '@standardnotes/domain-core' +import { LogSessionUserAgentOption } from '@standardnotes/settings' +import { Setting } from '../Setting/Setting' describe('RefreshSessionToken', () => { let sessionService: SessionServiceInterface @@ -14,16 +18,20 @@ describe('RefreshSessionToken', () => { let domainEventFactory: DomainEventFactoryInterface let domainEventPublisher: DomainEventPublisherInterface let timer: TimerInterface + let getSetting: GetSetting let logger: Logger const createUseCase = () => - new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, logger) + new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, getSetting, logger) beforeEach(() => { session = {} as jest.Mocked session.uuid = '1-2-3' session.refreshExpiration = new Date(123) + getSetting = {} as jest.Mocked + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) + sessionService = {} as jest.Mocked sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true) sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false }) @@ -69,6 +77,35 @@ describe('RefreshSessionToken', () => { expect(domainEventPublisher.publish).toHaveBeenCalled() }) + it('should refresh session token and update user agent if enabled', async () => { + getSetting.execute = jest.fn().mockReturnValue( + Result.ok({ + setting: {} as jest.Mocked, + decryptedValue: LogSessionUserAgentOption.Enabled, + }), + ) + + const result = await createUseCase().execute({ + accessToken: '123', + refreshToken: '234', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + }) + + expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false }) + + expect(result).toEqual({ + success: true, + sessionPayload: { + access_token: 'token1', + refresh_token: 'token2', + access_expiration: 123, + refresh_expiration: 234, + }, + }) + + expect(domainEventPublisher.publish).toHaveBeenCalled() + }) + it('should refresh a session token even if publishing domain event fails', async () => { domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test')) diff --git a/packages/auth/src/Domain/UseCase/RefreshSessionToken.ts b/packages/auth/src/Domain/UseCase/RefreshSessionToken.ts index 1a1c34c40..846f81e22 100644 --- a/packages/auth/src/Domain/UseCase/RefreshSessionToken.ts +++ b/packages/auth/src/Domain/UseCase/RefreshSessionToken.ts @@ -1,23 +1,24 @@ -import { inject, injectable } from 'inversify' import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' +import { SettingName } from '@standardnotes/domain-core' +import { LogSessionUserAgentOption } from '@standardnotes/settings' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' import { SessionServiceInterface } from '../Session/SessionServiceInterface' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { RefreshSessionTokenResponse } from './RefreshSessionTokenResponse' import { RefreshSessionTokenDTO } from './RefreshSessionTokenDTO' +import { GetSetting } from './GetSetting/GetSetting' -@injectable() export class RefreshSessionToken { constructor( - @inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface, - @inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface, - @inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface, - @inject(TYPES.Auth_Timer) private timer: TimerInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private sessionService: SessionServiceInterface, + private domainEventFactory: DomainEventFactoryInterface, + private domainEventPublisher: DomainEventPublisherInterface, + private timer: TimerInterface, + private getSetting: GetSetting, + private logger: Logger, ) {} async execute(dto: RefreshSessionTokenDTO): Promise { @@ -46,7 +47,9 @@ export class RefreshSessionToken { } } - session.userAgent = dto.userAgent + if (await this.isLoggingUserAgentEnabledOnSessions(session.userUuid)) { + session.userAgent = dto.userAgent + } const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral }) @@ -64,4 +67,19 @@ export class RefreshSessionToken { userUuid: session.userUuid, } } + + private async isLoggingUserAgentEnabledOnSessions(userUuid: string): Promise { + const loggingSettingOrError = await this.getSetting.execute({ + settingName: SettingName.NAMES.LogSessionUserAgent, + decrypted: true, + userUuid: userUuid, + allowSensitiveRetrieval: true, + }) + if (loggingSettingOrError.isFailed()) { + return true + } + const loggingSetting = loggingSettingOrError.getValue() + + return loggingSetting.decryptedValue === LogSessionUserAgentOption.Enabled + } }