Compare commits
2 Commits
d5c136790e
...
e80e3f9a94
Author | SHA1 | Date | |
---|---|---|---|
e80e3f9a94 | |||
b7514941f0 |
@ -78,7 +78,7 @@ class AuthRequest {
|
||||
this._cookie = cookie;
|
||||
this._refresh_cookie = refresh_cookie;
|
||||
this._is_successful = false;
|
||||
logger ('started processing new auth request');
|
||||
logger.extend ('constructor') ('started processing new auth request');
|
||||
}
|
||||
|
||||
private default_header (set_content = true) {
|
||||
@ -97,7 +97,8 @@ class AuthRequest {
|
||||
data,
|
||||
leave_open
|
||||
}: AccessSettings): Promise<AccessResult> {
|
||||
logger ('allowed access');
|
||||
const log = logger.extend ('allow_access');
|
||||
log ('allowed access');
|
||||
this.default_header (typeof redirect_to !== 'string' && !leave_open);
|
||||
|
||||
const at = await auth.sign (
|
||||
@ -119,7 +120,7 @@ class AuthRequest {
|
||||
cookies.push (build_cookie (this._cookie, at.signature));
|
||||
|
||||
if (include_refresh_token) {
|
||||
logger ('including refresh token');
|
||||
log ('including refresh token');
|
||||
if (typeof refresh_token_expires_in !== 'number')
|
||||
throw new Error ('no expiry time defined for refresh tokens');
|
||||
const rt = await auth.sign (
|
||||
@ -136,7 +137,7 @@ class AuthRequest {
|
||||
}
|
||||
|
||||
if (cookies.length > 0) {
|
||||
logger ('sending %d cookies', cookies.length);
|
||||
log ('sending %d cookies', cookies.length);
|
||||
this.response.setHeader (
|
||||
'Set-Cookie',
|
||||
cookies
|
||||
@ -146,7 +147,7 @@ class AuthRequest {
|
||||
this._is_successful = true;
|
||||
|
||||
if (typeof redirect_to === 'string') {
|
||||
logger ('redirecting to %s', redirect_to);
|
||||
log ('redirecting to %s', redirect_to);
|
||||
this.response.setHeader ('Location', redirect_to);
|
||||
this.response.statusCode = 302;
|
||||
if (!leave_open)
|
||||
@ -155,7 +156,7 @@ class AuthRequest {
|
||||
}
|
||||
|
||||
if (!leave_open) {
|
||||
logger ('finishing http request');
|
||||
log ('finishing http request');
|
||||
this.response.writeHead (200);
|
||||
this.response.end (JSON.stringify (res));
|
||||
}
|
||||
@ -169,7 +170,8 @@ class AuthRequest {
|
||||
data?: Record<string, unknown>,
|
||||
leave_open = false
|
||||
): Promise<string> {
|
||||
logger ('allowed part token');
|
||||
const log = logger.extend ('allow_part');
|
||||
log ('allowed part token');
|
||||
this.default_header ();
|
||||
|
||||
const pt = await auth.sign (
|
||||
@ -185,7 +187,7 @@ class AuthRequest {
|
||||
};
|
||||
|
||||
if (!leave_open) {
|
||||
logger ('finishing http request');
|
||||
log ('finishing http request');
|
||||
this.response.writeHead (200);
|
||||
this.response.end (JSON.stringify (res));
|
||||
}
|
||||
@ -195,11 +197,12 @@ class AuthRequest {
|
||||
}
|
||||
|
||||
public invalid (error_description?: string, leave_open = false): void {
|
||||
logger ('rejecting invalid request');
|
||||
const log = logger.extend ('invalid');
|
||||
log ('rejecting invalid request');
|
||||
this.default_header ();
|
||||
this.response.statusCode = 400;
|
||||
if (!leave_open) {
|
||||
logger ('finishing http request');
|
||||
log ('finishing http request');
|
||||
this.response.end (JSON.stringify ({
|
||||
error: 'invalid_request',
|
||||
error_description
|
||||
@ -208,11 +211,12 @@ class AuthRequest {
|
||||
}
|
||||
|
||||
public deny (leave_open = false): void {
|
||||
logger ('denied access');
|
||||
const log = logger.extend ('deny');
|
||||
log ('denied access');
|
||||
this.default_header ();
|
||||
this.response.statusCode = 401;
|
||||
if (!leave_open) {
|
||||
logger ('finishing http request');
|
||||
log ('finishing http request');
|
||||
this.response.end (JSON.stringify ({ error: 'invalid_client' }));
|
||||
}
|
||||
}
|
||||
@ -230,18 +234,19 @@ interface CreateHandlerOptions {
|
||||
|
||||
type ProcessRequestOptions = Omit<CreateHandlerOptions, 'parse_body'>
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
// eslint-disable-next-line max-lines-per-function, max-statements
|
||||
async function process_request (
|
||||
request: AuthRequest,
|
||||
token: RegExpExecArray | null,
|
||||
default_handler: AuthRequestHandler,
|
||||
options?: ProcessRequestOptions
|
||||
): Promise<void> {
|
||||
const log = logger.extend ('process_request');
|
||||
if (token === null)
|
||||
return default_handler (request);
|
||||
|
||||
if ((/Basic/ui).test (token?.groups?.type as string)) {
|
||||
logger ('found basic login data');
|
||||
log ('found basic login data');
|
||||
request.is_basic = true;
|
||||
|
||||
let login = token?.groups?.token as string;
|
||||
@ -255,7 +260,7 @@ async function process_request (
|
||||
}
|
||||
|
||||
if ((/Bearer/ui).test (token?.groups?.type as string)) {
|
||||
logger ('found bearer login data');
|
||||
log ('found bearer login data');
|
||||
request.is_bearer = true;
|
||||
request.token = token?.groups?.token;
|
||||
|
||||
@ -264,7 +269,7 @@ async function process_request (
|
||||
if (!token_data.valid)
|
||||
return default_handler (request);
|
||||
|
||||
logger ('bearer token is valid');
|
||||
log ('bearer token is valid');
|
||||
|
||||
request.token_data = token_data.data;
|
||||
request.token_id = token_data.id;
|
||||
@ -274,7 +279,7 @@ async function process_request (
|
||||
&& typeof options.refresh !== 'undefined'
|
||||
&& token_data.type === 'refresh_token'
|
||||
) {
|
||||
logger ('found refresh token, emitting new access token');
|
||||
log ('found refresh token, emitting new access token');
|
||||
request.allow_access (options.refresh);
|
||||
return Promise.resolve ();
|
||||
}
|
||||
@ -287,7 +292,7 @@ async function process_request (
|
||||
&& Object.keys (options.modules)
|
||||
.includes (token_data.next_module)
|
||||
) {
|
||||
logger ('processing module %s', token_data.next_module);
|
||||
log ('processing module %s', token_data.next_module);
|
||||
return options.modules[token_data.next_module] (request);
|
||||
}
|
||||
|
||||
@ -295,7 +300,7 @@ async function process_request (
|
||||
return Promise.resolve ();
|
||||
}
|
||||
|
||||
logger ('no matching login method, triggering default handler');
|
||||
log ('no matching login method, triggering default handler');
|
||||
return default_handler (request);
|
||||
}
|
||||
|
||||
@ -304,7 +309,7 @@ export default function create_auth_handler (
|
||||
default_handler: AuthRequestHandler,
|
||||
options?: CreateHandlerOptions
|
||||
): AuthHandler {
|
||||
logger ('creating new auth handler');
|
||||
logger.extend ('create_auth_handler') ('creating new auth handler');
|
||||
if (
|
||||
typeof options?.cookie !== 'undefined'
|
||||
&& typeof options?.refresh_cookie !== 'undefined'
|
||||
|
@ -42,7 +42,8 @@ interface SignatureOptions
|
||||
|
||||
class Authority {
|
||||
public async verify (key: string): Promise<VerificationResult> {
|
||||
logger ('verifying token');
|
||||
const log = logger.extend ('verify');
|
||||
log ('verifying token');
|
||||
const result: VerificationResult = {
|
||||
authorized: false,
|
||||
valid: false,
|
||||
@ -63,7 +64,7 @@ class Authority {
|
||||
);
|
||||
|
||||
if (data === null) {
|
||||
logger ('token invalid');
|
||||
log ('token invalid');
|
||||
result.error = 'invalid signature';
|
||||
return result;
|
||||
}
|
||||
@ -71,10 +72,10 @@ class Authority {
|
||||
result.id = data.id;
|
||||
result.type = data.type;
|
||||
|
||||
logger ('parsing token %s %s', result.type, result.id);
|
||||
log ('parsing token %s %s', result.type, result.id);
|
||||
|
||||
if (!blacklist.is_valid (data.id)) {
|
||||
logger ('token is blacklisted');
|
||||
log ('token is blacklisted');
|
||||
result.error = 'blacklisted';
|
||||
return result;
|
||||
}
|
||||
@ -84,7 +85,7 @@ class Authority {
|
||||
result.next_module = data.next_module;
|
||||
result.data = data.obj;
|
||||
|
||||
logger (
|
||||
log (
|
||||
'valid %s; targeting module %s',
|
||||
result.type,
|
||||
result.next_module
|
||||
@ -98,7 +99,8 @@ class Authority {
|
||||
valid_for: number,
|
||||
options?: SignatureOptions
|
||||
): Promise<SignatureResult> {
|
||||
logger ('signing new %s', type);
|
||||
const log = logger.extend ('sign');
|
||||
log ('signing new %s', type);
|
||||
const time = Date.now ();
|
||||
const key = await keystore.get_sign_key (time / 1000, valid_for);
|
||||
const attributes = {
|
||||
@ -110,7 +112,7 @@ class Authority {
|
||||
next_module: options?.next_module
|
||||
};
|
||||
const signature = sign_object (options?.data, key, attributes);
|
||||
logger ('created token %s', attributes.id);
|
||||
log ('created token %s', attributes.id);
|
||||
return { id: attributes.id, signature };
|
||||
}
|
||||
}
|
||||
|
@ -22,46 +22,58 @@ class Blacklist {
|
||||
}
|
||||
|
||||
public clear (before: number = Number.POSITIVE_INFINITY): void {
|
||||
logger ('clearing blacklist');
|
||||
logger.extend ('clear') ('clearing blacklist');
|
||||
for (let i = this._signatures.length - 1; i >= 0; i--) {
|
||||
if (this._signatures[i].iat < before)
|
||||
this._signatures.splice (i, 1);
|
||||
this.remove_signature (i);
|
||||
}
|
||||
}
|
||||
|
||||
public add_signature (hash: string): void {
|
||||
logger ('blacklisting signature %s', hash);
|
||||
logger.extend ('add_signature') ('blacklisting signature %s', hash);
|
||||
this._signatures.push ({ iat: Date.now (), hash });
|
||||
}
|
||||
|
||||
public remove_signature (hash: string): void {
|
||||
logger ('removing signature from blacklist %s', hash);
|
||||
for (let i = this._signatures.length - 1; i >= 0; i--) {
|
||||
if (this._signatures[i].hash === hash)
|
||||
this._signatures.splice (i, 1);
|
||||
public remove_signature (signature: number | string): void {
|
||||
const log = logger.extend ('remove_signature');
|
||||
log ('removing signature from blacklist %s', signature);
|
||||
if (typeof signature === 'string') {
|
||||
log ('received string, searching through signatures');
|
||||
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 {
|
||||
logger ('checking signature for blacklist entry %s', hash);
|
||||
const log = logger.extend ('is_valid');
|
||||
log ('checking signature for blacklist entry %s', hash);
|
||||
for (const sig of this._signatures) {
|
||||
if (sig.hash === hash) {
|
||||
logger ('found matching blacklist entry');
|
||||
log ('found matching blacklist entry');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
logger ('signature is not blacklisted');
|
||||
log ('signature is not blacklisted');
|
||||
return true;
|
||||
}
|
||||
|
||||
public export_blacklist (): Signature[] {
|
||||
logger ('exporting blacklist');
|
||||
logger.extend ('export_blacklist') ('exporting blacklist');
|
||||
return this._signatures;
|
||||
}
|
||||
|
||||
public import_blacklist (data: Signature[]): void {
|
||||
logger ('importing %d blacklist entries', data.length);
|
||||
logger.extend ('import_blacklist') (
|
||||
'importing %d blacklist entries',
|
||||
data.length
|
||||
);
|
||||
this._signatures.push (...data);
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,8 @@ class GatewayClass {
|
||||
private _options: GatewayOptions;
|
||||
|
||||
public constructor (options: GatewayOptions = {}) {
|
||||
logger ('creating new gateway');
|
||||
const log = logger.extend ('constructor');
|
||||
log ('creating new gateway');
|
||||
if (
|
||||
typeof options?.cookie !== 'undefined'
|
||||
&& typeof options?.refresh_cookie !== 'undefined'
|
||||
@ -50,15 +51,16 @@ class GatewayClass {
|
||||
}
|
||||
|
||||
public deny (res: ServerResponse): void {
|
||||
logger ('denied http request');
|
||||
logger.extend ('deny') ('denied http request');
|
||||
res.statusCode = 403;
|
||||
res.end ();
|
||||
}
|
||||
|
||||
public redirect (res: ServerResponse): void {
|
||||
logger ('redirecting http request to %s', this._options.redirect_url);
|
||||
const log = logger.extend ('redirect');
|
||||
log ('redirecting http request to %s', this._options.redirect_url);
|
||||
if (typeof this._options.redirect_url !== 'string') {
|
||||
logger ('no redirect url defined');
|
||||
log ('no redirect url defined');
|
||||
this.deny (res);
|
||||
return;
|
||||
}
|
||||
@ -68,34 +70,36 @@ class GatewayClass {
|
||||
}
|
||||
|
||||
public get_header_auth (req: IncomingMessage): string | null {
|
||||
logger ('extracting authorization header');
|
||||
const log = logger.extend ('get_header_auth');
|
||||
log ('extracting authorization header');
|
||||
const auth_header = req.headers.authorization;
|
||||
const auth = (/(?<type>\w+) (?<data>.*)/u).exec (auth_header || '');
|
||||
if (auth === null)
|
||||
return null;
|
||||
if (auth.groups?.type !== 'Bearer')
|
||||
return null;
|
||||
logger ('found bearer token');
|
||||
log ('found bearer token');
|
||||
return auth.groups?.data;
|
||||
}
|
||||
|
||||
public async try_access (req: IncomingMessage): Promise<boolean> {
|
||||
logger ('authenticating incoming request');
|
||||
const log = logger.extend ('try_access');
|
||||
log ('authenticating incoming request');
|
||||
let auth = this.get_header_auth (req);
|
||||
if (auth === null)
|
||||
auth = extract_cookie (this._options.cookie?.name, req.headers.cookie);
|
||||
if (auth === null) {
|
||||
logger ('found no auth token');
|
||||
log ('found no auth token');
|
||||
return false;
|
||||
}
|
||||
|
||||
const ver = await authority.verify (auth);
|
||||
|
||||
logger ('setting connection info');
|
||||
log ('setting connection info');
|
||||
const con = req.connection as unknown as Record<string, unknown>;
|
||||
con.auth = { token_id: ver.id, token_data: ver.data };
|
||||
|
||||
logger ('token valid: %s', ver.authorized);
|
||||
log ('token valid: %s', ver.authorized);
|
||||
return ver.authorized;
|
||||
}
|
||||
|
||||
@ -103,26 +107,27 @@ class GatewayClass {
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse
|
||||
): Promise<boolean> {
|
||||
const log = logger.extend ('try_refresh');
|
||||
if (
|
||||
typeof this._options.refresh_cookie === 'undefined'
|
||||
|| typeof this._options.refresh_settings === 'undefined'
|
||||
)
|
||||
return false;
|
||||
|
||||
logger ('trying to apply refresh token');
|
||||
log ('trying to apply refresh token');
|
||||
|
||||
const refresh = extract_cookie (
|
||||
this._options.refresh_cookie.name,
|
||||
req.headers.cookie
|
||||
);
|
||||
if (refresh === null) {
|
||||
logger ('could not find refresh token');
|
||||
log ('could not find refresh token');
|
||||
return false;
|
||||
}
|
||||
|
||||
const ver = await authority.verify (refresh);
|
||||
if (ver.type === 'refresh_token' && ver.valid) {
|
||||
logger ('refresh token valid, generating new tokens');
|
||||
log ('refresh token valid, generating new tokens');
|
||||
const auth_request = new AuthRequest (
|
||||
req,
|
||||
res,
|
||||
@ -136,18 +141,18 @@ class GatewayClass {
|
||||
leave_open: true
|
||||
});
|
||||
|
||||
logger ('setting connection info');
|
||||
log ('setting connection info');
|
||||
const con = req.connection as unknown as Record<string, unknown>;
|
||||
con.auth = {
|
||||
token_id: refresh_result.access_token_id,
|
||||
token_data: ver.data
|
||||
};
|
||||
|
||||
logger ('tokens refreshed');
|
||||
log ('tokens refreshed');
|
||||
return true;
|
||||
}
|
||||
|
||||
logger ('refresh token invalid');
|
||||
log ('refresh token invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -155,17 +160,18 @@ class GatewayClass {
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse
|
||||
): Promise<boolean> {
|
||||
logger ('trying to authenticate http request');
|
||||
const log = logger.extend ('authenticate');
|
||||
log ('trying to authenticate http request');
|
||||
if (await this.try_access (req)) {
|
||||
logger ('authenticated via access_token');
|
||||
log ('authenticated via access_token');
|
||||
return true;
|
||||
}
|
||||
if (await this.try_refresh (req, res)) {
|
||||
logger ('authenticated via refresh_token');
|
||||
log ('authenticated via refresh_token');
|
||||
return true;
|
||||
}
|
||||
|
||||
logger ('could not verify session');
|
||||
log ('could not verify session');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -174,19 +180,20 @@ class GatewayClass {
|
||||
res: ServerResponse,
|
||||
next: AnyFunc
|
||||
): Promise<unknown> {
|
||||
logger ('processing incoming http request');
|
||||
const log = logger.extend ('process_request');
|
||||
log ('processing incoming http request');
|
||||
if (await this.authenticate (req, res)) {
|
||||
logger ('authentification successful, calling next handler');
|
||||
log ('authentification successful, calling next handler');
|
||||
return next ();
|
||||
}
|
||||
|
||||
logger ('failed to authenticate, redirecting client');
|
||||
log ('failed to authenticate, redirecting client');
|
||||
return this.redirect (res);
|
||||
}
|
||||
|
||||
public async logout (req: IncomingMessage): Promise<void> {
|
||||
const l = logger.extend ('logout');
|
||||
l ('invalidating all submitted tokens');
|
||||
const log = logger.extend ('logout');
|
||||
log ('invalidating all submitted tokens');
|
||||
const auth_strings = [
|
||||
this.get_header_auth (req),
|
||||
extract_cookie (this._options.cookie?.name, req.headers.cookie),
|
||||
@ -200,12 +207,12 @@ class GatewayClass {
|
||||
)
|
||||
).filter ((v) => v.valid);
|
||||
|
||||
l ('found %d tokens: %O', tokens.length, tokens);
|
||||
log ('found %d tokens: %O', tokens.length, tokens);
|
||||
|
||||
for (const token of tokens)
|
||||
blacklist.add_signature (token.id);
|
||||
|
||||
l ('complete');
|
||||
log ('complete');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* 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 {
|
||||
key: string;
|
||||
valid_until: number;
|
||||
|
@ -10,6 +10,7 @@ import { to_b58 } from '@sapphirecode/encoding-helper';
|
||||
import { debug } from './debug';
|
||||
import { KeyStoreData, KeyStoreExport } from './Key';
|
||||
import { redis } from './Redis';
|
||||
import { redis_key_store } from './RedisData/RedisKeyStore';
|
||||
|
||||
const logger = debug ('keystore');
|
||||
|
||||
@ -30,7 +31,10 @@ class KeyStore {
|
||||
this.garbage_collect ();
|
||||
}, renew_interval);
|
||||
this._instance = to_b58 (random_hex (16), 'hex');
|
||||
logger ('created keystore instance %s', this._instance);
|
||||
logger.extend ('constructor') (
|
||||
'created keystore instance %s',
|
||||
this._instance
|
||||
);
|
||||
}
|
||||
|
||||
private get_index (iat: number, instance = this._instance): string {
|
||||
@ -55,11 +59,12 @@ class KeyStore {
|
||||
}
|
||||
};
|
||||
if (this._sync_redis)
|
||||
await redis.set_key ({ ...result.public_key, index });
|
||||
await redis_key_store.set ({ ...result.public_key, index });
|
||||
this._keys[index] = result;
|
||||
}
|
||||
|
||||
private garbage_collect (): void {
|
||||
const log = logger.extend ('garbage_collect');
|
||||
const time = (new Date)
|
||||
.getTime ();
|
||||
const keys = Object.keys (this._keys);
|
||||
@ -68,12 +73,12 @@ class KeyStore {
|
||||
if (typeof entry.private_key !== 'undefined'
|
||||
&& entry.private_key.valid_until < time
|
||||
) {
|
||||
logger ('deleting expired private key');
|
||||
log ('deleting expired private key');
|
||||
delete entry.private_key;
|
||||
}
|
||||
|
||||
if (entry.public_key.valid_until < time) {
|
||||
logger ('deleting expired key pair');
|
||||
log ('deleting expired key pair');
|
||||
delete this._keys[index];
|
||||
}
|
||||
}
|
||||
@ -84,7 +89,8 @@ class KeyStore {
|
||||
valid_for: number,
|
||||
instance?: string
|
||||
): Promise<string> {
|
||||
logger (
|
||||
const log = logger.extend ('get_sign_key');
|
||||
log (
|
||||
'querying key from %s for timestamp %d, valid for %d',
|
||||
instance,
|
||||
iat,
|
||||
@ -103,34 +109,35 @@ class KeyStore {
|
||||
.getTime () + (valid_for * 1000);
|
||||
|
||||
if (typeof this._keys[index] !== 'undefined') {
|
||||
logger ('loading existing key');
|
||||
log ('loading existing key');
|
||||
const key = this._keys[index];
|
||||
|
||||
if (typeof key.private_key === 'undefined')
|
||||
throw new Error ('cannot access already expired keys');
|
||||
|
||||
if (key.public_key.valid_until < valid_until) {
|
||||
logger ('updating key valid timespan to match new value');
|
||||
log ('updating key valid timespan to match new value');
|
||||
key.public_key.valid_until = valid_until;
|
||||
}
|
||||
|
||||
return key.private_key?.key as string;
|
||||
}
|
||||
|
||||
logger ('key does not exist, creating a new one');
|
||||
log ('key does not exist, creating a new one');
|
||||
await this.create_key (index, valid_for);
|
||||
return this._keys[index].private_key?.key as string;
|
||||
}
|
||||
|
||||
public async get_key (iat: number, instance?: string): Promise<string> {
|
||||
logger ('querying public key from %s for timestamp %d', instance, iat);
|
||||
const log = logger.extend ('get_key');
|
||||
log ('querying public key from %s for timestamp %d', instance, iat);
|
||||
const index = this.get_index (iat, instance);
|
||||
|
||||
let key = null;
|
||||
|
||||
if (typeof this._keys[index] === 'undefined') {
|
||||
if (this._sync_redis)
|
||||
key = await redis.get_key (index);
|
||||
key = await redis_key_store.get (index);
|
||||
}
|
||||
else { key = this._keys[index].public_key; }
|
||||
|
||||
@ -141,18 +148,23 @@ class KeyStore {
|
||||
}
|
||||
|
||||
public export_verification_data (): KeyStoreExport {
|
||||
logger ('exporting public keys');
|
||||
const log = logger.extend ('export_verification_data');
|
||||
log ('exporting public keys');
|
||||
log ('cleaning up before export');
|
||||
this.garbage_collect ();
|
||||
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 });
|
||||
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public import_verification_data (data: KeyStoreExport): void {
|
||||
logger ('importing %d public keys', data.length);
|
||||
const log = logger.extend ('import_verification_data');
|
||||
log ('importing %d public keys', data.length);
|
||||
for (const key of data) {
|
||||
log ('importing key %s', key.index);
|
||||
if (typeof this._keys[key.index] !== 'undefined')
|
||||
throw new Error ('cannot import to the same instance');
|
||||
this._keys[key.index] = {
|
||||
@ -162,11 +174,12 @@ class KeyStore {
|
||||
}
|
||||
};
|
||||
}
|
||||
log ('running garbage collector');
|
||||
this.garbage_collect ();
|
||||
}
|
||||
|
||||
public reset_instance (): void {
|
||||
logger ('resetting keystore');
|
||||
logger.extend ('reset_instance') ('resetting keystore');
|
||||
this._instance = to_b58 (random_hex (16), 'hex');
|
||||
this._keys = {};
|
||||
this._sync_redis = false;
|
||||
|
48
lib/Redis.ts
48
lib/Redis.ts
@ -7,13 +7,9 @@
|
||||
|
||||
import IORedis from 'ioredis';
|
||||
import { debug } from './debug';
|
||||
import { LabelledKey } from './Key';
|
||||
|
||||
const logger = debug ('redis');
|
||||
|
||||
export type SyncClass = 'blacklist' | 'keystore'
|
||||
export type SyncValue = LabelledKey | string;
|
||||
|
||||
export class Redis {
|
||||
private _redis: IORedis | null = null;
|
||||
|
||||
@ -55,47 +51,15 @@ export class Redis {
|
||||
log ('done');
|
||||
}
|
||||
|
||||
public async set (
|
||||
sync_class: SyncClass,
|
||||
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));
|
||||
public get redis (): IORedis {
|
||||
if (this._redis === null)
|
||||
throw new Error ('redis is not connected');
|
||||
|
||||
log ('saved value');
|
||||
return this._redis;
|
||||
}
|
||||
|
||||
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);
|
||||
public get is_active (): boolean {
|
||||
return this._redis !== null;
|
||||
}
|
||||
}
|
||||
|
||||
|
36
lib/RedisData/RedisBlacklistStore.ts
Normal file
36
lib/RedisData/RedisBlacklistStore.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
52
lib/RedisData/RedisKeyStore.ts
Normal file
52
lib/RedisData/RedisKeyStore.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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;
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* 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 { debug } from './debug';
|
||||
|
||||
@ -54,7 +61,8 @@ function extract_cookie (
|
||||
name: string|undefined,
|
||||
header: string|undefined
|
||||
): string| null {
|
||||
logger (`extracting cookie ${name}`);
|
||||
const log = logger.extend ('extract_cookie');
|
||||
log (`extracting cookie ${name}`);
|
||||
|
||||
const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu;
|
||||
|
||||
@ -64,9 +72,9 @@ function extract_cookie (
|
||||
cookie_regex,
|
||||
header,
|
||||
(res: RegExpMatchArray) => {
|
||||
logger ('parsing cookie %s', res.groups?.name);
|
||||
log ('parsing cookie %s', res.groups?.name);
|
||||
if (res.groups?.name === name) {
|
||||
logger ('found cookie');
|
||||
log ('found cookie');
|
||||
result = res.groups?.value as string;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
function debug (scope: string): build_debug.Debugger {
|
||||
|
@ -55,4 +55,4 @@
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -19,33 +19,46 @@ describe ('redis', () => {
|
||||
clock_setup ();
|
||||
});
|
||||
|
||||
let iat1 = 0;
|
||||
let iat2 = 0;
|
||||
let k1 = '';
|
||||
let k2 = '';
|
||||
let i1 = '';
|
||||
let i2 = '';
|
||||
|
||||
afterAll (() => clock_finalize ());
|
||||
|
||||
it ('should write and read all keys', async () => {
|
||||
const iat1 = (new Date)
|
||||
it ('should generate two keys', async () => {
|
||||
iat1 = (new Date)
|
||||
.getTime () / 1000;
|
||||
await ks.get_sign_key (iat1, frame);
|
||||
const k1 = await ks.get_key (iat1);
|
||||
k1 = await ks.get_key (iat1);
|
||||
|
||||
jasmine.clock ()
|
||||
.tick (frame * 1000);
|
||||
|
||||
const iat2 = (new Date)
|
||||
iat2 = (new Date)
|
||||
.getTime () / 1000;
|
||||
await ks.get_sign_key (iat2, frame);
|
||||
const k2 = await ks.get_key (iat2);
|
||||
k2 = await ks.get_key (iat2);
|
||||
// eslint-disable-next-line dot-notation
|
||||
const index1 = ks['get_index'] (iat1);
|
||||
i1 = ks['get_index'] (iat1);
|
||||
// eslint-disable-next-line dot-notation
|
||||
const index2 = ks['get_index'] (iat2);
|
||||
i2 = ks['get_index'] (iat2);
|
||||
});
|
||||
|
||||
it ('should have two keys in redis', async () => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect (JSON.parse (await redis['_redis']?.get (index1) as string).key)
|
||||
expect (JSON.parse (await redis['_redis']
|
||||
?.get (`keystore_${i1}`) as string).key)
|
||||
.toEqual (k1);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect (JSON.parse (await redis['_redis']?.get (index2) as string).key)
|
||||
expect (JSON.parse (await redis['_redis']
|
||||
?.get (`keystore_${i2}`) as string).key)
|
||||
.toEqual (k2);
|
||||
});
|
||||
|
||||
it ('should read two keys with a new instance', async () => {
|
||||
const old_instance = ks.instance_id;
|
||||
ks.reset_instance ();
|
||||
expectAsync (ks.get_key (iat1, old_instance))
|
||||
|
Loading…
x
Reference in New Issue
Block a user