Timo Hocker
b58af27719
All checks were successful
continuous-integration/drone/push Build is passing
211 lines
5.7 KiB
TypeScript
211 lines
5.7 KiB
TypeScript
/*
|
|
* 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>, December 2020
|
|
*/
|
|
|
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
import { run_regex } from '@sapphirecode/utilities';
|
|
import authority from './Authority';
|
|
import { AuthRequest, AccessSettings } from './AuthHandler';
|
|
import { debug } from './debug';
|
|
|
|
const logger = debug ('gateway');
|
|
|
|
type AnyFunc = (...args: unknown[]) => unknown;
|
|
type Gateway = (
|
|
req: IncomingMessage,
|
|
res: ServerResponse, next: AnyFunc
|
|
) => unknown;
|
|
|
|
interface RefreshSettings extends AccessSettings {
|
|
leave_open?: never;
|
|
redirect_to?: never;
|
|
data?: never;
|
|
}
|
|
|
|
interface GatewayOptions {
|
|
redirect_url?: string;
|
|
cookie_name?: string;
|
|
refresh_cookie_name?: string;
|
|
refresh_settings?: RefreshSettings;
|
|
}
|
|
|
|
interface AuthCookies {
|
|
access_cookie: string | null;
|
|
refresh_cookie: string | null;
|
|
}
|
|
|
|
class GatewayClass {
|
|
private _options: GatewayOptions;
|
|
|
|
public constructor (options: GatewayOptions = {}) {
|
|
logger ('creating new gateway');
|
|
if (
|
|
typeof options.cookie_name === 'string'
|
|
&& options.cookie_name === options.refresh_cookie_name
|
|
)
|
|
throw new Error ('access and refresh cookies cannot have the same name');
|
|
|
|
this._options = options;
|
|
}
|
|
|
|
public deny (res: ServerResponse): void {
|
|
logger ('denied http request');
|
|
res.statusCode = 403;
|
|
res.end ();
|
|
}
|
|
|
|
public redirect (res: ServerResponse): void {
|
|
logger ('redirecting http request to %s', this._options.redirect_url);
|
|
if (typeof this._options.redirect_url !== 'string') {
|
|
logger ('no redirect url defined');
|
|
this.deny (res);
|
|
return;
|
|
}
|
|
res.statusCode = 302;
|
|
res.setHeader ('Location', this._options.redirect_url);
|
|
res.end ();
|
|
}
|
|
|
|
public get_header_auth (req: IncomingMessage): string | null {
|
|
logger ('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');
|
|
return auth.groups?.data;
|
|
}
|
|
|
|
public get_cookie_auth (req: IncomingMessage): AuthCookies {
|
|
logger ('extracting tokens from cookies');
|
|
const result: AuthCookies = {
|
|
access_cookie: null,
|
|
refresh_cookie: null
|
|
};
|
|
|
|
const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu;
|
|
|
|
run_regex (
|
|
cookie_regex,
|
|
req.headers.cookie,
|
|
(res: RegExpMatchArray) => {
|
|
logger ('parsing cookie %s', res.groups?.name);
|
|
if (res.groups?.name === this._options.cookie_name)
|
|
result.access_cookie = res.groups?.value as string;
|
|
else if (res.groups?.name === this._options.refresh_cookie_name)
|
|
result.refresh_cookie = res.groups?.value as string;
|
|
}
|
|
);
|
|
|
|
logger ('parsed cookies: %O', result);
|
|
return result;
|
|
}
|
|
|
|
public authenticate (req: IncomingMessage): boolean {
|
|
logger ('authenticating incoming request');
|
|
const cookies = this.get_cookie_auth (req);
|
|
let auth = this.get_header_auth (req);
|
|
if (auth === null)
|
|
auth = cookies.access_cookie;
|
|
if (auth === null) {
|
|
logger ('found no auth token');
|
|
return false;
|
|
}
|
|
|
|
const ver = authority.verify (auth);
|
|
|
|
logger ('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);
|
|
return ver.authorized;
|
|
}
|
|
|
|
public async try_refresh (
|
|
req: IncomingMessage,
|
|
res: ServerResponse
|
|
): Promise<boolean> {
|
|
if (
|
|
typeof this._options.refresh_cookie_name === 'undefined'
|
|
|| typeof this._options.refresh_settings === 'undefined'
|
|
)
|
|
return false;
|
|
|
|
logger ('trying to apply refresh token');
|
|
|
|
const refresh = this.get_cookie_auth (req).refresh_cookie;
|
|
if (refresh === null) {
|
|
logger ('could not find refresh token');
|
|
return false;
|
|
}
|
|
|
|
const ver = 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_name,
|
|
this._options.refresh_cookie_name
|
|
);
|
|
const refresh_result = await auth_request.allow_access ({
|
|
...this._options.refresh_settings,
|
|
data: ver.data,
|
|
leave_open: true
|
|
});
|
|
|
|
logger ('setting connection info');
|
|
const con = req.connection as unknown as Record<string, unknown>;
|
|
con.auth = {
|
|
token_id: refresh_result.access_token_id,
|
|
token_data: this._options.refresh_settings.data
|
|
};
|
|
|
|
logger ('tokens refreshed');
|
|
return true;
|
|
}
|
|
|
|
logger ('refresh token invalid');
|
|
return false;
|
|
}
|
|
|
|
public async process_request (
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
next: AnyFunc
|
|
): Promise<unknown> {
|
|
logger ('processing incoming http request');
|
|
if (this.authenticate (req)) {
|
|
logger ('authentification successful, calling next handler');
|
|
return next ();
|
|
}
|
|
if (await this.try_refresh (req, res)) {
|
|
logger ('refresh successful, calling next handler');
|
|
return next ();
|
|
}
|
|
|
|
logger ('could not verify session, redirecting to auth gateway');
|
|
return this.redirect (res);
|
|
}
|
|
}
|
|
|
|
export default function create_gateway (options: GatewayOptions): Gateway {
|
|
const g = new GatewayClass (options);
|
|
return g.process_request.bind (g);
|
|
}
|
|
|
|
export {
|
|
AnyFunc,
|
|
Gateway,
|
|
GatewayOptions,
|
|
GatewayClass,
|
|
RefreshSettings
|
|
};
|