Compare commits

..

No commits in common. "e80e3f9a94ee01376a40b237fa470da419b0d7e0" and "d5c136790ee7521dc5d884e23b108a22c122c49d" have entirely different histories.

14 changed files with 3447 additions and 3533 deletions

View File

@ -78,7 +78,7 @@ class AuthRequest {
this._cookie = cookie; this._cookie = cookie;
this._refresh_cookie = refresh_cookie; this._refresh_cookie = refresh_cookie;
this._is_successful = false; this._is_successful = false;
logger.extend ('constructor') ('started processing new auth request'); logger ('started processing new auth request');
} }
private default_header (set_content = true) { private default_header (set_content = true) {
@ -97,8 +97,7 @@ class AuthRequest {
data, data,
leave_open leave_open
}: AccessSettings): Promise<AccessResult> { }: AccessSettings): Promise<AccessResult> {
const log = logger.extend ('allow_access'); logger ('allowed access');
log ('allowed access');
this.default_header (typeof redirect_to !== 'string' && !leave_open); this.default_header (typeof redirect_to !== 'string' && !leave_open);
const at = await auth.sign ( const at = await auth.sign (
@ -120,7 +119,7 @@ class AuthRequest {
cookies.push (build_cookie (this._cookie, at.signature)); cookies.push (build_cookie (this._cookie, at.signature));
if (include_refresh_token) { if (include_refresh_token) {
log ('including refresh token'); logger ('including refresh token');
if (typeof refresh_token_expires_in !== 'number') if (typeof refresh_token_expires_in !== 'number')
throw new Error ('no expiry time defined for refresh tokens'); throw new Error ('no expiry time defined for refresh tokens');
const rt = await auth.sign ( const rt = await auth.sign (
@ -137,7 +136,7 @@ class AuthRequest {
} }
if (cookies.length > 0) { if (cookies.length > 0) {
log ('sending %d cookies', cookies.length); logger ('sending %d cookies', cookies.length);
this.response.setHeader ( this.response.setHeader (
'Set-Cookie', 'Set-Cookie',
cookies cookies
@ -147,7 +146,7 @@ class AuthRequest {
this._is_successful = true; this._is_successful = true;
if (typeof redirect_to === 'string') { if (typeof redirect_to === 'string') {
log ('redirecting to %s', redirect_to); logger ('redirecting to %s', redirect_to);
this.response.setHeader ('Location', redirect_to); this.response.setHeader ('Location', redirect_to);
this.response.statusCode = 302; this.response.statusCode = 302;
if (!leave_open) if (!leave_open)
@ -156,7 +155,7 @@ class AuthRequest {
} }
if (!leave_open) { if (!leave_open) {
log ('finishing http request'); logger ('finishing http request');
this.response.writeHead (200); this.response.writeHead (200);
this.response.end (JSON.stringify (res)); this.response.end (JSON.stringify (res));
} }
@ -170,8 +169,7 @@ class AuthRequest {
data?: Record<string, unknown>, data?: Record<string, unknown>,
leave_open = false leave_open = false
): Promise<string> { ): Promise<string> {
const log = logger.extend ('allow_part'); logger ('allowed part token');
log ('allowed part token');
this.default_header (); this.default_header ();
const pt = await auth.sign ( const pt = await auth.sign (
@ -187,7 +185,7 @@ class AuthRequest {
}; };
if (!leave_open) { if (!leave_open) {
log ('finishing http request'); logger ('finishing http request');
this.response.writeHead (200); this.response.writeHead (200);
this.response.end (JSON.stringify (res)); this.response.end (JSON.stringify (res));
} }
@ -197,12 +195,11 @@ class AuthRequest {
} }
public invalid (error_description?: string, leave_open = false): void { public invalid (error_description?: string, leave_open = false): void {
const log = logger.extend ('invalid'); logger ('rejecting invalid request');
log ('rejecting invalid request');
this.default_header (); this.default_header ();
this.response.statusCode = 400; this.response.statusCode = 400;
if (!leave_open) { if (!leave_open) {
log ('finishing http request'); logger ('finishing http request');
this.response.end (JSON.stringify ({ this.response.end (JSON.stringify ({
error: 'invalid_request', error: 'invalid_request',
error_description error_description
@ -211,12 +208,11 @@ class AuthRequest {
} }
public deny (leave_open = false): void { public deny (leave_open = false): void {
const log = logger.extend ('deny'); logger ('denied access');
log ('denied access');
this.default_header (); this.default_header ();
this.response.statusCode = 401; this.response.statusCode = 401;
if (!leave_open) { if (!leave_open) {
log ('finishing http request'); logger ('finishing http request');
this.response.end (JSON.stringify ({ error: 'invalid_client' })); this.response.end (JSON.stringify ({ error: 'invalid_client' }));
} }
} }
@ -234,19 +230,18 @@ interface CreateHandlerOptions {
type ProcessRequestOptions = Omit<CreateHandlerOptions, 'parse_body'> type ProcessRequestOptions = Omit<CreateHandlerOptions, 'parse_body'>
// eslint-disable-next-line max-lines-per-function, max-statements // eslint-disable-next-line max-lines-per-function
async function process_request ( async function process_request (
request: AuthRequest, request: AuthRequest,
token: RegExpExecArray | null, token: RegExpExecArray | null,
default_handler: AuthRequestHandler, default_handler: AuthRequestHandler,
options?: ProcessRequestOptions options?: ProcessRequestOptions
): Promise<void> { ): Promise<void> {
const log = logger.extend ('process_request');
if (token === null) if (token === null)
return default_handler (request); return default_handler (request);
if ((/Basic/ui).test (token?.groups?.type as string)) { if ((/Basic/ui).test (token?.groups?.type as string)) {
log ('found basic login data'); logger ('found basic login data');
request.is_basic = true; request.is_basic = true;
let login = token?.groups?.token as string; let login = token?.groups?.token as string;
@ -260,7 +255,7 @@ async function process_request (
} }
if ((/Bearer/ui).test (token?.groups?.type as string)) { if ((/Bearer/ui).test (token?.groups?.type as string)) {
log ('found bearer login data'); logger ('found bearer login data');
request.is_bearer = true; request.is_bearer = true;
request.token = token?.groups?.token; request.token = token?.groups?.token;
@ -269,7 +264,7 @@ async function process_request (
if (!token_data.valid) if (!token_data.valid)
return default_handler (request); return default_handler (request);
log ('bearer token is valid'); logger ('bearer token is valid');
request.token_data = token_data.data; request.token_data = token_data.data;
request.token_id = token_data.id; request.token_id = token_data.id;
@ -279,7 +274,7 @@ async function process_request (
&& typeof options.refresh !== 'undefined' && typeof options.refresh !== 'undefined'
&& token_data.type === 'refresh_token' && token_data.type === 'refresh_token'
) { ) {
log ('found refresh token, emitting new access token'); logger ('found refresh token, emitting new access token');
request.allow_access (options.refresh); request.allow_access (options.refresh);
return Promise.resolve (); return Promise.resolve ();
} }
@ -292,7 +287,7 @@ async function process_request (
&& Object.keys (options.modules) && Object.keys (options.modules)
.includes (token_data.next_module) .includes (token_data.next_module)
) { ) {
log ('processing module %s', token_data.next_module); logger ('processing module %s', token_data.next_module);
return options.modules[token_data.next_module] (request); return options.modules[token_data.next_module] (request);
} }
@ -300,7 +295,7 @@ async function process_request (
return Promise.resolve (); return Promise.resolve ();
} }
log ('no matching login method, triggering default handler'); logger ('no matching login method, triggering default handler');
return default_handler (request); return default_handler (request);
} }
@ -309,7 +304,7 @@ export default function create_auth_handler (
default_handler: AuthRequestHandler, default_handler: AuthRequestHandler,
options?: CreateHandlerOptions options?: CreateHandlerOptions
): AuthHandler { ): AuthHandler {
logger.extend ('create_auth_handler') ('creating new auth handler'); logger ('creating new auth handler');
if ( if (
typeof options?.cookie !== 'undefined' typeof options?.cookie !== 'undefined'
&& typeof options?.refresh_cookie !== 'undefined' && typeof options?.refresh_cookie !== 'undefined'

View File

@ -42,8 +42,7 @@ interface SignatureOptions
class Authority { class Authority {
public async verify (key: string): Promise<VerificationResult> { public async verify (key: string): Promise<VerificationResult> {
const log = logger.extend ('verify'); logger ('verifying token');
log ('verifying token');
const result: VerificationResult = { const result: VerificationResult = {
authorized: false, authorized: false,
valid: false, valid: false,
@ -64,7 +63,7 @@ class Authority {
); );
if (data === null) { if (data === null) {
log ('token invalid'); logger ('token invalid');
result.error = 'invalid signature'; result.error = 'invalid signature';
return result; return result;
} }
@ -72,10 +71,10 @@ class Authority {
result.id = data.id; result.id = data.id;
result.type = data.type; result.type = data.type;
log ('parsing token %s %s', result.type, result.id); logger ('parsing token %s %s', result.type, result.id);
if (!blacklist.is_valid (data.id)) { if (!blacklist.is_valid (data.id)) {
log ('token is blacklisted'); logger ('token is blacklisted');
result.error = 'blacklisted'; result.error = 'blacklisted';
return result; return result;
} }
@ -85,7 +84,7 @@ class Authority {
result.next_module = data.next_module; result.next_module = data.next_module;
result.data = data.obj; result.data = data.obj;
log ( logger (
'valid %s; targeting module %s', 'valid %s; targeting module %s',
result.type, result.type,
result.next_module result.next_module
@ -99,8 +98,7 @@ class Authority {
valid_for: number, valid_for: number,
options?: SignatureOptions options?: SignatureOptions
): Promise<SignatureResult> { ): Promise<SignatureResult> {
const log = logger.extend ('sign'); logger ('signing new %s', type);
log ('signing new %s', type);
const time = Date.now (); const time = Date.now ();
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 = {
@ -112,7 +110,7 @@ class Authority {
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);
log ('created token %s', attributes.id); logger ('created token %s', attributes.id);
return { id: attributes.id, signature }; return { id: attributes.id, signature };
} }
} }

View File

@ -22,58 +22,46 @@ class Blacklist {
} }
public clear (before: number = Number.POSITIVE_INFINITY): void { public clear (before: number = Number.POSITIVE_INFINITY): void {
logger.extend ('clear') ('clearing blacklist'); logger ('clearing blacklist');
for (let i = this._signatures.length - 1; i >= 0; i--) { for (let i = this._signatures.length - 1; i >= 0; i--) {
if (this._signatures[i].iat < before) if (this._signatures[i].iat < before)
this.remove_signature (i); this._signatures.splice (i, 1);
} }
} }
public add_signature (hash: string): void { public add_signature (hash: string): void {
logger.extend ('add_signature') ('blacklisting signature %s', hash); logger ('blacklisting signature %s', hash);
this._signatures.push ({ iat: Date.now (), hash }); this._signatures.push ({ iat: Date.now (), hash });
} }
public remove_signature (signature: number | string): void { public remove_signature (hash: string): void {
const log = logger.extend ('remove_signature'); logger ('removing signature from blacklist %s', hash);
log ('removing signature from blacklist %s', signature); for (let i = this._signatures.length - 1; i >= 0; i--) {
if (typeof signature === 'string') { if (this._signatures[i].hash === hash)
log ('received string, searching through signatures'); this._signatures.splice (i, 1);
for (let i = this._signatures.length - 1; i >= 0; i--) {
if (this._signatures[i].hash === signature)
this._signatures.splice (i, 1);
}
}
else {
log ('received index, removing at index');
this._signatures.splice (signature, 1);
} }
} }
public is_valid (hash: string): boolean { public is_valid (hash: string): boolean {
const log = logger.extend ('is_valid'); logger ('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.hash === hash) {
log ('found matching blacklist entry'); logger ('found matching blacklist entry');
return false; return false;
} }
} }
log ('signature is not blacklisted'); logger ('signature is not blacklisted');
return true; return true;
} }
public export_blacklist (): Signature[] { public export_blacklist (): Signature[] {
logger.extend ('export_blacklist') ('exporting blacklist'); logger ('exporting blacklist');
return this._signatures; return this._signatures;
} }
public import_blacklist (data: Signature[]): void { public import_blacklist (data: Signature[]): void {
logger.extend ('import_blacklist') ( logger ('importing %d blacklist entries', data.length);
'importing %d blacklist entries',
data.length
);
this._signatures.push (...data); this._signatures.push (...data);
} }
} }

View File

@ -38,8 +38,7 @@ class GatewayClass {
private _options: GatewayOptions; private _options: GatewayOptions;
public constructor (options: GatewayOptions = {}) { public constructor (options: GatewayOptions = {}) {
const log = logger.extend ('constructor'); logger ('creating new gateway');
log ('creating new gateway');
if ( if (
typeof options?.cookie !== 'undefined' typeof options?.cookie !== 'undefined'
&& typeof options?.refresh_cookie !== 'undefined' && typeof options?.refresh_cookie !== 'undefined'
@ -51,16 +50,15 @@ class GatewayClass {
} }
public deny (res: ServerResponse): void { public deny (res: ServerResponse): void {
logger.extend ('deny') ('denied http request'); logger ('denied http request');
res.statusCode = 403; res.statusCode = 403;
res.end (); res.end ();
} }
public redirect (res: ServerResponse): void { public redirect (res: ServerResponse): void {
const log = logger.extend ('redirect'); logger ('redirecting http request to %s', this._options.redirect_url);
log ('redirecting http request to %s', this._options.redirect_url);
if (typeof this._options.redirect_url !== 'string') { if (typeof this._options.redirect_url !== 'string') {
log ('no redirect url defined'); logger ('no redirect url defined');
this.deny (res); this.deny (res);
return; return;
} }
@ -70,36 +68,34 @@ class GatewayClass {
} }
public get_header_auth (req: IncomingMessage): string | null { public get_header_auth (req: IncomingMessage): string | null {
const log = logger.extend ('get_header_auth'); logger ('extracting authorization header');
log ('extracting authorization header');
const auth_header = req.headers.authorization; const auth_header = req.headers.authorization;
const auth = (/(?<type>\w+) (?<data>.*)/u).exec (auth_header || ''); const auth = (/(?<type>\w+) (?<data>.*)/u).exec (auth_header || '');
if (auth === null) if (auth === null)
return null; return null;
if (auth.groups?.type !== 'Bearer') if (auth.groups?.type !== 'Bearer')
return null; return null;
log ('found bearer token'); logger ('found bearer token');
return auth.groups?.data; return auth.groups?.data;
} }
public async try_access (req: IncomingMessage): Promise<boolean> { public async try_access (req: IncomingMessage): Promise<boolean> {
const log = logger.extend ('try_access'); logger ('authenticating incoming request');
log ('authenticating incoming request');
let auth = this.get_header_auth (req); let auth = this.get_header_auth (req);
if (auth === null) if (auth === null)
auth = extract_cookie (this._options.cookie?.name, req.headers.cookie); auth = extract_cookie (this._options.cookie?.name, req.headers.cookie);
if (auth === null) { if (auth === null) {
log ('found no auth token'); logger ('found no auth token');
return false; return false;
} }
const ver = await authority.verify (auth); const ver = await authority.verify (auth);
log ('setting connection info'); logger ('setting connection info');
const con = req.connection as unknown as Record<string, unknown>; const con = req.connection as unknown as Record<string, unknown>;
con.auth = { token_id: ver.id, token_data: ver.data }; con.auth = { token_id: ver.id, token_data: ver.data };
log ('token valid: %s', ver.authorized); logger ('token valid: %s', ver.authorized);
return ver.authorized; return ver.authorized;
} }
@ -107,27 +103,26 @@ class GatewayClass {
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse res: ServerResponse
): Promise<boolean> { ): Promise<boolean> {
const log = logger.extend ('try_refresh');
if ( if (
typeof this._options.refresh_cookie === 'undefined' typeof this._options.refresh_cookie === 'undefined'
|| typeof this._options.refresh_settings === 'undefined' || typeof this._options.refresh_settings === 'undefined'
) )
return false; return false;
log ('trying to apply refresh token'); logger ('trying to apply refresh token');
const refresh = extract_cookie ( const refresh = extract_cookie (
this._options.refresh_cookie.name, this._options.refresh_cookie.name,
req.headers.cookie req.headers.cookie
); );
if (refresh === null) { if (refresh === null) {
log ('could not find refresh token'); logger ('could not find refresh token');
return false; return false;
} }
const ver = await authority.verify (refresh); const ver = await authority.verify (refresh);
if (ver.type === 'refresh_token' && ver.valid) { if (ver.type === 'refresh_token' && ver.valid) {
log ('refresh token valid, generating new tokens'); logger ('refresh token valid, generating new tokens');
const auth_request = new AuthRequest ( const auth_request = new AuthRequest (
req, req,
res, res,
@ -141,18 +136,18 @@ class GatewayClass {
leave_open: true leave_open: true
}); });
log ('setting connection info'); logger ('setting connection info');
const con = req.connection as unknown as Record<string, unknown>; const con = req.connection as unknown as Record<string, unknown>;
con.auth = { con.auth = {
token_id: refresh_result.access_token_id, token_id: refresh_result.access_token_id,
token_data: ver.data token_data: ver.data
}; };
log ('tokens refreshed'); logger ('tokens refreshed');
return true; return true;
} }
log ('refresh token invalid'); logger ('refresh token invalid');
return false; return false;
} }
@ -160,18 +155,17 @@ class GatewayClass {
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse res: ServerResponse
): Promise<boolean> { ): Promise<boolean> {
const log = logger.extend ('authenticate'); logger ('trying to authenticate http request');
log ('trying to authenticate http request');
if (await this.try_access (req)) { if (await this.try_access (req)) {
log ('authenticated via access_token'); logger ('authenticated via access_token');
return true; return true;
} }
if (await this.try_refresh (req, res)) { if (await this.try_refresh (req, res)) {
log ('authenticated via refresh_token'); logger ('authenticated via refresh_token');
return true; return true;
} }
log ('could not verify session'); logger ('could not verify session');
return false; return false;
} }
@ -180,20 +174,19 @@ class GatewayClass {
res: ServerResponse, res: ServerResponse,
next: AnyFunc next: AnyFunc
): Promise<unknown> { ): Promise<unknown> {
const log = logger.extend ('process_request'); logger ('processing incoming http request');
log ('processing incoming http request');
if (await this.authenticate (req, res)) { if (await this.authenticate (req, res)) {
log ('authentification successful, calling next handler'); logger ('authentification successful, calling next handler');
return next (); return next ();
} }
log ('failed to authenticate, redirecting client'); logger ('failed to authenticate, redirecting client');
return this.redirect (res); return this.redirect (res);
} }
public async logout (req: IncomingMessage): Promise<void> { public async logout (req: IncomingMessage): Promise<void> {
const log = logger.extend ('logout'); const l = logger.extend ('logout');
log ('invalidating all submitted tokens'); l ('invalidating all submitted tokens');
const auth_strings = [ const auth_strings = [
this.get_header_auth (req), this.get_header_auth (req),
extract_cookie (this._options.cookie?.name, req.headers.cookie), extract_cookie (this._options.cookie?.name, req.headers.cookie),
@ -207,12 +200,12 @@ class GatewayClass {
) )
).filter ((v) => v.valid); ).filter ((v) => v.valid);
log ('found %d tokens: %O', tokens.length, tokens); l ('found %d tokens: %O', tokens.length, tokens);
for (const token of tokens) for (const token of tokens)
blacklist.add_signature (token.id); blacklist.add_signature (token.id);
log ('complete'); l ('complete');
} }
} }

View File

@ -1,10 +1,3 @@
/*
* 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
*/
export interface Key { export interface Key {
key: string; key: string;
valid_until: number; valid_until: number;

View File

@ -10,7 +10,6 @@ import { to_b58 } from '@sapphirecode/encoding-helper';
import { debug } from './debug'; import { debug } from './debug';
import { KeyStoreData, KeyStoreExport } from './Key'; import { KeyStoreData, KeyStoreExport } from './Key';
import { redis } from './Redis'; import { redis } from './Redis';
import { redis_key_store } from './RedisData/RedisKeyStore';
const logger = debug ('keystore'); const logger = debug ('keystore');
@ -31,10 +30,7 @@ class KeyStore {
this.garbage_collect (); this.garbage_collect ();
}, renew_interval); }, renew_interval);
this._instance = to_b58 (random_hex (16), 'hex'); this._instance = to_b58 (random_hex (16), 'hex');
logger.extend ('constructor') ( logger ('created keystore instance %s', this._instance);
'created keystore instance %s',
this._instance
);
} }
private get_index (iat: number, instance = this._instance): string { private get_index (iat: number, instance = this._instance): string {
@ -59,12 +55,11 @@ class KeyStore {
} }
}; };
if (this._sync_redis) if (this._sync_redis)
await redis_key_store.set ({ ...result.public_key, index }); await redis.set_key ({ ...result.public_key, index });
this._keys[index] = result; this._keys[index] = result;
} }
private garbage_collect (): void { private garbage_collect (): void {
const log = logger.extend ('garbage_collect');
const time = (new Date) const time = (new Date)
.getTime (); .getTime ();
const keys = Object.keys (this._keys); const keys = Object.keys (this._keys);
@ -73,12 +68,12 @@ class KeyStore {
if (typeof entry.private_key !== 'undefined' if (typeof entry.private_key !== 'undefined'
&& entry.private_key.valid_until < time && entry.private_key.valid_until < time
) { ) {
log ('deleting expired private key'); logger ('deleting expired private key');
delete entry.private_key; delete entry.private_key;
} }
if (entry.public_key.valid_until < time) { if (entry.public_key.valid_until < time) {
log ('deleting expired key pair'); logger ('deleting expired key pair');
delete this._keys[index]; delete this._keys[index];
} }
} }
@ -89,8 +84,7 @@ class KeyStore {
valid_for: number, valid_for: number,
instance?: string instance?: string
): Promise<string> { ): Promise<string> {
const log = logger.extend ('get_sign_key'); logger (
log (
'querying key from %s for timestamp %d, valid for %d', 'querying key from %s for timestamp %d, valid for %d',
instance, instance,
iat, iat,
@ -109,35 +103,34 @@ class KeyStore {
.getTime () + (valid_for * 1000); .getTime () + (valid_for * 1000);
if (typeof this._keys[index] !== 'undefined') { if (typeof this._keys[index] !== 'undefined') {
log ('loading existing key'); logger ('loading existing key');
const key = this._keys[index]; const key = this._keys[index];
if (typeof key.private_key === 'undefined') if (typeof key.private_key === 'undefined')
throw new Error ('cannot access already expired keys'); throw new Error ('cannot access already expired keys');
if (key.public_key.valid_until < valid_until) { if (key.public_key.valid_until < valid_until) {
log ('updating key valid timespan to match new value'); logger ('updating key valid timespan to match new value');
key.public_key.valid_until = valid_until; key.public_key.valid_until = valid_until;
} }
return key.private_key?.key as string; return key.private_key?.key as string;
} }
log ('key does not exist, creating a new one'); logger ('key does not exist, creating a new one');
await this.create_key (index, valid_for); await this.create_key (index, valid_for);
return this._keys[index].private_key?.key as string; return this._keys[index].private_key?.key as string;
} }
public async get_key (iat: number, instance?: string): Promise<string> { public async get_key (iat: number, instance?: string): Promise<string> {
const log = logger.extend ('get_key'); logger ('querying public key from %s for timestamp %d', instance, iat);
log ('querying public key from %s for timestamp %d', instance, iat);
const index = this.get_index (iat, instance); const index = this.get_index (iat, instance);
let key = null; let key = null;
if (typeof this._keys[index] === 'undefined') { if (typeof this._keys[index] === 'undefined') {
if (this._sync_redis) if (this._sync_redis)
key = await redis_key_store.get (index); key = await redis.get_key (index);
} }
else { key = this._keys[index].public_key; } else { key = this._keys[index].public_key; }
@ -148,23 +141,18 @@ class KeyStore {
} }
public export_verification_data (): KeyStoreExport { public export_verification_data (): KeyStoreExport {
const log = logger.extend ('export_verification_data'); logger ('exporting public keys');
log ('exporting public keys');
log ('cleaning up before export');
this.garbage_collect (); this.garbage_collect ();
const out: KeyStoreExport = []; const out: KeyStoreExport = [];
for (const index of Object.keys (this._keys)) { for (const index of Object.keys (this._keys))
log ('exporting key %s', index);
out.push ({ ...this._keys[index].public_key, index }); out.push ({ ...this._keys[index].public_key, index });
}
return out; return out;
} }
public import_verification_data (data: KeyStoreExport): void { public import_verification_data (data: KeyStoreExport): void {
const log = logger.extend ('import_verification_data'); logger ('importing %d public keys', data.length);
log ('importing %d public keys', data.length);
for (const key of data) { for (const key of data) {
log ('importing key %s', key.index);
if (typeof this._keys[key.index] !== 'undefined') if (typeof this._keys[key.index] !== 'undefined')
throw new Error ('cannot import to the same instance'); throw new Error ('cannot import to the same instance');
this._keys[key.index] = { this._keys[key.index] = {
@ -174,12 +162,11 @@ class KeyStore {
} }
}; };
} }
log ('running garbage collector');
this.garbage_collect (); this.garbage_collect ();
} }
public reset_instance (): void { public reset_instance (): void {
logger.extend ('reset_instance') ('resetting keystore'); logger ('resetting keystore');
this._instance = to_b58 (random_hex (16), 'hex'); this._instance = to_b58 (random_hex (16), 'hex');
this._keys = {}; this._keys = {};
this._sync_redis = false; this._sync_redis = false;

View File

@ -7,9 +7,13 @@
import IORedis from 'ioredis'; import IORedis from 'ioredis';
import { debug } from './debug'; import { debug } from './debug';
import { LabelledKey } from './Key';
const logger = debug ('redis'); const logger = debug ('redis');
export type SyncClass = 'blacklist' | 'keystore'
export type SyncValue = LabelledKey | string;
export class Redis { export class Redis {
private _redis: IORedis | null = null; private _redis: IORedis | null = null;
@ -51,15 +55,47 @@ export class Redis {
log ('done'); log ('done');
} }
public get redis (): IORedis { public async set (
if (this._redis === null) sync_class: SyncClass,
throw new Error ('redis is not connected'); key: string,
value: SyncValue
): Promise<void> {
const log = logger.extend ('set');
log ('trying to set %s value %s to redis', sync_class, key);
if (this._redis === null) {
log ('redis is inactive, skipping');
return;
}
let valid_for = null;
if (sync_class === 'keystore') {
valid_for = Math.floor (
(key.valid_until - (new Date)
.getTime ()) / 1000
);
log ('key is valid for %d seconds', valid_for);
}
if (valid_for === null)
await this._redis.set (key, JSON.stringify (value));
else
await this._redis.setex (key, valid_for, JSON.stringify (value));
return this._redis; log ('saved value');
} }
public get is_active (): boolean { public async get_key (index: string): Promise<LabelledKey | null> {
return this._redis !== 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);
} }
} }

View File

@ -1,36 +0,0 @@
/*
* 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 { debug } from '../debug';
import { redis } from '../Redis';
const logger = debug ('RedisBlacklistStore');
export class RedisBlacklistStore {
public async add (key: string): Promise<void> {
const log = logger.extend ('set');
log ('trying to add key %s to redis blacklist', key);
if (!redis.is_active) {
log ('redis is inactive, skipping');
return;
}
await redis.redis.sadd ('blacklist', key);
log ('saved key');
}
public async get (key: string): Promise<boolean> {
const log = logger.extend ('get');
log ('trying to find key %s in redis blacklist', key);
if (!redis.is_active) {
log ('redis is inactive, skipping');
return false;
}
const res = await redis.redis.sismember ('blacklist', key) === 1;
log ('found key %s', res);
return res;
}
}

View File

@ -1,52 +0,0 @@
/*
* 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 { debug } from '../debug';
import { LabelledKey } from '../Key';
import { redis } from '../Redis';
const logger = debug ('RedisKeyStore');
export class RedisKeyStore {
public async set (value: LabelledKey): Promise<void> {
const log = logger.extend ('set');
log ('trying to set key %s to redis', value.index);
if (!redis.is_active) {
log ('redis is inactive, skipping');
return;
}
const valid_for = Math.floor (
(value.valid_until - (new Date)
.getTime ()) / 1000
);
log ('key is valid for %d seconds', valid_for);
await redis.redis.setex (
`keystore_${value.index}`,
valid_for,
JSON.stringify (value)
);
log ('saved key');
}
public async get (index: string): Promise<LabelledKey | null> {
const log = logger.extend ('get');
log ('trying to get key %s from redis', index);
if (!redis.is_active) {
log ('redis is inactive, skipping');
return null;
}
const res = await redis.redis.get (`keystore_${index}`);
if (res === null) {
log ('key not found in redis');
return null;
}
log ('key found');
return JSON.parse (res);
}
}
export const redis_key_store = new RedisKeyStore;

View File

@ -1,10 +1,3 @@
/*
* 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 { run_regex } from '@sapphirecode/utilities'; import { run_regex } from '@sapphirecode/utilities';
import { debug } from './debug'; import { debug } from './debug';
@ -61,8 +54,7 @@ function extract_cookie (
name: string|undefined, name: string|undefined,
header: string|undefined header: string|undefined
): string| null { ): string| null {
const log = logger.extend ('extract_cookie'); logger (`extracting cookie ${name}`);
log (`extracting cookie ${name}`);
const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu; const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu;
@ -72,9 +64,9 @@ function extract_cookie (
cookie_regex, cookie_regex,
header, header,
(res: RegExpMatchArray) => { (res: RegExpMatchArray) => {
log ('parsing cookie %s', res.groups?.name); logger ('parsing cookie %s', res.groups?.name);
if (res.groups?.name === name) { if (res.groups?.name === name) {
log ('found cookie'); logger ('found cookie');
result = res.groups?.value as string; result = res.groups?.value as string;
} }
} }

View File

@ -1,10 +1,3 @@
/*
* 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 build_debug from 'debug'; import build_debug from 'debug';
function debug (scope: string): build_debug.Debugger { function debug (scope: string): build_debug.Debugger {

View File

@ -55,4 +55,4 @@
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
} }
} }

View File

@ -19,46 +19,33 @@ describe ('redis', () => {
clock_setup (); clock_setup ();
}); });
let iat1 = 0;
let iat2 = 0;
let k1 = '';
let k2 = '';
let i1 = '';
let i2 = '';
afterAll (() => clock_finalize ()); afterAll (() => clock_finalize ());
it ('should generate two keys', async () => { it ('should write and read all keys', async () => {
iat1 = (new Date) const iat1 = (new Date)
.getTime () / 1000; .getTime () / 1000;
await ks.get_sign_key (iat1, frame); await ks.get_sign_key (iat1, frame);
k1 = await ks.get_key (iat1); const k1 = await ks.get_key (iat1);
jasmine.clock () jasmine.clock ()
.tick (frame * 1000); .tick (frame * 1000);
iat2 = (new Date) const iat2 = (new Date)
.getTime () / 1000; .getTime () / 1000;
await ks.get_sign_key (iat2, frame); await ks.get_sign_key (iat2, frame);
k2 = await ks.get_key (iat2); const k2 = await ks.get_key (iat2);
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
i1 = ks['get_index'] (iat1); const index1 = ks['get_index'] (iat1);
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
i2 = ks['get_index'] (iat2); const index2 = ks['get_index'] (iat2);
});
it ('should have two keys in redis', async () => {
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
expect (JSON.parse (await redis['_redis'] expect (JSON.parse (await redis['_redis']?.get (index1) as string).key)
?.get (`keystore_${i1}`) as string).key)
.toEqual (k1); .toEqual (k1);
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
expect (JSON.parse (await redis['_redis'] expect (JSON.parse (await redis['_redis']?.get (index2) as string).key)
?.get (`keystore_${i2}`) as string).key)
.toEqual (k2); .toEqual (k2);
});
it ('should read two keys with a new instance', async () => {
const old_instance = ks.instance_id; const old_instance = ks.instance_id;
ks.reset_instance (); ks.reset_instance ();
expectAsync (ks.get_key (iat1, old_instance)) expectAsync (ks.get_key (iat1, old_instance))

6580
yarn.lock

File diff suppressed because it is too large Load Diff