/* * 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 , 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 = (/(?\w+) (?.*)/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*(?[^;=]+)=(?[^;]+)/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; 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 { 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; 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 { 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 };