瀏覽代碼

fix(auth): check for user agent persisting on session during a session refresh (#1016)

Karol Sójko 1 年之前
父節點
當前提交
0b46eff16e

+ 13 - 1
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<AuthenticateUser>(TYPES.Auth_AuthenticateUser).to(AuthenticateUser)
     container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest)
-    container.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken)
+    container
+      .bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken)
+      .toConstantValue(
+        new RefreshSessionToken(
+          container.get<SessionServiceInterface>(TYPES.Auth_SessionService),
+          container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
+          container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
     container
       .bind<VerifyMFA>(TYPES.Auth_VerifyMFA)

+ 38 - 1
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>
     session.uuid = '1-2-3'
     session.refreshExpiration = new Date(123)
 
+    getSetting = {} as jest.Mocked<GetSetting>
+    getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found'))
+
     sessionService = {} as jest.Mocked<SessionServiceInterface>
     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<Setting>,
+        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'))
 

+ 27 - 9
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<RefreshSessionTokenResponse> {
@@ -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<boolean> {
+    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
+  }
 }