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 { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser' import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler' import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
export class ContainerConfigLoader { export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {} constructor(private mode: 'server' | 'worker' = 'server') {}
@ -986,7 +987,18 @@ export class ContainerConfigLoader {
.toConstantValue(new CleanupExpiredSessions(container.get(TYPES.Auth_SessionRepository))) .toConstantValue(new CleanupExpiredSessions(container.get(TYPES.Auth_SessionRepository)))
container.bind<AuthenticateUser>(TYPES.Auth_AuthenticateUser).to(AuthenticateUser) container.bind<AuthenticateUser>(TYPES.Auth_AuthenticateUser).to(AuthenticateUser)
container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest) 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<SignIn>(TYPES.Auth_SignIn).to(SignIn)
container container
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA) .bind<VerifyMFA>(TYPES.Auth_VerifyMFA)

View file

@ -7,6 +7,10 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston' import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' 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', () => { describe('RefreshSessionToken', () => {
let sessionService: SessionServiceInterface let sessionService: SessionServiceInterface
@ -14,16 +18,20 @@ describe('RefreshSessionToken', () => {
let domainEventFactory: DomainEventFactoryInterface let domainEventFactory: DomainEventFactoryInterface
let domainEventPublisher: DomainEventPublisherInterface let domainEventPublisher: DomainEventPublisherInterface
let timer: TimerInterface let timer: TimerInterface
let getSetting: GetSetting
let logger: Logger let logger: Logger
const createUseCase = () => const createUseCase = () =>
new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, logger) new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, getSetting, logger)
beforeEach(() => { beforeEach(() => {
session = {} as jest.Mocked<Session> session = {} as jest.Mocked<Session>
session.uuid = '1-2-3' session.uuid = '1-2-3'
session.refreshExpiration = new Date(123) 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 = {} as jest.Mocked<SessionServiceInterface>
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true) sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false }) sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
@ -69,6 +77,35 @@ describe('RefreshSessionToken', () => {
expect(domainEventPublisher.publish).toHaveBeenCalled() 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 () => { it('should refresh a session token even if publishing domain event fails', async () => {
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test')) 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 { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { SettingName } from '@standardnotes/domain-core'
import { LogSessionUserAgentOption } from '@standardnotes/settings'
import { Logger } from 'winston' import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { SessionServiceInterface } from '../Session/SessionServiceInterface' import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { RefreshSessionTokenResponse } from './RefreshSessionTokenResponse' import { RefreshSessionTokenResponse } from './RefreshSessionTokenResponse'
import { RefreshSessionTokenDTO } from './RefreshSessionTokenDTO' import { RefreshSessionTokenDTO } from './RefreshSessionTokenDTO'
import { GetSetting } from './GetSetting/GetSetting'
@injectable()
export class RefreshSessionToken { export class RefreshSessionToken {
constructor( constructor(
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface, private sessionService: SessionServiceInterface,
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface, private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface, private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface, private timer: TimerInterface,
@inject(TYPES.Auth_Logger) private logger: Logger, private getSetting: GetSetting,
private logger: Logger,
) {} ) {}
async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> { 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 }) const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral })
@ -64,4 +67,19 @@ export class RefreshSessionToken {
userUuid: session.userUuid, 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
}
} }