This commit is contained in:
parent
4c42a682d5
commit
d6a40871c4
12
README.md
12
README.md
@ -104,6 +104,18 @@ const {blacklist} = require('@sapphirecode/auth-server-helper');
|
|||||||
blacklist.add_signature(token_id); // the token id is returned from any function that creates tokens
|
blacklist.add_signature(token_id); // the token id is returned from any function that creates tokens
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Exporting and importing public keys to validate tokens across server instances
|
||||||
|
|
||||||
|
```js
|
||||||
|
const {keystore} = require('@sapphirecode/auth-server-helper');
|
||||||
|
|
||||||
|
const export = keystore.export_verification_data();
|
||||||
|
|
||||||
|
// second instance
|
||||||
|
|
||||||
|
keystore.import_verification_data(export);
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT © Timo Hocker <timo@scode.ovh>
|
MIT © Timo Hocker <timo@scode.ovh>
|
||||||
|
@ -49,7 +49,7 @@ class Authority {
|
|||||||
key,
|
key,
|
||||||
(info) => {
|
(info) => {
|
||||||
try {
|
try {
|
||||||
return keystore.get_key (info.iat / 1000);
|
return keystore.get_key (info.iat / 1000, info.iss);
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return '';
|
return '';
|
||||||
@ -89,6 +89,7 @@ class Authority {
|
|||||||
const attributes = {
|
const attributes = {
|
||||||
id: create_salt (),
|
id: create_salt (),
|
||||||
iat: time,
|
iat: time,
|
||||||
|
iss: keystore.instance_id,
|
||||||
type,
|
type,
|
||||||
valid_for,
|
valid_for,
|
||||||
next_module: options?.next_module
|
next_module: options?.next_module
|
||||||
|
@ -5,9 +5,10 @@
|
|||||||
* Created by Timo Hocker <timo@scode.ovh>, December 2020
|
* Created by Timo Hocker <timo@scode.ovh>, December 2020
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generate_keypair } from '@sapphirecode/crypto-helper';
|
import { generate_keypair, random_hex } from '@sapphirecode/crypto-helper';
|
||||||
|
import { to_b58 } from '@sapphirecode/encoding-helper';
|
||||||
|
|
||||||
const renew_interval = 60;
|
const renew_interval = 3600;
|
||||||
|
|
||||||
interface Key {
|
interface Key {
|
||||||
key: string;
|
key: string;
|
||||||
@ -21,11 +22,6 @@ interface KeyPair {
|
|||||||
|
|
||||||
type KeyStoreData = Record<string, KeyPair>;
|
type KeyStoreData = Record<string, KeyPair>;
|
||||||
|
|
||||||
function get_index (iat: number): string {
|
|
||||||
return Math.floor (iat / renew_interval)
|
|
||||||
.toFixed (0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function create_key (valid_for: number) {
|
async function create_key (valid_for: number) {
|
||||||
const time = (new Date)
|
const time = (new Date)
|
||||||
.getTime ();
|
.getTime ();
|
||||||
@ -45,11 +41,22 @@ async function create_key (valid_for: number) {
|
|||||||
class KeyStore {
|
class KeyStore {
|
||||||
private _keys: KeyStoreData = {};
|
private _keys: KeyStoreData = {};
|
||||||
private _interval: NodeJS.Timeout;
|
private _interval: NodeJS.Timeout;
|
||||||
|
private _instance: string;
|
||||||
|
|
||||||
|
public get instance_id (): string {
|
||||||
|
return this._instance;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor () {
|
public constructor () {
|
||||||
this._interval = setInterval (() => {
|
this._interval = setInterval (() => {
|
||||||
this.garbage_collect ();
|
this.garbage_collect ();
|
||||||
}, renew_interval);
|
}, renew_interval);
|
||||||
|
this._instance = to_b58 (random_hex (16), 'hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
private get_index (iat: number, instance = this._instance): string {
|
||||||
|
return instance + Math.floor (iat / renew_interval)
|
||||||
|
.toFixed (0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private garbage_collect (set: KeyStoreData = this._keys): void {
|
private garbage_collect (set: KeyStoreData = this._keys): void {
|
||||||
@ -68,7 +75,11 @@ class KeyStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_sign_key (iat: number, valid_for: number): Promise<string> {
|
public async get_sign_key (
|
||||||
|
iat: number,
|
||||||
|
valid_for: number,
|
||||||
|
instance?: string
|
||||||
|
): Promise<string> {
|
||||||
if (valid_for <= 0)
|
if (valid_for <= 0)
|
||||||
throw new Error ('cannot create infinitely valid key');
|
throw new Error ('cannot create infinitely valid key');
|
||||||
|
|
||||||
@ -76,7 +87,7 @@ class KeyStore {
|
|||||||
.getTime ())
|
.getTime ())
|
||||||
throw new Error ('cannot access already expired keys');
|
throw new Error ('cannot access already expired keys');
|
||||||
|
|
||||||
const index = get_index (iat);
|
const index = this.get_index (iat, instance);
|
||||||
|
|
||||||
const valid_until = (new Date)
|
const valid_until = (new Date)
|
||||||
.getTime () + (valid_for * 1000);
|
.getTime () + (valid_for * 1000);
|
||||||
@ -86,6 +97,9 @@ class KeyStore {
|
|||||||
if (key.public_key.valid_until < valid_until)
|
if (key.public_key.valid_until < valid_until)
|
||||||
key.public_key.valid_until = valid_until;
|
key.public_key.valid_until = valid_until;
|
||||||
|
|
||||||
|
if (typeof key.private_key === 'undefined')
|
||||||
|
throw new Error ('cannot access already expired keys');
|
||||||
|
|
||||||
return key.private_key?.key as string;
|
return key.private_key?.key as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,8 +107,8 @@ class KeyStore {
|
|||||||
return this._keys[index].private_key?.key as string;
|
return this._keys[index].private_key?.key as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get_key (iat: number): string {
|
public get_key (iat: number, instance?: string): string {
|
||||||
const index = get_index (iat);
|
const index = this.get_index (iat, instance);
|
||||||
|
|
||||||
if (typeof this._keys[index] === 'undefined')
|
if (typeof this._keys[index] === 'undefined')
|
||||||
throw new Error ('key could not be found');
|
throw new Error ('key could not be found');
|
||||||
@ -115,8 +129,12 @@ class KeyStore {
|
|||||||
public import_verification_data (data: KeyStoreData): void {
|
public import_verification_data (data: KeyStoreData): void {
|
||||||
const import_set = { ...data };
|
const import_set = { ...data };
|
||||||
this.garbage_collect (import_set);
|
this.garbage_collect (import_set);
|
||||||
|
for (const key of Object.keys (import_set)) {
|
||||||
// TODO: import
|
if (typeof this._keys[key] !== 'undefined')
|
||||||
|
throw new Error ('cannot import to the same instance');
|
||||||
|
this._keys[key] = import_set[key];
|
||||||
|
}
|
||||||
|
this.garbage_collect ();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { type } from 'os';
|
|
||||||
import ks from '../lib/KeyStore';
|
import ks from '../lib/KeyStore';
|
||||||
|
|
||||||
export class Response extends http.IncomingMessage {
|
export class Response extends http.IncomingMessage {
|
||||||
@ -64,7 +63,7 @@ export function clock_setup ():void {
|
|||||||
assert_keystore_state ();
|
assert_keystore_state ();
|
||||||
|
|
||||||
const date = (new Date);
|
const date = (new Date);
|
||||||
date.setSeconds (2, 0);
|
date.setHours (0, 0, 2, 0);
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
.install ();
|
.install ();
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
* Created by Timo Hocker <timo@scode.ovh>, December 2020
|
* Created by Timo Hocker <timo@scode.ovh>, December 2020
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ks from '../../lib/KeyStore';
|
import ks, { KeyStore } from '../../lib/KeyStore';
|
||||||
import { clock_finalize, clock_setup } from '../Helper';
|
import { clock_finalize, clock_setup } from '../Helper';
|
||||||
|
|
||||||
const frame = 60;
|
const frame = 3600;
|
||||||
|
|
||||||
/* eslint-disable-next-line max-lines-per-function */
|
/* eslint-disable-next-line max-lines-per-function */
|
||||||
describe ('key store', () => {
|
describe ('key store', () => {
|
||||||
@ -146,5 +146,40 @@ describe ('key store', () => {
|
|||||||
.toBeRejectedWithError ('cannot create infinitely valid key');
|
.toBeRejectedWithError ('cannot create infinitely valid key');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: required use case: insert keys for verification of old tokens
|
it ('should export and import all keys', async () => {
|
||||||
|
const iat = (new Date)
|
||||||
|
.getTime () / 1000;
|
||||||
|
|
||||||
|
const sign = await ks.get_sign_key (iat, frame);
|
||||||
|
const ver = ks.get_key (iat);
|
||||||
|
const exp = ks.export_verification_data ();
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect (Object.keys (ks['_keys']))
|
||||||
|
.toEqual (Object.keys (exp));
|
||||||
|
expect (Object.keys (exp)
|
||||||
|
.filter ((v) => typeof exp[v].private_key !== 'undefined').length)
|
||||||
|
.toEqual (0);
|
||||||
|
|
||||||
|
const ks2 = (new KeyStore);
|
||||||
|
expect (ks2.instance_id).not.toEqual (ks.instance_id);
|
||||||
|
ks2.import_verification_data (exp);
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect (ks2['_keys'])
|
||||||
|
.toEqual (exp);
|
||||||
|
|
||||||
|
const sign2 = await ks2.get_sign_key (iat, frame);
|
||||||
|
const ver2 = ks2.get_key (iat);
|
||||||
|
expect (sign).not.toEqual (sign2);
|
||||||
|
expect (ver).not.toEqual (ver2);
|
||||||
|
await expectAsync (ks2.get_sign_key (iat, 60, ks.instance_id))
|
||||||
|
.toBeRejectedWithError ('cannot access already expired keys');
|
||||||
|
expect (ks2.get_key (iat, ks.instance_id))
|
||||||
|
.toEqual (ver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('should disallow importing to itself', () => {
|
||||||
|
const exp = ks.export_verification_data ();
|
||||||
|
expect (() => ks.import_verification_data (exp))
|
||||||
|
.toThrowError ('cannot import to the same instance');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user