fix(auth): check for user agent persisting on session during a session refresh (#1016)
This commit is contained in:
parent
df67982bca
commit
0b46eff16e
3 changed files with 78 additions and 11 deletions
|
@ -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)
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue