Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ export class RevokedUserAccess {
/**
* @param {{
* revokedAllTimeStamp?: number
* revokedSessionTimeStamps?: Record<string, number | undefined>
* revokedSessionIds?: string[]
* }} param
*/
constructor({ revokedAllTimeStamp, revokedSessionTimeStamps }) {
constructor({ revokedAllTimeStamp, revokedSessionIds }) {
this.revokedAllTimeStamp = revokedAllTimeStamp;
this.revokedSessionTimeStamps = revokedSessionTimeStamps;
this.revokedSessionIds = revokedSessionIds;
}

isAccessTokenRevoked(decodedToken) {
Expand All @@ -17,7 +17,7 @@ export class RevokedUserAccess {
}

const sessionId = decodedToken.sid;
if (this.revokedSessionTimeStamps?.[sessionId] && issuedAt < this.revokedSessionTimeStamps[sessionId]) {
if (this.revokedSessionIds?.includes(sessionId)) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,47 @@ import { featureToggles } from '../../../shared/infrastructure/feature-toggles/i
import { RevokedUserAccess } from '../../domain/models/RevokedUserAccess.js';

const revokedUserAccessTemporaryStorage = temporaryStorage.withPrefix('revoked-user-access:');
const revokedUserAccessLifespanMs = config.authentication.revokedUserAccessLifespanMs;
const { revokedUserAccessLifespanMs } = config.authentication;

const isSessionLogoutEnabled = featureToggles.use('isSessionLogoutEnabled');

/**
* Saves the revoke date for all the accesses of a user.
* Saves the revoke date for a user session.
*
* @param {Object} params - The params object.
* @param {string} params.userId - The ID of the user to revoke access for.
* @param {Date} params.revokeUntil - The date until the user's access should be revoked.
* @param {string} params.sessionId - The ID of the user’s session to revoke.
*/
async function revokeAll({ userId, revokeUntil }) {
async function revokeSession({ userId, sessionId }) {
Joi.assert(userId, Joi.required());
Joi.assert(revokeUntil, Joi.date().required());

await revokedUserAccessTemporaryStorage.save({
key: userId,
value: Math.floor(revokeUntil.getTime() / 1000),
expirationDelaySeconds: revokedUserAccessLifespanMs / 1000,
});
Joi.assert(sessionId, Joi.required());

await revokedUserAccessTemporaryStorage.save({
key: `${userId}:all`,
value: Math.floor(revokeUntil.getTime() / 1000),
key: `${userId}:${sessionId}`,
value: '',
expirationDelaySeconds: revokedUserAccessLifespanMs / 1000,
});
}

/**
* Saves the revoke date for a user session.
* Saves the revoke date for all the accesses of a user.
*
* @param {Object} params - The params object.
* @param {string} params.userId - The ID of the user to revoke access for.
* @param {string} params.sessionId - The ID of the user’s session to revoke.
* @param {Date} params.revokeUntil - The date until the user's access should be revoked.
*/
async function revokeSession({ userId, sessionId, revokeUntil }) {
async function revokeAll({ userId, revokeUntil }) {
Joi.assert(userId, Joi.required());
Joi.assert(sessionId, Joi.required());
Joi.assert(revokeUntil, Joi.date().required());

await revokedUserAccessTemporaryStorage.save({
key: `${userId}:${sessionId}`,
key: userId,
value: Math.floor(revokeUntil.getTime() / 1000),
expirationDelaySeconds: revokedUserAccessLifespanMs / 1000,
});

await revokedUserAccessTemporaryStorage.save({
key: `${userId}:all`,
value: Math.floor(revokeUntil.getTime() / 1000),
expirationDelaySeconds: revokedUserAccessLifespanMs / 1000,
});
Expand All @@ -66,17 +64,17 @@ async function findByUserId(userId) {
return new RevokedUserAccess({ revokedAllTimeStamp });
}

const revokeKeys = await revokedUserAccessTemporaryStorage.keys(`${userId}:*`);
const revokedKeys = await revokedUserAccessTemporaryStorage.keys(`${userId}:*`);

const revokedAllKey = `${userId}:all`;

const revokedTimeStamps = Object.fromEntries(
await Promise.all(
revokeKeys.map(async (key) => [key.split(':')[1], await revokedUserAccessTemporaryStorage.get(key)]),
),
);
const revokedAllTimeStamp = revokedKeys.includes(revokedAllKey)
? await revokedUserAccessTemporaryStorage.get(`${userId}:all`)
: undefined;

const { all: revokedAllTimeStamp, ...revokedSessionTimeStamps } = revokedTimeStamps;
const revokedSessionIds = revokedKeys.filter((key) => key !== revokedAllKey).map((key) => key.split(':')[1]);

return new RevokedUserAccess({ revokedAllTimeStamp, revokedSessionTimeStamps });
return new RevokedUserAccess({ revokedAllTimeStamp, revokedSessionIds });
}

export const revokedUserAccessRepository = { revokeAll, revokeSession, findByUserId };
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,13 @@ export class FeatureTogglesClient {
this.#currentValues[key] = newValue;

this.#eventTarget.dispatchEvent(new FeatureTogglesEvent('set', key, newValue, oldValue));

break;
}

break;
}

default: {
logger.warn({ type: message.type }, 'unknown message type');
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import lodash from 'lodash';

const { trim, noop } = lodash;

import { KeyValueStorage } from './KeyValueStorage.js';

class InMemoryKeyValueStorage extends KeyValueStorage {
#store = new Map();
#store;

constructor() {
constructor({ store = new Map() } = {}) {
super();
this.#store = store;
}

async save({ key, value, expirationDelaySeconds }) {
const storageKey = trim(key) || InMemoryKeyValueStorage.generateKey();
if (expirationDelaySeconds) {
setTimeout(() => this.#store.delete(storageKey), expirationDelaySeconds * 1000);
}
Comment thread
nlepage marked this conversation as resolved.
async save({ key, value }) {
const storageKey = key?.trim() ?? InMemoryKeyValueStorage.generateKey();
this.#store.set(storageKey, value);
return storageKey;
}

async update(key, value) {
const storageKey = trim(key);
const storageKey = key.trim();
this.#store.set(storageKey, value);
}

Expand Down Expand Up @@ -50,11 +44,11 @@ class InMemoryKeyValueStorage extends KeyValueStorage {
}

quit() {
noop;
// noop
}

async expire() {
noop;
// noop
}

async ttl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class KeyValueStorage {
},

ttl(key) {
return storage.ttl(key);
return storage.ttl(prefix + key);
},

lpush({ key, value }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,8 @@ describe('Acceptance | Identity Access Management | Route | Token', function ()
// then
expect(response.statusCode).to.equal(204);

const revokeSessionTimestamp = await revokedUserAccessTemporaryStorage.get(`${userId}:${sessionId}`);
expect(revokeSessionTimestamp).to.be.a('number');
const revokedKeys = await revokedUserAccessTemporaryStorage.keys(`${userId}:*`);
expect(revokedKeys).to.deep.equal([`${userId}:${sessionId}`]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Integration | Identity Access Management | Domain | UseCase | revoke-s
await usecases.revokeSession({ userId, sessionId });

// then
const revokeTimeStamp = await revokedUserAccessTemporaryStorage.get(`${userId}:${sessionId}`);
expect(revokeTimeStamp).to.be.a('number');
const revokedKeys = await revokedUserAccessTemporaryStorage.keys(`${userId}:*`);
expect(revokedKeys).to.deep.equal([`${userId}:${sessionId}`]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { setImmediate } from 'node:timers/promises';

import { expect } from 'chai';

import { config } from '../../../../../config/config.js';
import { RevokedUserAccess } from '../../../../../src/identity-access-management/domain/models/RevokedUserAccess.js';
import { revokedUserAccessRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js';
import { featureToggles } from '../../../../../src/shared/infrastructure/feature-toggles/index.js';
Expand Down Expand Up @@ -35,15 +36,16 @@ describe('Integration | Identity Access Management | Infrastructure | Repository
describe('#revokeSession', function () {
it('saves revoked access for user session in TemporaryStorage', async function () {
// given
const revokeUntil = new Date();
const revokedTimeStamp = Math.floor(revokeUntil.getTime() / 1000);
const sessionId = crypto.randomUUID();

// when
await revokedUserAccessRepository.revokeSession({ userId: 12345, sessionId: 67890, revokeUntil });
await revokedUserAccessRepository.revokeSession({ userId: 12345, sessionId });

// then
const result = await revokedUserAccessTemporaryStorage.get('12345:67890');
expect(result).to.equal(revokedTimeStamp);
const result = await revokedUserAccessTemporaryStorage.get(`12345:${sessionId}`);
expect(result).to.equal('');
const ttl = await revokedUserAccessTemporaryStorage.ttl(`12345:${sessionId}`);
expect(ttl).to.equal(config.authentication.revokedUserAccessLifespanMs / 1000);
});
});

Expand All @@ -59,7 +61,7 @@ describe('Integration | Identity Access Management | Infrastructure | Repository
// then
expect(result).to.deep.equal({
revokedAllTimeStamp,
revokedSessionTimeStamps: undefined,
revokedSessionIds: undefined,
});
expect(result).to.be.instanceOf(RevokedUserAccess);
});
Expand All @@ -75,22 +77,17 @@ describe('Integration | Identity Access Management | Infrastructure | Repository
const revokedAllTimeStamp = Math.floor(new Date('2026-08-27T15:00:50Z').getTime() / 1000);
await revokedUserAccessTemporaryStorage.save({ key: '12345:all', value: revokedAllTimeStamp });

const session1RevokedTimestamp = Math.floor(new Date().getTime('2026-08-27T16:00:50Z') / 1000);
await revokedUserAccessTemporaryStorage.save({ key: '12345:session1', value: session1RevokedTimestamp });

const session2RevokedTimestamp = Math.floor(new Date().getTime('2026-08-27T17:00:50Z') / 1000);
await revokedUserAccessTemporaryStorage.save({ key: '12345:session2', value: session2RevokedTimestamp });
await revokedUserAccessTemporaryStorage.save({ key: '12345:session1', value: '' });
await revokedUserAccessTemporaryStorage.save({ key: '12345:session2', value: '' });

// when
const result = await revokedUserAccessRepository.findByUserId(12345);

// then
result.revokedSessionIds.sort();
expect(result).to.deep.equal({
revokedAllTimeStamp,
revokedSessionTimeStamps: {
session1: session1RevokedTimestamp,
session2: session2RevokedTimestamp,
},
revokedSessionIds: ['session1', 'session2'],
});
expect(result).to.be.instanceOf(RevokedUserAccess);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ describe('Unit | Identity Access Management | Domain | Model | RevokedUserAccess
it('builds a revoke user access model', function () {
//when
const revokedAllTimeStamp = Math.floor(new Date().getTime() / 1000);
const revokedSessionTimeStamps = { 12345: Math.floor(new Date().getTime() / 1000) };
const revokedUserAccess = new RevokedUserAccess({ revokedAllTimeStamp, revokedSessionTimeStamps });
const revokedSessionIds = [crypto.randomUUID()];
const revokedUserAccess = new RevokedUserAccess({ revokedAllTimeStamp, revokedSessionIds });

//then
expect(revokedUserAccess.revokedAllTimeStamp).to.equal(revokedAllTimeStamp);
expect(revokedUserAccess.revokedSessionTimeStamps).to.equal(revokedSessionTimeStamps);
expect(revokedUserAccess.revokedSessionIds).to.equal(revokedSessionIds);
});
});

Expand Down Expand Up @@ -52,11 +52,9 @@ describe('Unit | Identity Access Management | Domain | Model | RevokedUserAccess
context("when access token's session is revoked", function () {
it('returns true', function () {
//given
const revokedAllTimeStamp = Math.floor(new Date('2024-12-01').getTime() / 1000);
const iat = Math.floor(new Date('2024-11-01').getTime() / 1000);
const sid = crypto.randomUUID();
const decodedToken = { iat, sid };
const revokedUserAccess = new RevokedUserAccess({ revokedSessionTimeStamps: { [sid]: revokedAllTimeStamp } });
const decodedToken = { sid };
const revokedUserAccess = new RevokedUserAccess({ revokedSessionIds: [sid] });

//when
const result = revokedUserAccess.isAccessTokenRevoked(decodedToken);
Expand All @@ -69,11 +67,9 @@ describe('Unit | Identity Access Management | Domain | Model | RevokedUserAccess
context("when access token's session is not revoked", function () {
it('returns false', function () {
//given
const revokedAllTimeStamp = Math.floor(new Date('2024-10-01').getTime() / 1000);
const iat = Math.floor(new Date('2024-12-01').getTime() / 1000);
const sid = crypto.randomUUID();
const decodedToken = { iat, sid };
const revokedUserAccess = new RevokedUserAccess({ revokedSessionTimeStamps: { [sid]: revokedAllTimeStamp } });
const decodedToken = { sid };
const revokedUserAccess = new RevokedUserAccess({ revokedSessionIds: [crypto.randomUUID()] });

//when
const result = revokedUserAccess.isAccessTokenRevoked(decodedToken);
Expand Down
Loading
Loading