/* * 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 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 = (/(?\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 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; 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 = 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; 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 { 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 { 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 };