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

This commit is contained in:
Karol Sójko 2024-01-09 09:57:02 +01:00 committed by GitHub
parent df67982bca
commit 0b46eff16e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 11 deletions

View file

@ -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)

View file

@ -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'))

View file

@ -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
}
}