mirror of
https://github.com/standardnotes/server
synced 2026-09-13 00:45:26 -04:00
fix: Fixes ephemeral session revocation
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import * as IORedis from 'ioredis'
|
||||
|
||||
import { EphemeralSession } from '../../Domain/Session/EphemeralSession'
|
||||
import { RedisEphemeralSessionRepository } from './RedisEphemeralSessionRepository'
|
||||
|
||||
describe('RedisEphemeralSessionRepository', () => {
|
||||
let redisClient: jest.Mocked<IORedis.Redis>
|
||||
let pipeline: {
|
||||
del: jest.Mock
|
||||
srem: jest.Mock
|
||||
exec: jest.Mock
|
||||
}
|
||||
let repository: RedisEphemeralSessionRepository
|
||||
|
||||
beforeEach(() => {
|
||||
pipeline = {
|
||||
del: jest.fn().mockReturnThis(),
|
||||
srem: jest.fn().mockReturnThis(),
|
||||
exec: jest.fn().mockResolvedValue([]),
|
||||
}
|
||||
|
||||
redisClient = {
|
||||
get: jest.fn(),
|
||||
pipeline: jest.fn().mockReturnValue(pipeline),
|
||||
} as unknown as jest.Mocked<IORedis.Redis>
|
||||
|
||||
repository = new RedisEphemeralSessionRepository(redisClient, 3600)
|
||||
})
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete private identifier mapping when revoking a session', async () => {
|
||||
const session = {
|
||||
uuid: 'session-uuid',
|
||||
userUuid: 'user-uuid',
|
||||
privateIdentifier: 'private-id',
|
||||
} as EphemeralSession
|
||||
|
||||
redisClient.get.mockResolvedValue(JSON.stringify(session))
|
||||
|
||||
await repository.deleteOne('session-uuid', 'user-uuid')
|
||||
|
||||
expect(redisClient.get).toHaveBeenCalledWith('session:session-uuid:user-uuid')
|
||||
expect(pipeline.del).toHaveBeenCalledWith('session:session-uuid')
|
||||
expect(pipeline.del).toHaveBeenCalledWith('session:session-uuid:user-uuid')
|
||||
expect(pipeline.del).toHaveBeenCalledWith('session-private-id:private-id')
|
||||
expect(pipeline.srem).toHaveBeenCalledWith('user-sessions:user-uuid', 'session-uuid')
|
||||
expect(pipeline.exec).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should skip private identifier deletion when session is not found', async () => {
|
||||
redisClient.get.mockResolvedValue(null)
|
||||
|
||||
await repository.deleteOne('session-uuid', 'user-uuid')
|
||||
|
||||
expect(pipeline.del).toHaveBeenCalledWith('session:session-uuid')
|
||||
expect(pipeline.del).toHaveBeenCalledWith('session:session-uuid:user-uuid')
|
||||
expect(pipeline.del).not.toHaveBeenCalledWith(expect.stringContaining('session-private-id'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -26,12 +26,18 @@ export class RedisEphemeralSessionRepository implements EphemeralSessionReposito
|
||||
}
|
||||
|
||||
async deleteOne(uuid: string, userUuid: string): Promise<void> {
|
||||
const session = await this.findOneByUuidAndUserUuid(uuid, userUuid)
|
||||
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
|
||||
pipeline.del(`${this.PREFIX}:${uuid}`)
|
||||
pipeline.del(`${this.PREFIX}:${uuid}:${userUuid}`)
|
||||
pipeline.srem(`${this.USER_SESSIONS_PREFIX}:${userUuid}`, uuid)
|
||||
|
||||
if (session?.privateIdentifier) {
|
||||
pipeline.del(`${this.PREFIX_PRIVATE_ID}:${session.privateIdentifier}`)
|
||||
}
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { CacheEntry, CacheEntryRepositoryInterface } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { EphemeralSession } from '../../Domain/Session/EphemeralSession'
|
||||
import { TypeORMEphemeralSessionRepository } from './TypeORMEphemeralSessionRepository'
|
||||
|
||||
describe('TypeORMEphemeralSessionRepository', () => {
|
||||
let cacheEntryRepository: jest.Mocked<CacheEntryRepositoryInterface>
|
||||
let timer: jest.Mocked<TimerInterface>
|
||||
let repository: TypeORMEphemeralSessionRepository
|
||||
|
||||
beforeEach(() => {
|
||||
cacheEntryRepository = {
|
||||
findUnexpiredOneByKey: jest.fn(),
|
||||
removeByKey: jest.fn(),
|
||||
save: jest.fn(),
|
||||
} as unknown as jest.Mocked<CacheEntryRepositoryInterface>
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
|
||||
repository = new TypeORMEphemeralSessionRepository(cacheEntryRepository, 3600, timer)
|
||||
})
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete private identifier mapping when revoking a session', async () => {
|
||||
const session = {
|
||||
uuid: 'session-uuid',
|
||||
userUuid: 'user-uuid',
|
||||
privateIdentifier: 'private-id',
|
||||
} as EphemeralSession
|
||||
|
||||
cacheEntryRepository.findUnexpiredOneByKey.mockResolvedValueOnce(
|
||||
CacheEntry.create({
|
||||
key: 'session:session-uuid:user-uuid',
|
||||
value: JSON.stringify(session),
|
||||
expiresAt: new Date(),
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
await repository.deleteOne('session-uuid', 'user-uuid')
|
||||
|
||||
expect(cacheEntryRepository.removeByKey).toHaveBeenCalledWith('session:session-uuid')
|
||||
expect(cacheEntryRepository.removeByKey).toHaveBeenCalledWith('session:session-uuid:user-uuid')
|
||||
expect(cacheEntryRepository.removeByKey).toHaveBeenCalledWith('session-private-id:private-id')
|
||||
})
|
||||
|
||||
it('should skip private identifier deletion when session is not found', async () => {
|
||||
cacheEntryRepository.findUnexpiredOneByKey.mockResolvedValueOnce(null)
|
||||
|
||||
await repository.deleteOne('session-uuid', 'user-uuid')
|
||||
|
||||
expect(cacheEntryRepository.removeByKey).toHaveBeenCalledWith('session:session-uuid')
|
||||
expect(cacheEntryRepository.removeByKey).toHaveBeenCalledWith('session:session-uuid:user-uuid')
|
||||
expect(cacheEntryRepository.removeByKey).not.toHaveBeenCalledWith(expect.stringContaining('session-private-id'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -27,9 +27,15 @@ export class TypeORMEphemeralSessionRepository implements EphemeralSessionReposi
|
||||
}
|
||||
|
||||
async deleteOne(uuid: string, userUuid: string): Promise<void> {
|
||||
const session = await this.findOneByUuidAndUserUuid(uuid, userUuid)
|
||||
|
||||
await this.cacheEntryRepository.removeByKey(`${this.PREFIX}:${uuid}`)
|
||||
await this.cacheEntryRepository.removeByKey(`${this.PREFIX}:${uuid}:${userUuid}`)
|
||||
|
||||
if (session?.privateIdentifier) {
|
||||
await this.cacheEntryRepository.removeByKey(`${this.PREFIX_PRIVATE_ID}:${session.privateIdentifier}`)
|
||||
}
|
||||
|
||||
const userSessionsJSON = await this.cacheEntryRepository.findUnexpiredOneByKey(
|
||||
`${this.USER_SESSIONS_PREFIX}:${userUuid}`,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user