Skip to content
Draft
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
50 changes: 50 additions & 0 deletions src/utils/revocation-list/check-claims.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const queryOperators = {
eq: (a, b) => a === b,
ne: (a, b) => a !== b,
gt: (a, b) => a > b,
gte: (a, b) => a >= b,
lt: (a, b) => a < b,
lte: (a, b) => a <= b,
regex: (a, b) => {
if (typeof b !== 'string' && !(b instanceof RegExp)) {
return true;
}
const valueAsRegEx = new RegExp(b);

return valueAsRegEx.test(String(a));
},
sw: (a, b) => {
if (typeof b !== 'string') {
return true;
}
return String(a).startsWith(b);
},
};

const queryOperatorsKeys = Object.keys(queryOperators);

/**
* Check rule claims with decoded token claims by comparison query operators
* @param {Object} ruleClaim
* @param {string} ruleClaim.query
* @param {string | number | RegExp} ruleClaim.value
* @param {*} decodedClaimValue
* @returns {boolean}
*/
const checkClaimValueByRule = (ruleClaim, decodedClaimValue) => {
if (ruleClaim === undefined) {
return true;
}
const { query, value } = ruleClaim;
const comparisonFunc = queryOperators[query];

if (comparisonFunc === undefined) {
return true;
}
return comparisonFunc(decodedClaimValue, value);
};

module.exports = {
queryOperatorsKeys,
checkClaimValueByRule,
};
171 changes: 171 additions & 0 deletions src/utils/revocation-list/revoke-token-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const uuidv4 = require('uuid').v4;
const sortStoreList = require('./sort-list');
const { queryOperatorsKeys, checkClaimValueByRule } = require('./check-claims');

/*
store
{
"c68892eb-2fdf-45e5-9d0d-6e4592640eca": {
"expAt": 1578009600000,
"type": "all",
"claims": {
"userId": {
"query": "eq",
"value": "sometestuserid",
},
}
},
"b637ce58-8d55-44f4-a07d-9d77ad2043f8": {
"expAt": 1578009600000,
"type": "refresh",
"claims": {
"userId": {
"query": "eq",
"value": "someuserid",
},
"iat": {
"query": "gte",
"value": 1578009600000, // '03-01-2020'
},
}
},
"30106816-99a4-499a-bced-c1b91717efa9": {
"expAt": 1578009600000,
"type": "access",
"claims": {
"iat": {
"query": "gte",
"value": 1669939200000, // '2022-12-02'
}
},
},
}
*/
/*
rule 1:
{
"type": "refresh",
"claims": {
"userId": {
"eq": "someuserid",
},
"iat": {
"gte": 1578009600000,
},
}
}
rule 2:
{
"type": "access",
"claims": {
"iat": {
"gte": 1669939200000, // '2022-12-02'
}
}
}
rule 3:
{
"type": "all",
"claims": {
"userId": {
"eq": "sometestuserid",
},
}
}
*/

// move to global config
const tokenexpAt = {
refresh: 365 * 24 * 60 * 60 * 1000, // 365 days in ms
access: 1 * 60 * 60 * 1000, // 1 hour in ms
};

// const TOKEN_TYPES = ['refresh', 'access'];

class RevokeTokenApi {
constructor() {
this.store = {};
}

addRule(rule) {
const { type, claims: addingClaims } = rule;

const id = uuidv4();
// rule expiration time
const expAt = Date.now() + (tokenexpAt[type] || tokenexpAt.refresh);

const claims = {};
Object.entries(addingClaims).forEach(([tokenClaim, queryValue]) => {
const claimQueryOptions = Object.entries(queryValue);

const [query, value] = claimQueryOptions
.find(([queryKey]) => queryOperatorsKeys.includes(queryKey));

claims[tokenClaim] = {
query,
value,
};
});

const newStoreRule = {
[id]: {
id,
type,
expAt,
claims,
},
};

this.store = sortStoreList({ ...this.store, ...newStoreRule });

// sync rules with other nodes

return id;
}

/**
* Remove rule from store by rule id
* @param {string} id Rule ID
*/
removeRule(id) {
const { [id]: ruleByID, ...restRules } = this.store;

this.store = restRules;

// or
// delete this.store[id];

// sync rules with other nodes
}

isRevoked(decodedData) {
const ruleList = Object.entries(this.store);
const decodedDataKeys = Object.keys(decodedData);

const isRevokedByRule = ruleList.some(([ruleID, rule]) => {
const { type, expAt, claims } = rule;

// remove rule if rule expired
if (Date.now() > expAt) {
this.removeRule(ruleID);
return false;
}

if (type !== decodedData.type && type !== 'all') {
return false;
}
return decodedDataKeys.every((dataClaim) => {
const ruleClaimValue = claims[dataClaim];
const claimValue = decodedData[dataClaim];

return checkClaimValueByRule(ruleClaimValue, claimValue);
});
});

return isRevokedByRule;
}
}

module.exports = {
RevokeTokenApi,
};
21 changes: 21 additions & 0 deletions src/utils/revocation-list/sort-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* sorting rules for fast find revoked token in rule list
* @param {Object} store
*/
const sortStoreList = (store) => {
// more logic for sorting?
const sortedList = Object.entries(store)
.sort((ruleA, ruleB) => {
const [, rulePropsA] = ruleA;
const [, rulePropsB] = ruleB;

const ruleClaimsSizeA = Object.keys(rulePropsA.claims).length;
const ruleClaimsSizeB = Object.keys(rulePropsB.claims).length;

return ruleClaimsSizeA - ruleClaimsSizeB;
});

return Object.fromEntries(sortedList);
};

module.exports = sortStoreList;
144 changes: 144 additions & 0 deletions test/suites/utils/revocation-list/revoke-token-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const assert = require('assert');

const { RevokeTokenApi } = require('../../../../src/utils/revocation-list/revoke-token-api');

describe('#revoke-token-api', function suite() {
it('add rule', () => {
const revokeApi = new RevokeTokenApi();

const rule = {
type: 'refresh',
claims: {
userId: {
eq: 'testuserid',
},
exp: {
lte: 1578009600000, // '03-01-2020'
},
},
};

const id = revokeApi.addRule(rule);

assert.ok(typeof id === 'string');
assert.ok(revokeApi.store[id]);
assert.deepStrictEqual(revokeApi.store[id], {
id,
type: 'refresh',
expAt: revokeApi.store[id].expAt,
claims: {
userId: {
query: 'eq',
value: 'testuserid',
},
exp: {
query: 'lte',
value: 1578009600000, // '03-01-2020'
},
},
});
});

it('remove rule', () => {
const revokeApi = new RevokeTokenApi();

const rule = {
type: 'all',
claims: {
userId: {
regex: /.*/,
},
},
};

const id = revokeApi.addRule(rule);
assert.ok(revokeApi.store[id]);

revokeApi.removeRule(id);
assert.ok(revokeApi.store[id] === undefined);
assert.deepStrictEqual(revokeApi.store, {});
});

describe('check token by rules', () => {
it('should token revoked', () => {
const revokeApi = new RevokeTokenApi();

const rule = {
type: 'access',
claims: {
userId: {
eq: 'testuserid',
},
iat: {
gte: 1578009600000, // 03.01.2020
},
},
};
const rule2 = {
type: 'refresh',
claims: {
exp: {
lte: 1669939200000, // 02.12.2022
},
},
};
const decodedData = {
userId: 'someuserid',
type: 'refresh',
iat: 1576926000000, // 21.12.2019
exp: 1618939200000, // 20.04.2021
};

revokeApi.addRule(rule);
revokeApi.addRule(rule2);

const isTokenRevoked = revokeApi.isRevoked(decodedData);
assert.ok(isTokenRevoked);
});

it('should token not revoked', () => {
const revokeApi = new RevokeTokenApi();

const rule = {
type: 'access',
claims: {
userId: {
eq: 'testuserid',
},
iat: {
gte: 1578009600000, // 03.01.2020
},
},
};
const rule2 = {
type: 'refresh',
claims: {
exp: {
lte: 1669939200000, // 02.12.2022
},
},
};
const rule3 = {
type: 'all',
claims: {
exp: {
lte: 1558939200000, // 27.05.2019
},
},
};
const decodedTokenData = {
userId: 'someuserid',
type: 'access',
iat: 1618790400000, // 19.04.2021
exp: 1618939200000, // 20.04.2021
};

revokeApi.addRule(rule);
revokeApi.addRule(rule2);
revokeApi.addRule(rule3);

const isTokenRevoked = revokeApi.isRevoked(decodedTokenData);
assert.strictEqual(isTokenRevoked, false);
});
});
});