This commit is contained in:
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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
16
lib/Key.ts
Normal 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[];
|
@ -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
86
lib/Redis.ts
Normal 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;
|
@ -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
|
||||
|
Reference in New Issue
Block a user