blacklist with automatic garbage collector
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
31f739d4b8
commit
64d4f00629
@ -6,13 +6,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
create_salt,
|
|
||||||
sign_object,
|
sign_object,
|
||||||
verify_signature_get_info
|
verify_signature_get_info
|
||||||
} from '@sapphirecode/crypto-helper';
|
} from '@sapphirecode/crypto-helper';
|
||||||
import keystore from './KeyStore';
|
import keystore from './KeyStore';
|
||||||
import blacklist from './Blacklist';
|
import blacklist from './Blacklist';
|
||||||
import { debug } from './debug';
|
import { debug } from './debug';
|
||||||
|
import { generate_token_id } from './token_id';
|
||||||
|
|
||||||
const logger = debug ('authority');
|
const logger = debug ('authority');
|
||||||
|
|
||||||
@ -102,13 +102,15 @@ class Authority {
|
|||||||
const log = logger.extend ('sign');
|
const log = logger.extend ('sign');
|
||||||
log ('signing new %s', type);
|
log ('signing new %s', type);
|
||||||
const time = Date.now ();
|
const time = Date.now ();
|
||||||
|
const valid_until = time + (valid_for * 1e3);
|
||||||
const key = await keystore.get_sign_key (time / 1000, valid_for);
|
const key = await keystore.get_sign_key (time / 1000, valid_for);
|
||||||
const attributes = {
|
const attributes = {
|
||||||
id: create_salt (),
|
id: generate_token_id (new Date (valid_until)),
|
||||||
iat: time,
|
iat: time,
|
||||||
iss: keystore.instance_id,
|
iss: keystore.instance_id,
|
||||||
type,
|
type,
|
||||||
valid_for,
|
valid_for,
|
||||||
|
valid_until,
|
||||||
next_module: options?.next_module
|
next_module: options?.next_module
|
||||||
};
|
};
|
||||||
const signature = sign_object (options?.data, key, attributes);
|
const signature = sign_object (options?.data, key, attributes);
|
||||||
|
@ -7,19 +7,31 @@
|
|||||||
|
|
||||||
import { debug } from './debug';
|
import { debug } from './debug';
|
||||||
import { redis_blacklist_store } from './RedisData/RedisBlacklistStore';
|
import { redis_blacklist_store } from './RedisData/RedisBlacklistStore';
|
||||||
|
import { parse_token_id } from './token_id';
|
||||||
|
|
||||||
const logger = debug ('blacklist');
|
const logger = debug ('blacklist');
|
||||||
|
|
||||||
interface Signature {
|
interface Signature {
|
||||||
hash: string;
|
token_id: string;
|
||||||
|
iat: number;
|
||||||
|
valid_until: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExportedSignature {
|
||||||
|
token_id: string;
|
||||||
iat: number;
|
iat: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Blacklist {
|
class Blacklist {
|
||||||
private _signatures: Signature[];
|
private _signatures: Signature[];
|
||||||
|
private _interval: NodeJS.Timeout;
|
||||||
|
|
||||||
public constructor () {
|
public constructor () {
|
||||||
this._signatures = [];
|
this._signatures = [];
|
||||||
|
this._interval = setInterval (
|
||||||
|
this.garbage_collect.bind (this),
|
||||||
|
3600000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clear (
|
public async clear (
|
||||||
@ -34,10 +46,15 @@ class Blacklist {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async add_signature (hash: string): Promise<void> {
|
public async add_signature (token_id: string): Promise<void> {
|
||||||
logger.extend ('add_signature') ('blacklisting signature %s', hash);
|
logger.extend ('add_signature') ('blacklisting signature %s', token_id);
|
||||||
this._signatures.push ({ iat: Date.now (), hash });
|
const parsed = parse_token_id (token_id);
|
||||||
await redis_blacklist_store.add (hash);
|
this._signatures.push ({
|
||||||
|
iat: Date.now (),
|
||||||
|
token_id,
|
||||||
|
valid_until: parsed.valid_until
|
||||||
|
});
|
||||||
|
await redis_blacklist_store.add (token_id, parsed.valid_until);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove_signature (signature: number | string): Promise<void> {
|
public async remove_signature (signature: number | string): Promise<void> {
|
||||||
@ -48,13 +65,20 @@ class Blacklist {
|
|||||||
log ('received string, searching through signatures');
|
log ('received string, searching through signatures');
|
||||||
key = signature;
|
key = signature;
|
||||||
for (let i = this._signatures.length - 1; i >= 0; i--) {
|
for (let i = this._signatures.length - 1; i >= 0; i--) {
|
||||||
if (this._signatures[i].hash === signature)
|
if (this._signatures[i].token_id === signature) {
|
||||||
|
log ('removing sigature %s at %d', signature, i);
|
||||||
this._signatures.splice (i, 1);
|
this._signatures.splice (i, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log ('received index, removing at index');
|
log (
|
||||||
key = this._signatures[signature].hash;
|
'received index, removing signature %s at index %s',
|
||||||
|
this._signatures[signature].token_id,
|
||||||
|
signature
|
||||||
|
);
|
||||||
|
|
||||||
|
key = this._signatures[signature].token_id;
|
||||||
this._signatures.splice (signature, 1);
|
this._signatures.splice (signature, 1);
|
||||||
}
|
}
|
||||||
await redis_blacklist_store.remove (key);
|
await redis_blacklist_store.remove (key);
|
||||||
@ -64,7 +88,7 @@ class Blacklist {
|
|||||||
const log = logger.extend ('is_valid');
|
const log = logger.extend ('is_valid');
|
||||||
log ('checking signature for blacklist entry %s', hash);
|
log ('checking signature for blacklist entry %s', hash);
|
||||||
for (const sig of this._signatures) {
|
for (const sig of this._signatures) {
|
||||||
if (sig.hash === hash) {
|
if (sig.token_id === hash) {
|
||||||
log ('found matching blacklist entry');
|
log ('found matching blacklist entry');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -80,22 +104,44 @@ class Blacklist {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public export_blacklist (): Signature[] {
|
public export_blacklist (): ExportedSignature[] {
|
||||||
logger.extend ('export_blacklist') ('exporting blacklist');
|
logger.extend ('export_blacklist') ('exporting blacklist');
|
||||||
return this._signatures;
|
return this._signatures.map ((v) => ({
|
||||||
|
iat: v.iat,
|
||||||
|
token_id: v.token_id
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public import_blacklist (data: Signature[]): void {
|
public import_blacklist (data: ExportedSignature[]): void {
|
||||||
logger.extend ('import_blacklist') (
|
logger.extend ('import_blacklist') (
|
||||||
'importing %d blacklist entries',
|
'importing %d blacklist entries',
|
||||||
data.length
|
data.length
|
||||||
);
|
);
|
||||||
this._signatures.push (...data);
|
for (const token of data) {
|
||||||
|
const parsed = parse_token_id (token.token_id);
|
||||||
|
this._signatures.push ({
|
||||||
|
token_id: token.token_id,
|
||||||
|
iat: token.iat,
|
||||||
|
valid_until: parsed.valid_until
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sync_redis (url: string): void {
|
public sync_redis (url: string): void {
|
||||||
redis_blacklist_store.connect (url);
|
redis_blacklist_store.connect (url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async garbage_collect (): Promise<void> {
|
||||||
|
const log = logger.extend ('garbage_collect');
|
||||||
|
const time = new Date;
|
||||||
|
log ('removing signatures expired before', time);
|
||||||
|
for (let i = this._signatures.length - 1; i >= 0; i--) {
|
||||||
|
if (this._signatures[i].valid_until < time) {
|
||||||
|
log ('signature %s expired', this._signatures[i].token_id);
|
||||||
|
await this.remove_signature (i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bl = (new Blacklist);
|
const bl = (new Blacklist);
|
||||||
|
@ -27,7 +27,7 @@ class KeyStore {
|
|||||||
public constructor () {
|
public constructor () {
|
||||||
this._interval = setInterval (() => {
|
this._interval = setInterval (() => {
|
||||||
this.garbage_collect ();
|
this.garbage_collect ();
|
||||||
}, renew_interval);
|
}, renew_interval * 1000);
|
||||||
this._instance = to_b58 (random_hex (16), 'hex');
|
this._instance = to_b58 (random_hex (16), 'hex');
|
||||||
logger.extend ('constructor') (
|
logger.extend ('constructor') (
|
||||||
'created keystore instance %s',
|
'created keystore instance %s',
|
||||||
|
@ -11,14 +11,18 @@ import { Redis } from '../Redis';
|
|||||||
const logger = debug ('RedisBlacklistStore');
|
const logger = debug ('RedisBlacklistStore');
|
||||||
|
|
||||||
export class RedisBlacklistStore extends Redis {
|
export class RedisBlacklistStore extends Redis {
|
||||||
public async add (key: string): Promise<void> {
|
public async add (key: string, valid_until: Date): Promise<void> {
|
||||||
const log = logger.extend ('set');
|
const log = logger.extend ('set');
|
||||||
log ('trying to add key %s to redis blacklist', key);
|
log ('trying to add key %s to redis blacklist', key);
|
||||||
if (!this.is_active) {
|
if (!this.is_active) {
|
||||||
log ('redis is inactive, skipping');
|
log ('redis is inactive, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.redis.sadd ('blacklist', key);
|
await this.redis.setex (
|
||||||
|
`blacklist_${key}`,
|
||||||
|
(valid_until.getTime () - Date.now ()) / 1000,
|
||||||
|
1
|
||||||
|
);
|
||||||
log ('saved key');
|
log ('saved key');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +33,7 @@ export class RedisBlacklistStore extends Redis {
|
|||||||
log ('redis is inactive, skipping');
|
log ('redis is inactive, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.redis.srem ('blacklist', key);
|
await this.redis.del (`blacklist_${key}`);
|
||||||
log ('removed key');
|
log ('removed key');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +44,7 @@ export class RedisBlacklistStore extends Redis {
|
|||||||
log ('redis is inactive, skipping');
|
log ('redis is inactive, skipping');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const res = await this.redis.sismember ('blacklist', key) === 1;
|
const res = await this.redis.exists (`blacklist_${key}`) === 1;
|
||||||
log ('found key %s', res);
|
log ('found key %s', res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
21
lib/token_id.ts
Normal file
21
lib/token_id.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { create_salt } from '@sapphirecode/crypto-helper';
|
||||||
|
import { to_b58 } from '@sapphirecode/encoding-helper';
|
||||||
|
|
||||||
|
export function generate_token_id (valid_until: Date) {
|
||||||
|
const salt = create_salt ();
|
||||||
|
return `${to_b58 (salt, 'hex')};${valid_until.toISOString ()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse_token_id (id: string) {
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
const regex = /^(?<hash>[A-HJ-NP-Za-km-z1-9]+);(?<date>\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d{3}Z)$/u;
|
||||||
|
const result = regex.exec (id);
|
||||||
|
if (result === null)
|
||||||
|
throw new Error (`invalid token id ${id}`);
|
||||||
|
if (typeof result.groups === 'undefined')
|
||||||
|
throw new Error ('invalid state');
|
||||||
|
return {
|
||||||
|
hash: result.groups.hash as string,
|
||||||
|
valid_until: new Date (result.groups.date as string)
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sapphirecode/auth-server-helper",
|
"name": "@sapphirecode/auth-server-helper",
|
||||||
"version": "4.0.0",
|
"version": "4.0.1",
|
||||||
"main": "dist/lib/index.js",
|
"main": "dist/lib/index.js",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Timo Hocker",
|
"name": "Timo Hocker",
|
||||||
|
@ -6,10 +6,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import blacklist, { Blacklist } from '../../lib/Blacklist';
|
import blacklist, { Blacklist } from '../../lib/Blacklist';
|
||||||
|
import { generate_token_id, parse_token_id } from '../../lib/token_id';
|
||||||
import { clock_finalize, clock_setup } from '../Helper';
|
import { clock_finalize, clock_setup } from '../Helper';
|
||||||
|
|
||||||
// eslint-disable-next-line max-lines-per-function
|
// eslint-disable-next-line max-lines-per-function
|
||||||
describe ('blacklist', () => {
|
describe ('blacklist', () => {
|
||||||
|
const token1 = generate_token_id (new Date (Date.now () + 3600000));
|
||||||
|
const token2 = generate_token_id (new Date (Date.now () + 3600000));
|
||||||
|
const token3 = generate_token_id (new Date (Date.now () + 3600000));
|
||||||
|
|
||||||
beforeAll (() => {
|
beforeAll (() => {
|
||||||
clock_setup ();
|
clock_setup ();
|
||||||
});
|
});
|
||||||
@ -19,77 +24,88 @@ describe ('blacklist', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it ('should validate any string', async () => {
|
it ('should validate any string', async () => {
|
||||||
expect (await blacklist.is_valid ('foo'))
|
expect (await blacklist.is_valid (token1))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
expect (await blacklist.is_valid ('bar'))
|
expect (await blacklist.is_valid (token2))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
expect (await blacklist.is_valid ('baz'))
|
expect (await blacklist.is_valid (token3))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should blacklist strings', async () => {
|
it ('should blacklist strings', async () => {
|
||||||
await blacklist.add_signature ('foo');
|
await blacklist.add_signature (token1);
|
||||||
await blacklist.add_signature ('bar');
|
await blacklist.add_signature (token2);
|
||||||
expect (await blacklist.is_valid ('foo'))
|
expect (await blacklist.is_valid (token1))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
expect (await blacklist.is_valid ('bar'))
|
expect (await blacklist.is_valid (token2))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
expect (await blacklist.is_valid ('baz'))
|
expect (await blacklist.is_valid (token3))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should remove one string', async () => {
|
it ('should remove one string', async () => {
|
||||||
await blacklist.remove_signature ('foo');
|
await blacklist.remove_signature (token1);
|
||||||
expect (await blacklist.is_valid ('foo'))
|
expect (await blacklist.is_valid (token1))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
expect (await blacklist.is_valid ('bar'))
|
expect (await blacklist.is_valid (token2))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
expect (await blacklist.is_valid ('baz'))
|
expect (await blacklist.is_valid (token3))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should clear after time', async () => {
|
it ('should clear after time', async () => {
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
.tick (5000);
|
.tick (5000);
|
||||||
await blacklist.add_signature ('baz');
|
await blacklist.add_signature (token3);
|
||||||
await blacklist.clear (Date.now () - 100);
|
await blacklist.clear (Date.now () - 100);
|
||||||
expect (await blacklist.is_valid ('foo'))
|
expect (await blacklist.is_valid (token1))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
expect (await blacklist.is_valid ('bar'))
|
expect (await blacklist.is_valid (token2))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
expect (await blacklist.is_valid ('baz'))
|
expect (await blacklist.is_valid (token3))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should clear all', async () => {
|
it ('should clear all', async () => {
|
||||||
await blacklist.add_signature ('foo');
|
await blacklist.add_signature (token1);
|
||||||
await blacklist.add_signature ('bar');
|
await blacklist.add_signature (token2);
|
||||||
await blacklist.add_signature ('baz');
|
await blacklist.add_signature (token3);
|
||||||
expect (await blacklist.is_valid ('foo'))
|
expect (await blacklist.is_valid (token1))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
expect (await blacklist.is_valid ('bar'))
|
expect (await blacklist.is_valid (token2))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
expect (await blacklist.is_valid ('baz'))
|
expect (await blacklist.is_valid (token3))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
await blacklist.clear ();
|
await blacklist.clear ();
|
||||||
expect (await blacklist.is_valid ('foo'))
|
expect (await blacklist.is_valid (token1))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
expect (await blacklist.is_valid ('bar'))
|
expect (await blacklist.is_valid (token2))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
expect (await blacklist.is_valid ('baz'))
|
expect (await blacklist.is_valid (token3))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should export and import data', async () => {
|
it ('should export and import data', async () => {
|
||||||
await blacklist.add_signature ('baz');
|
const time = new Date;
|
||||||
const exp = blacklist.export_blacklist ();
|
const token = generate_token_id (time);
|
||||||
|
await blacklist.add_signature (token);
|
||||||
// eslint-disable-next-line dot-notation
|
// eslint-disable-next-line dot-notation
|
||||||
expect (blacklist['_signatures'])
|
expect (blacklist['_signatures'])
|
||||||
.toEqual (exp);
|
.toEqual ([
|
||||||
|
{
|
||||||
|
token_id: token,
|
||||||
|
iat: time.getTime (),
|
||||||
|
valid_until: time
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const exp = blacklist.export_blacklist ();
|
||||||
|
expect (exp)
|
||||||
|
.toEqual ([ { token_id: token, iat: time.getTime () } ]);
|
||||||
const bl2 = (new Blacklist);
|
const bl2 = (new Blacklist);
|
||||||
bl2.import_blacklist (exp);
|
bl2.import_blacklist (exp);
|
||||||
// eslint-disable-next-line dot-notation
|
// eslint-disable-next-line dot-notation
|
||||||
expect (bl2['_signatures'])
|
expect (bl2['_signatures'])
|
||||||
.toEqual (exp);
|
// eslint-disable-next-line dot-notation
|
||||||
|
.toEqual (blacklist['_signatures']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -192,7 +192,7 @@ describe ('gateway', () => {
|
|||||||
expect (resp.statusCode)
|
expect (resp.statusCode)
|
||||||
.toEqual (200);
|
.toEqual (200);
|
||||||
const blacklisted = blacklist.export_blacklist ()
|
const blacklisted = blacklist.export_blacklist ()
|
||||||
.map ((v) => v.hash);
|
.map ((v) => v.token_id);
|
||||||
expect (blacklisted)
|
expect (blacklisted)
|
||||||
.toContain (token.id);
|
.toContain (token.id);
|
||||||
expect (blacklisted)
|
expect (blacklisted)
|
||||||
|
13
test/spec/token_id.ts
Normal file
13
test/spec/token_id.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { generate_token_id, parse_token_id } from '../../lib/token_id';
|
||||||
|
|
||||||
|
describe ('token_id', () => {
|
||||||
|
it ('should always generate valid tokens', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
const date = new Date;
|
||||||
|
const token_id = generate_token_id (new Date);
|
||||||
|
const parsed = parse_token_id (token_id);
|
||||||
|
expect (parsed.valid_until)
|
||||||
|
.toEqual (date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user