auth-server-helper/lib/Gateway.ts
Timo Hocker 3aaaf10fd9
All checks were successful
continuous-integration/drone/push Build is passing
improved cookie security
2022-01-08 22:10:21 +01:00

197 lines
5.2 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 authority from './Authority';
import { AuthRequest, AccessSettings } from './AuthHandler';
import { debug } from './debug';
import { extract_cookie } from './cookie';
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;
}
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 try_access (req: IncomingMessage): boolean {
logger ('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');
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 = extract_cookie (
this._options.refresh_cookie_name,
req.headers.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 authenticate (
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> {
logger ('trying to authenticate http request');
if (this.try_access (req)) {
logger ('authenticated via access_token');
return true;
}
if (await this.try_refresh (req, res)) {
logger ('authenticated via refresh_token');
return true;
}
logger ('could not verify session');
return false;
}
public async process_request (
req: IncomingMessage,
res: ServerResponse,
next: AnyFunc
): Promise<unknown> {
logger ('processing incoming http request');
if (await this.authenticate (req, res)) {
logger ('authentification successful, calling next handler');
return next ();
}
logger ('failed to authenticate, redirecting client');
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
};