redis sync
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Timo Hocker
2022-08-08 15:52:56 +02:00
parent 122bd7b574
commit fd26975559
17 changed files with 1860 additions and 1403 deletions

View File

@ -231,12 +231,12 @@ interface CreateHandlerOptions {
type ProcessRequestOptions = Omit<CreateHandlerOptions, 'parse_body'>
// eslint-disable-next-line max-lines-per-function
function process_request (
async function process_request (
request: AuthRequest,
token: RegExpExecArray | null,
default_handler: AuthRequestHandler,
options?: ProcessRequestOptions
): Promise<void> | void {
): Promise<void> {
if (token === null)
return default_handler (request);
@ -259,7 +259,7 @@ function process_request (
request.is_bearer = true;
request.token = token?.groups?.token;
const token_data = auth.verify (request.token as string);
const token_data = await auth.verify (request.token as string);
if (!token_data.valid)
return default_handler (request);

View File

@ -41,7 +41,7 @@ interface SignatureOptions
}
class Authority {
public verify (key: string): VerificationResult {
public async verify (key: string): Promise<VerificationResult> {
logger ('verifying token');
const result: VerificationResult = {
authorized: false,
@ -49,7 +49,7 @@ class Authority {
type: 'none',
id: ''
};
const data = verify_signature_get_info (
const data = await verify_signature_get_info (
key,
(info) => {
try {

View File

@ -17,7 +17,8 @@ const logger = debug ('gateway');
type AnyFunc = (...args: unknown[]) => unknown;
type Gateway = (
req: IncomingMessage,
res: ServerResponse, next: AnyFunc
res: ServerResponse,
next: AnyFunc
) => unknown;
interface RefreshSettings extends AccessSettings {
@ -78,7 +79,7 @@ class GatewayClass {
return auth.groups?.data;
}
public try_access (req: IncomingMessage): boolean {
public async try_access (req: IncomingMessage): Promise<boolean> {
logger ('authenticating incoming request');
let auth = this.get_header_auth (req);
if (auth === null)
@ -88,7 +89,7 @@ class GatewayClass {
return false;
}
const ver = authority.verify (auth);
const ver = await authority.verify (auth);
logger ('setting connection info');
const con = req.connection as unknown as Record<string, unknown>;
@ -119,20 +120,20 @@ class GatewayClass {
return false;
}
const ver = authority.verify (refresh);
const ver = await authority.verify (refresh);
if (ver.type === 'refresh_token' && ver.valid) {
logger ('refresh token valid, generating new tokens');
const auth_request = new AuthRequest (
req,
res,
''
, this._options.cookie,
'',
this._options.cookie,
this._options.refresh_cookie
);
const refresh_result = await auth_request.allow_access ({
...this._options.refresh_settings,
data: ver.data,
leave_open: true
data: ver.data,
leave_open: true
});
logger ('setting connection info');
@ -155,7 +156,7 @@ class GatewayClass {
res: ServerResponse
): Promise<boolean> {
logger ('trying to authenticate http request');
if (this.try_access (req)) {
if (await this.try_access (req)) {
logger ('authenticated via access_token');
return true;
}
@ -183,7 +184,7 @@ class GatewayClass {
return this.redirect (res);
}
public logout (req: IncomingMessage): void {
public async logout (req: IncomingMessage): Promise<void> {
const l = logger.extend ('logout');
l ('invalidating all submitted tokens');
const auth_strings = [
@ -191,10 +192,13 @@ class GatewayClass {
extract_cookie (this._options.cookie?.name, req.headers.cookie),
extract_cookie (this._options.refresh_cookie?.name, req.headers.cookie)
];
const tokens = auth_strings
.filter ((v) => v !== null)
.map ((v) => authority.verify (v as string))
.filter ((v) => v.valid);
const tokens = (
await Promise.all (
auth_strings
.filter ((v) => v !== null)
.map ((v) => authority.verify (v as string))
)
).filter ((v) => v.valid);
l ('found %d tokens: %O', tokens.length, tokens);
@ -210,10 +214,4 @@ export default function create_gateway (options: GatewayOptions): Gateway {
return g.process_request.bind (g);
}
export {
AnyFunc,
Gateway,
GatewayOptions,
GatewayClass,
RefreshSettings
};
export { AnyFunc, Gateway, GatewayOptions, GatewayClass, RefreshSettings };

16
lib/Key.ts Normal file
View File

@ -0,0 +1,16 @@
export interface Key {
key: string;
valid_until: number;
}
export interface LabelledKey extends Key {
index: string;
}
export interface KeyPair {
private_key?: Key;
public_key: Key;
}
export type KeyStoreData = Record<string, KeyPair>;
export type KeyStoreExport = LabelledKey[];

View File

@ -8,49 +8,18 @@
import { generate_keypair, random_hex } from '@sapphirecode/crypto-helper';
import { to_b58 } from '@sapphirecode/encoding-helper';
import { debug } from './debug';
import { KeyStoreData, KeyStoreExport } from './Key';
import { redis } from './Redis';
const logger = debug ('keystore');
const renew_interval = 3600;
interface Key {
key: string;
valid_until: number;
}
interface LabelledKey extends Key {
index: string;
}
interface KeyPair {
private_key?: Key;
public_key: Key;
}
type KeyStoreData = Record<string, KeyPair>;
type KeyStoreExport = LabelledKey[];
async function create_key (valid_for: number) {
logger ('generating new key');
const time = (new Date)
.getTime ();
const pair = await generate_keypair ();
return {
private_key: {
key: pair.private_key,
valid_until: time + (renew_interval * 1000)
},
public_key: {
key: pair.public_key,
valid_until: time + (valid_for * 1000)
}
};
}
class KeyStore {
private _keys: KeyStoreData = {};
private _interval: NodeJS.Timeout;
private _instance: string;
private _sync_redis = false;
public get instance_id (): string {
return this._instance;
@ -69,6 +38,27 @@ class KeyStore {
.toFixed (0);
}
private async create_key (index: string, valid_for: number): Promise<void> {
const log = logger.extend ('create_key');
log ('generating new key');
const time = (new Date)
.getTime ();
const pair = await generate_keypair ();
const result = {
private_key: {
key: pair.private_key,
valid_until: time + (renew_interval * 1000)
},
public_key: {
key: pair.public_key,
valid_until: time + (valid_for * 1000)
}
};
if (this._sync_redis)
await redis.set_key ({ ...result.public_key, index });
this._keys[index] = result;
}
private garbage_collect (): void {
const time = (new Date)
.getTime ();
@ -128,19 +118,26 @@ class KeyStore {
}
logger ('key does not exist, creating a new one');
this._keys[index] = await create_key (valid_for);
await this.create_key (index, valid_for);
return this._keys[index].private_key?.key as string;
}
public get_key (iat: number, instance?: string): string {
public async get_key (iat: number, instance?: string): Promise<string> {
logger ('querying public key from %s for timestamp %d', instance, iat);
const index = this.get_index (iat, instance);
if (typeof this._keys[index] === 'undefined')
let key = null;
if (typeof this._keys[index] === 'undefined') {
if (this._sync_redis)
key = await redis.get_key (index);
}
else { key = this._keys[index].public_key; }
if (key === null)
throw new Error ('key could not be found');
const key = this._keys[index];
return key.public_key.key;
return key.key;
}
public export_verification_data (): KeyStoreExport {
@ -172,9 +169,16 @@ class KeyStore {
logger ('resetting keystore');
this._instance = to_b58 (random_hex (16), 'hex');
this._keys = {};
this._sync_redis = false;
redis.disconnect ();
}
public sync_redis (url: string): void {
redis.connect (url);
this._sync_redis = true;
}
}
const ks: KeyStore = (new KeyStore);
export default ks;
export { KeyStore, Key, LabelledKey, KeyStoreExport };
export { KeyStore };

86
lib/Redis.ts Normal file
View File

@ -0,0 +1,86 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
import IORedis from 'ioredis';
import { debug } from './debug';
import { LabelledKey } from './Key';
const logger = debug ('redis');
export class Redis {
private _redis: IORedis | null = null;
public connect (url: string): void {
const log = logger.extend ('connect');
log ('connecting to redis instance %s', url);
if (this._redis !== null) {
log ('disconnecting existing redis client');
this.disconnect ();
}
this._redis = new IORedis (url);
this._redis.on ('connect', () => {
log ('connected');
});
this._redis.on ('ready', () => {
log ('ready');
});
this._redis.on ('error', (err) => {
log ('error %o', err);
});
this._redis.on ('reconnecting', () => {
log ('reconnecting');
});
this._redis.on ('end', () => {
log ('connection ended');
});
}
public disconnect (): void {
const log = logger.extend ('disconnect');
log ('disconnecting redis client');
if (this._redis === null) {
log ('redis is inactive, skipping');
return;
}
this._redis.quit ();
this._redis = null;
log ('done');
}
public async set_key (key: LabelledKey): Promise<void> {
const log = logger.extend ('set_key');
log ('trying to set key %s to redis', key.index);
if (this._redis === null) {
log ('redis is inactive, skipping');
return;
}
const valid_for = (key.valid_until - (new Date)
.getTime ()) / 1000;
log ('key is valid for %d seconds', valid_for);
await this._redis.setex (key.index, valid_for, JSON.stringify (key));
log ('saved key');
}
public async get_key (index: string): Promise<LabelledKey | null> {
const log = logger.extend ('get_key');
log ('trying to get key %s from redis', index);
if (this._redis === null) {
log ('redis is inactive, skipping');
return null;
}
const res = await this._redis.get (index);
if (res === null) {
log ('key not found in redis');
return null;
}
log ('key found');
return JSON.parse (res);
}
}
export const redis = new Redis;

View File

@ -30,10 +30,11 @@ import create_gateway, {
AnyFunc,
RefreshSettings
} from './Gateway';
import keystore, {
KeyStore, KeyStoreExport,
import keystore, { KeyStore } from './KeyStore';
import {
KeyStoreExport,
LabelledKey, Key
} from './KeyStore';
} from './Key';
import {
CookieSettings,
SameSiteValue