/* * 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, CookieSettings } from './cookie'; import blacklist from './Blacklist'; 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?: CookieSettings; refresh_cookie?: CookieSettings; refresh_settings?: RefreshSettings; require_permissions?: string[]; } interface ConnectionInfo { token_id: string token_data: unknown permissions: string[] } class GatewayClass { private _options: GatewayOptions; public constructor (options: GatewayOptions = {}) { const log = logger.extend ('constructor'); log ('creating new gateway'); if ( typeof options?.cookie !== 'undefined' && typeof options?.refresh_cookie !== 'undefined' && 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.extend ('deny') ('denied http request'); res.statusCode = 403; res.end (); } public redirect (res: ServerResponse): void { const log = logger.extend ('redirect'); log ('redirecting http request to %s', this._options.redirect_url); if (typeof this._options.redirect_url !== 'string') { log ('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 { const log = logger.extend ('get_header_auth'); log ('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; log ('found bearer token'); return auth.groups?.data; } public async try_access (req: IncomingMessage): Promise { const log = logger.extend ('try_access'); log ('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) { log ('found no auth token'); return false; } const ver = await authority.verify (auth); log ('setting connection info'); const con = req.connection as unknown as Record; con.auth = { token_id: ver.id, token_data: ver.data, permissions: ver.permissions }; log ('token valid: %s', ver.authorized); return ver.authorized; } public async try_refresh ( req: IncomingMessage, res: ServerResponse ): Promise { const log = logger.extend ('try_refresh'); if ( typeof this._options.refresh_cookie === 'undefined' || typeof this._options.refresh_settings === 'undefined' ) return false; log ('trying to apply refresh token'); const refresh = extract_cookie ( this._options.refresh_cookie.name, req.headers.cookie ); if (refresh === null) { log ('could not find refresh token'); return false; } const ver = await authority.verify (refresh); if (ver.type === 'refresh_token' && ver.valid) { log ('refresh token valid, generating new tokens'); const auth_request = new AuthRequest ( req, res, '', this._options.cookie, this._options.refresh_cookie ); const refresh_result = await auth_request.allow_access ({ ...this._options.refresh_settings, data: ver.data, leave_open: true }); log ('setting connection info'); const con = req.connection as unknown as Record; con.auth = { token_id: refresh_result.access_token_id, token_data: ver.data, permissions: ver.permissions }; log ('tokens refreshed'); return true; } log ('refresh token invalid'); return false; } public async authenticate ( req: IncomingMessage, res: ServerResponse ): Promise { const log = logger.extend ('authenticate'); log ('trying to authenticate http request'); if (await this.try_access (req)) { log ('authenticated via access_token'); return true; } if (await this.try_refresh (req, res)) { log ('authenticated via refresh_token'); return true; } log ('could not verify session'); return false; } 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); } public async process_request ( req: IncomingMessage, res: ServerResponse, next: AnyFunc ): Promise { const log = logger.extend ('process_request'); log ('processing incoming http request'); if (await this.authenticate (req, res)) { log ('authentification successful'); log ('checking permissions'); if (!this.check_permissions (req)) return this.redirect (res); log ('authorization successful. calling next handler'); return next (); } log ('failed to authenticate, redirecting client'); return this.redirect (res); } public async logout (req: IncomingMessage): Promise { const log = logger.extend ('logout'); log ('invalidating all submitted tokens'); 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) ]; const tokens = ( await Promise.all ( auth_strings .filter ((v) => v !== null) .map ((v) => authority.verify (v as string)) ) ).filter ((v) => v.valid); log ('found %d tokens: %O', tokens.length, tokens); for (const token of tokens) { // eslint-disable-next-line no-await-in-loop await blacklist.add_signature (token.id); } log ('complete'); } public get_info (req: IncomingMessage): ConnectionInfo { const conn = req.connection as unknown as Record; const auth = conn.auth as Record; return { token_id: auth.token_id as string, token_data: auth.token_data, permissions: (auth.permissions as string[]) || [] }; } } 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 };