auth-server-helper/lib/Gateway.ts

271 lines
7.5 KiB
TypeScript
Raw Normal View History

/*
* 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
*/
2020-12-28 16:53:08 +01:00
import { IncomingMessage, ServerResponse } from 'http';
2020-12-19 15:40:49 +01:00
import authority from './Authority';
2022-01-04 21:32:04 +01:00
import { AuthRequest, AccessSettings } from './AuthHandler';
2022-01-05 12:32:04 +01:00
import { debug } from './debug';
2022-01-10 10:06:54 +01:00
import { extract_cookie, CookieSettings } from './cookie';
2022-05-02 13:30:09 +02:00
import blacklist from './Blacklist';
2022-01-05 12:32:04 +01:00
const logger = debug ('gateway');
2020-12-12 15:53:47 +01:00
2020-12-28 16:53:08 +01:00
type AnyFunc = (...args: unknown[]) => unknown;
type Gateway = (
req: IncomingMessage,
2022-08-08 15:52:56 +02:00
res: ServerResponse,
next: AnyFunc
2020-12-28 16:53:08 +01:00
) => unknown;
2020-12-06 15:51:59 +01:00
2022-01-04 21:32:04 +01:00
interface RefreshSettings extends AccessSettings {
leave_open?: never;
redirect_to?: never;
2022-01-05 08:11:18 +01:00
data?: never;
2022-01-04 21:32:04 +01:00
}
2020-12-06 15:51:59 +01:00
interface GatewayOptions {
redirect_url?: string;
2022-01-10 10:06:54 +01:00
cookie?: CookieSettings;
refresh_cookie?: CookieSettings;
2022-01-04 21:32:04 +01:00
refresh_settings?: RefreshSettings;
2022-09-12 12:29:43 +02:00
require_permissions?: string[];
}
interface ConnectionInfo {
token_id: string
token_data: unknown
permissions: string[]
2022-01-04 21:32:04 +01:00
}
2020-12-06 15:51:59 +01:00
class GatewayClass {
2020-12-06 21:06:40 +01:00
private _options: GatewayOptions;
2020-12-06 15:51:59 +01:00
public constructor (options: GatewayOptions = {}) {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('constructor');
log ('creating new gateway');
2022-01-04 21:32:04 +01:00
if (
2022-01-10 10:06:54 +01:00
typeof options?.cookie !== 'undefined'
&& typeof options?.refresh_cookie !== 'undefined'
&& options.cookie.name === options.refresh_cookie.name
2022-01-04 21:32:04 +01:00
)
throw new Error ('access and refresh cookies cannot have the same name');
2020-12-06 21:06:40 +01:00
this._options = options;
2020-12-06 15:51:59 +01:00
}
public deny (res: ServerResponse): void {
2022-08-15 17:33:25 +02:00
logger.extend ('deny') ('denied http request');
res.statusCode = 403;
2022-01-03 14:46:12 +01:00
res.end ();
}
public redirect (res: ServerResponse): void {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('redirect');
log ('redirecting http request to %s', this._options.redirect_url);
2022-01-03 14:46:12 +01:00
if (typeof this._options.redirect_url !== 'string') {
2022-08-15 17:33:25 +02:00
log ('no redirect url defined');
2022-01-03 14:46:12 +01:00
this.deny (res);
return;
}
2020-12-06 15:51:59 +01:00
res.statusCode = 302;
2020-12-06 21:06:40 +01:00
res.setHeader ('Location', this._options.redirect_url);
2020-12-06 15:51:59 +01:00
res.end ();
}
public get_header_auth (req: IncomingMessage): string | null {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('get_header_auth');
log ('extracting authorization header');
2020-12-28 16:53:08 +01:00
const auth_header = req.headers.authorization;
const auth = (/(?<type>\w+) (?<data>.*)/u).exec (auth_header || '');
2020-12-13 12:26:40 +01:00
if (auth === null)
return null;
2020-12-28 16:53:08 +01:00
if (auth.groups?.type !== 'Bearer')
2020-12-13 12:26:40 +01:00
return null;
2022-08-15 17:33:25 +02:00
log ('found bearer token');
2020-12-28 16:53:08 +01:00
return auth.groups?.data;
2020-12-13 12:26:40 +01:00
}
2022-08-08 15:52:56 +02:00
public async try_access (req: IncomingMessage): Promise<boolean> {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('try_access');
log ('authenticating incoming request');
2020-12-13 12:26:40 +01:00
let auth = this.get_header_auth (req);
if (auth === null)
2022-01-10 10:06:54 +01:00
auth = extract_cookie (this._options.cookie?.name, req.headers.cookie);
2022-01-05 12:32:04 +01:00
if (auth === null) {
2022-08-15 17:33:25 +02:00
log ('found no auth token');
2020-12-12 15:53:47 +01:00
return false;
2022-01-05 12:32:04 +01:00
}
2020-12-12 15:53:47 +01:00
2022-08-08 15:52:56 +02:00
const ver = await authority.verify (auth);
2021-01-03 15:32:29 +01:00
2022-08-15 17:33:25 +02:00
log ('setting connection info');
2021-01-05 15:59:06 +01:00
const con = req.connection as unknown as Record<string, unknown>;
2022-09-12 12:29:43 +02:00
con.auth = {
token_id: ver.id,
token_data: ver.data,
permissions: ver.permissions
};
2021-01-03 15:32:29 +01:00
2022-08-15 17:33:25 +02:00
log ('token valid: %s', ver.authorized);
2021-01-03 15:32:29 +01:00
return ver.authorized;
2020-12-06 15:51:59 +01:00
}
2022-01-04 21:32:04 +01:00
public async try_refresh (
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('try_refresh');
2022-01-04 21:32:04 +01:00
if (
2022-01-10 10:06:54 +01:00
typeof this._options.refresh_cookie === 'undefined'
2022-01-04 21:32:04 +01:00
|| typeof this._options.refresh_settings === 'undefined'
)
return false;
2022-08-15 17:33:25 +02:00
log ('trying to apply refresh token');
2022-01-05 12:32:04 +01:00
2022-01-08 22:10:02 +01:00
const refresh = extract_cookie (
2022-01-10 10:06:54 +01:00
this._options.refresh_cookie.name,
2022-01-08 22:10:02 +01:00
req.headers.cookie
);
2022-01-05 12:32:04 +01:00
if (refresh === null) {
2022-08-15 17:33:25 +02:00
log ('could not find refresh token');
2022-01-04 21:32:04 +01:00
return false;
2022-01-05 12:32:04 +01:00
}
2022-01-04 21:32:04 +01:00
2022-08-08 15:52:56 +02:00
const ver = await authority.verify (refresh);
2022-01-04 21:32:04 +01:00
if (ver.type === 'refresh_token' && ver.valid) {
2022-08-15 17:33:25 +02:00
log ('refresh token valid, generating new tokens');
2022-01-04 21:32:04 +01:00
const auth_request = new AuthRequest (
req,
res,
2022-08-08 15:52:56 +02:00
'',
this._options.cookie,
2022-01-10 10:06:54 +01:00
this._options.refresh_cookie
2022-01-04 21:32:04 +01:00
);
const refresh_result = await auth_request.allow_access ({
...this._options.refresh_settings,
2022-08-08 15:52:56 +02:00
data: ver.data,
leave_open: true
2022-01-04 21:32:04 +01:00
});
2022-08-15 17:33:25 +02:00
log ('setting connection info');
2022-01-04 21:32:04 +01:00
const con = req.connection as unknown as Record<string, unknown>;
con.auth = {
2022-09-12 12:29:43 +02:00
token_id: refresh_result.access_token_id,
token_data: ver.data,
permissions: ver.permissions
2022-01-04 21:32:04 +01:00
};
2022-08-15 17:33:25 +02:00
log ('tokens refreshed');
2022-01-04 21:32:04 +01:00
return true;
}
2022-08-15 17:33:25 +02:00
log ('refresh token invalid');
2022-01-04 21:32:04 +01:00
return false;
}
2022-01-05 12:58:00 +01:00
public async authenticate (
req: IncomingMessage,
res: ServerResponse
2022-01-05 13:01:32 +01:00
): Promise<boolean> {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('authenticate');
log ('trying to authenticate http request');
2022-08-08 15:52:56 +02:00
if (await this.try_access (req)) {
2022-08-15 17:33:25 +02:00
log ('authenticated via access_token');
2022-01-05 12:58:00 +01:00
return true;
}
if (await this.try_refresh (req, res)) {
2022-08-15 17:33:25 +02:00
log ('authenticated via refresh_token');
2022-01-05 12:58:00 +01:00
return true;
}
2022-08-15 17:33:25 +02:00
log ('could not verify session');
2022-01-05 12:58:00 +01:00
return false;
}
2022-09-12 12:29:43 +02:00
public check_permissions (
req: IncomingMessage,
permissions = this._options.require_permissions || []
): boolean {
for (const perm of permissions) {
if (!this.has_permission (req, perm))
return false;
}
return true;
}
public has_permission (req: IncomingMessage, permission: string) {
const info = this.get_info (req);
return info.permissions.includes (permission);
}
2022-01-04 21:32:04 +01:00
public async process_request (
2020-12-28 16:53:08 +01:00
req: IncomingMessage,
res: ServerResponse,
2020-12-06 21:06:40 +01:00
next: AnyFunc
2022-01-04 21:32:04 +01:00
): Promise<unknown> {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('process_request');
log ('processing incoming http request');
2022-01-05 12:58:00 +01:00
if (await this.authenticate (req, res)) {
2022-09-12 12:29:43 +02:00
log ('authentification successful');
log ('checking permissions');
if (!this.check_permissions (req))
return this.redirect (res);
log ('authorization successful. calling next handler');
2020-12-06 21:06:40 +01:00
return next ();
2022-01-05 12:32:04 +01:00
}
2022-08-15 17:33:25 +02:00
log ('failed to authenticate, redirecting client');
2020-12-06 21:06:40 +01:00
return this.redirect (res);
2020-12-06 15:51:59 +01:00
}
2022-05-02 13:30:09 +02:00
2022-08-08 15:52:56 +02:00
public async logout (req: IncomingMessage): Promise<void> {
2022-08-15 17:33:25 +02:00
const log = logger.extend ('logout');
log ('invalidating all submitted tokens');
2022-05-02 13:30:09 +02:00
const auth_strings = [
this.get_header_auth (req),
extract_cookie (this._options.cookie?.name, req.headers.cookie),
extract_cookie (this._options.refresh_cookie?.name, req.headers.cookie)
];
2022-08-08 15:52:56 +02:00
const tokens = (
await Promise.all (
auth_strings
.filter ((v) => v !== null)
.map ((v) => authority.verify (v as string))
)
).filter ((v) => v.valid);
2022-05-02 13:30:09 +02:00
2022-08-15 17:33:25 +02:00
log ('found %d tokens: %O', tokens.length, tokens);
2022-05-02 13:30:09 +02:00
2022-08-27 16:39:07 +02:00
for (const token of tokens) {
// eslint-disable-next-line no-await-in-loop
await blacklist.add_signature (token.id);
}
2022-05-02 13:30:09 +02:00
2022-08-15 17:33:25 +02:00
log ('complete');
2022-05-02 13:30:09 +02:00
}
2022-09-12 12:29:43 +02:00
public get_info (req: IncomingMessage): ConnectionInfo {
const conn = req.connection as unknown as Record<string, unknown>;
const auth = conn.auth as Record<string, unknown>;
return {
token_id: auth.token_id as string,
token_data: auth.token_data,
permissions: (auth.permissions as string[]) || []
};
}
2020-12-06 15:51:59 +01:00
}
export default function create_gateway (options: GatewayOptions): Gateway {
const g = new GatewayClass (options);
2020-12-28 16:53:08 +01:00
return g.process_request.bind (g);
2020-12-06 15:51:59 +01:00
}
2020-12-28 16:53:08 +01:00
2022-08-08 15:52:56 +02:00
export { AnyFunc, Gateway, GatewayOptions, GatewayClass, RefreshSettings };