mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
feat(auth): add publishing session created and session refreshed events
This commit is contained in:
@@ -9,6 +9,8 @@ import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { KeyParamsFactoryInterface } from '../User/KeyParamsFactoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { AuthResponseFactory20200115 } from './AuthResponseFactory20200115'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('AuthResponseFactory20200115', () => {
|
||||
let sessionService: SessionServiceInterface
|
||||
@@ -18,13 +20,24 @@ describe('AuthResponseFactory20200115', () => {
|
||||
let sessionPayload: SessionBody
|
||||
let logger: Logger
|
||||
let tokenEncoder: TokenEncoderInterface<SessionTokenData>
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createFactory = () =>
|
||||
new AuthResponseFactory20200115(sessionService, keyParamsFactory, userProjector, tokenEncoder, logger)
|
||||
new AuthResponseFactory20200115(
|
||||
sessionService,
|
||||
keyParamsFactory,
|
||||
userProjector,
|
||||
tokenEncoder,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
|
||||
sessionPayload = {
|
||||
access_token: 'access_token',
|
||||
@@ -52,6 +65,12 @@ describe('AuthResponseFactory20200115', () => {
|
||||
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<SessionTokenData>>
|
||||
tokenEncoder.encodeToken = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createSessionCreatedEvent = jest.fn().mockReturnValue({})
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should create a 20161215 auth response if user does not support sessions', async () => {
|
||||
@@ -82,6 +101,37 @@ describe('AuthResponseFactory20200115', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
key_params: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
session: {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
access_expiration: 123,
|
||||
refresh_expiration: 234,
|
||||
readonly_access: false,
|
||||
},
|
||||
user: {
|
||||
foo: 'bar',
|
||||
},
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create a 20200115 auth response even if publishing the domain event fails', async () => {
|
||||
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test'))
|
||||
user.supportsSessions = jest.fn().mockReturnValue(true)
|
||||
|
||||
const response = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
key_params: {
|
||||
key1: 'value1',
|
||||
|
||||
@@ -3,18 +3,22 @@ import {
|
||||
SessionTokenData,
|
||||
TokenEncoderInterface,
|
||||
} from '@standardnotes/security'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { KeyParamsFactoryInterface } from '../User/KeyParamsFactoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { AuthResponseFactory20190520 } from './AuthResponseFactory20190520'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
import { AuthResponseFactory20190520 } from './AuthResponseFactory20190520'
|
||||
|
||||
@injectable()
|
||||
export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
@@ -23,6 +27,8 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
@inject(TYPES.KeyParamsFactory) private keyParamsFactory: KeyParamsFactoryInterface,
|
||||
@inject(TYPES.UserProjector) userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.SessionTokenEncoder) protected override tokenEncoder: TokenEncoderInterface<SessionTokenData>,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Logger) logger: Logger,
|
||||
) {
|
||||
super(userProjector, tokenEncoder, logger)
|
||||
@@ -67,6 +73,16 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
return this.sessionService.createNewEphemeralSessionForUser(dto)
|
||||
}
|
||||
|
||||
return this.sessionService.createNewSessionForUser(dto)
|
||||
const session = this.sessionService.createNewSessionForUser(dto)
|
||||
|
||||
try {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSessionCreatedEvent({ userUuid: dto.user.uuid }),
|
||||
)
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to publish session created event: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
MuteEmailsSettingChangedEvent,
|
||||
EmailRequestedEvent,
|
||||
StatisticPersistenceRequestedEvent,
|
||||
SessionCreatedEvent,
|
||||
SessionRefreshedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -31,6 +33,36 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent {
|
||||
return {
|
||||
type: 'SESSION_CREATED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createSessionRefreshedEvent(dto: { userUuid: string }): SessionRefreshedEvent {
|
||||
return {
|
||||
type: 'SESSION_REFRESHED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createStatisticPersistenceRequestedEvent(dto: {
|
||||
statisticMeasureName: string
|
||||
value: number
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
MuteEmailsSettingChangedEvent,
|
||||
EmailRequestedEvent,
|
||||
StatisticPersistenceRequestedEvent,
|
||||
SessionCreatedEvent,
|
||||
SessionRefreshedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
|
||||
@@ -92,4 +94,6 @@ export interface DomainEventFactoryInterface {
|
||||
value: number
|
||||
date: number
|
||||
}): StatisticPersistenceRequestedEvent
|
||||
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent
|
||||
createSessionRefreshedEvent(dto: { userUuid: string }): SessionRefreshedEvent
|
||||
}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import 'reflect-metadata'
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { Session } from '../Session/Session'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { RefreshSessionToken } from './RefreshSessionToken'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('RefreshSessionToken', () => {
|
||||
let sessionService: SessionServiceInterface
|
||||
let session: Session
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let timer: TimerInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new RefreshSessionToken(sessionService)
|
||||
const createUseCase = () =>
|
||||
new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
session.uuid = '1-2-3'
|
||||
session.refreshExpiration = dayjs.utc().add(1, 'day').toDate()
|
||||
session.refreshExpiration = new Date(123)
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.isRefreshTokenValid = jest.fn().mockReturnValue(true)
|
||||
@@ -25,6 +33,18 @@ describe('RefreshSessionToken', () => {
|
||||
access_expiration: 123,
|
||||
refresh_expiration: 234,
|
||||
})
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createSessionRefreshedEvent = jest.fn().mockReturnValue({})
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(100))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should refresh session token', async () => {
|
||||
@@ -44,6 +64,29 @@ describe('RefreshSessionToken', () => {
|
||||
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'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
accessToken: '123',
|
||||
refreshToken: '234',
|
||||
})
|
||||
|
||||
expect(sessionService.refreshTokens).toHaveBeenCalledWith(session)
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
sessionPayload: {
|
||||
access_token: 'token1',
|
||||
refresh_token: 'token2',
|
||||
access_expiration: 123,
|
||||
refresh_expiration: 234,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should not refresh a session token if session is not found', async () => {
|
||||
@@ -77,7 +120,7 @@ describe('RefreshSessionToken', () => {
|
||||
})
|
||||
|
||||
it('should not refresh a session token if refresh token is expired', async () => {
|
||||
session.refreshExpiration = dayjs.utc().subtract(1, 'day').toDate()
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(200))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
accessToken: '123',
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
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'
|
||||
|
||||
@injectable()
|
||||
export class RefreshSessionToken {
|
||||
constructor(@inject(TYPES.SessionService) private sessionService: SessionServiceInterface) {}
|
||||
constructor(
|
||||
@inject(TYPES.SessionService) private sessionService: SessionServiceInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> {
|
||||
const session = await this.sessionService.getSessionFromToken(dto.accessToken)
|
||||
@@ -28,7 +38,7 @@ export class RefreshSessionToken {
|
||||
}
|
||||
}
|
||||
|
||||
if (session.refreshExpiration < dayjs.utc().toDate()) {
|
||||
if (session.refreshExpiration < this.timer.getUTCDate()) {
|
||||
return {
|
||||
success: false,
|
||||
errorTag: 'expired-refresh-token',
|
||||
@@ -38,6 +48,14 @@ export class RefreshSessionToken {
|
||||
|
||||
const sessionPayload = await this.sessionService.refreshTokens(session)
|
||||
|
||||
try {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSessionRefreshedEvent({ userUuid: session.userUuid }),
|
||||
)
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to publish session refreshed event: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionPayload,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SessionCreatedEventPayload } from './SessionCreatedEventPayload'
|
||||
|
||||
export interface SessionCreatedEvent extends DomainEventInterface {
|
||||
type: 'SESSION_CREATED'
|
||||
payload: SessionCreatedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface SessionCreatedEventPayload {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SessionRefreshedEventPayload } from './SessionRefreshedEventPayload'
|
||||
|
||||
export interface SessionRefreshedEvent extends DomainEventInterface {
|
||||
type: 'SESSION_REFRESHED'
|
||||
payload: SessionRefreshedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface SessionRefreshedEventPayload {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -54,6 +54,10 @@ export * from './Event/RefundProcessedEvent'
|
||||
export * from './Event/RefundProcessedEventPayload'
|
||||
export * from './Event/RevisionsCopyRequestedEvent'
|
||||
export * from './Event/RevisionsCopyRequestedEventPayload'
|
||||
export * from './Event/SessionCreatedEvent'
|
||||
export * from './Event/SessionCreatedEventPayload'
|
||||
export * from './Event/SessionRefreshedEvent'
|
||||
export * from './Event/SessionRefreshedEventPayload'
|
||||
export * from './Event/SharedSubscriptionInvitationCanceledEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
|
||||
|
||||
Reference in New Issue
Block a user