feat(auth): retain user agent, api version and received at on revoked sessions

This commit is contained in:
Karol Sójko
2022-08-29 13:18:05 +02:00
parent b7f7c3f164
commit dc88e2413b
9 changed files with 96 additions and 6 deletions
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class revokedSessionData1661771230400 implements MigrationInterface {
name = 'revokedSessionData1661771230400'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `received_at` datetime NULL')
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `user_agent` text NULL')
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `api_version` varchar(255) NULL')
}
public async down(): Promise<void> {
return
}
}
@@ -4,17 +4,23 @@ import { UserDisabledSessionUserAgentLoggingEvent } from '@standardnotes/domain-
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { UserDisabledSessionUserAgentLoggingEventHandler } from './UserDisabledSessionUserAgentLoggingEventHandler'
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
describe('UserDisabledSessionUserAgentLoggingEventHandler', () => {
let sessionRepository: SessionRepositoryInterface
let revokedSessionRepository: RevokedSessionRepositoryInterface
let event: UserDisabledSessionUserAgentLoggingEvent
const createHandler = () => new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository)
const createHandler = () =>
new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository, revokedSessionRepository)
beforeEach(() => {
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.clearUserAgentByUserUuid = jest.fn()
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
revokedSessionRepository.clearUserAgentByUserUuid = jest.fn()
event = {} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>
event.payload = {
userUuid: '1-2-3',
@@ -26,5 +32,6 @@ describe('UserDisabledSessionUserAgentLoggingEventHandler', () => {
await createHandler().handle(event)
expect(sessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
expect(revokedSessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
})
})
@@ -2,13 +2,18 @@ import { DomainEventHandlerInterface, UserDisabledSessionUserAgentLoggingEvent }
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
@injectable()
export class UserDisabledSessionUserAgentLoggingEventHandler implements DomainEventHandlerInterface {
constructor(@inject(TYPES.SessionRepository) private sessionRepository: SessionRepositoryInterface) {}
constructor(
@inject(TYPES.SessionRepository) private sessionRepository: SessionRepositoryInterface,
@inject(TYPES.SessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
) {}
async handle(event: UserDisabledSessionUserAgentLoggingEvent): Promise<void> {
await this.sessionRepository.clearUserAgentByUserUuid(event.payload.userUuid)
await this.revokedSessionRepository.clearUserAgentByUserUuid(event.payload.userUuid)
}
}
@@ -36,4 +36,25 @@ export class RevokedSession {
)
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
declare user: Promise<User>
@Column({
name: 'received_at',
type: 'datetime',
nullable: true,
})
declare receivedAt: Date
@Column({
name: 'user_agent',
type: 'text',
nullable: true,
})
declare userAgent: string | null
@Column({
name: 'api_version',
length: 255,
nullable: true,
})
declare apiVersion: string
}
@@ -1,3 +1,4 @@
import { Uuid } from '@standardnotes/common'
import { RevokedSession } from './RevokedSession'
export interface RevokedSessionRepositoryInterface {
@@ -5,4 +6,5 @@ export interface RevokedSessionRepositoryInterface {
findAllByUserUuid(userUuid: string): Promise<Array<RevokedSession>>
save(revokedSession: RevokedSession): Promise<RevokedSession>
remove(revokedSession: RevokedSession): Promise<RevokedSession>
clearUserAgentByUserUuid(userUuid: Uuid): Promise<void>
}
@@ -2,6 +2,7 @@ import 'reflect-metadata'
import * as winston from 'winston'
import { TimerInterface } from '@standardnotes/time'
import { ApiVersion } from '../Api/ApiVersion'
import { Session } from './Session'
import { SessionRepositoryInterface } from './SessionRepositoryInterface'
import { SessionService } from './SessionService'
@@ -47,6 +48,7 @@ describe('SessionService', () => {
session.uuid = '2e1e43'
session.userUuid = '1-2-3'
session.userAgent = 'Chrome'
session.apiVersion = ApiVersion.v20200115
session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
@@ -80,6 +82,7 @@ describe('SessionService', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMilliseconds = jest.fn().mockReturnValue(123)
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
deviceDetector = {} as jest.Mocked<UAParser>
deviceDetector.setUA = jest.fn().mockReturnThis()
@@ -111,6 +114,7 @@ describe('SessionService', () => {
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
uuid: '2e1e43',
received: true,
receivedAt: new Date(1),
})
})
@@ -476,6 +480,8 @@ describe('SessionService', () => {
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
uuid: '2e1e43',
userUuid: '1-2-3',
userAgent: 'Chrome',
apiVersion: '20200115',
createdAt: expect.any(Date),
})
})
@@ -203,6 +203,7 @@ export class SessionService implements SessionServiceInterface {
async markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession> {
revokedSession.received = true
revokedSession.receivedAt = this.timer.getUTCDate()
return this.revokedSessionRepository.save(revokedSession)
}
@@ -224,7 +225,9 @@ export class SessionService implements SessionServiceInterface {
const revokedSession = new RevokedSession()
revokedSession.uuid = session.uuid
revokedSession.userUuid = session.userUuid
revokedSession.createdAt = dayjs.utc().toDate()
revokedSession.createdAt = this.timer.getUTCDate()
revokedSession.apiVersion = session.apiVersion
revokedSession.userAgent = session.userAgent
return this.revokedSessionRepository.save(revokedSession)
}
@@ -246,8 +249,8 @@ export class SessionService implements SessionServiceInterface {
}
session.userUuid = dto.user.uuid
session.apiVersion = dto.apiVersion
session.createdAt = dayjs.utc().toDate()
session.updatedAt = dayjs.utc().toDate()
session.createdAt = this.timer.getUTCDate()
session.updatedAt = this.timer.getUTCDate()
session.readonlyAccess = dto.readonlyAccess
return session
@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
import { RevokedSession } from '../../Domain/Session/RevokedSession'
@@ -9,12 +9,14 @@ import { MySQLRevokedSessionRepository } from './MySQLRevokedSessionRepository'
describe('MySQLRevokedSessionRepository', () => {
let ormRepository: Repository<RevokedSession>
let queryBuilder: SelectQueryBuilder<RevokedSession>
let updateQueryBuilder: UpdateQueryBuilder<RevokedSession>
let session: RevokedSession
const createRepository = () => new MySQLRevokedSessionRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<RevokedSession>>
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<RevokedSession>>
session = {} as jest.Mocked<RevokedSession>
@@ -36,6 +38,24 @@ describe('MySQLRevokedSessionRepository', () => {
expect(ormRepository.remove).toHaveBeenCalledWith(session)
})
it('should clear user agent data on all user sessions', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().clearUserAgentByUserUuid('1-2-3')
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
userAgent: null,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should find one session by id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(session)
@@ -20,6 +20,17 @@ export class MySQLRevokedSessionRepository implements RevokedSessionRepositoryIn
return this.ormRepository.remove(revokedSession)
}
async clearUserAgentByUserUuid(userUuid: string): Promise<void> {
await this.ormRepository
.createQueryBuilder('revoked_session')
.update()
.set({
userAgent: null,
})
.where('user_uuid = :userUuid', { userUuid })
.execute()
}
async findAllByUserUuid(userUuid: string): Promise<RevokedSession[]> {
return this.ormRepository
.createQueryBuilder('revoked_session')