All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			
		
			
				
	
	
		
			197 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			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
 | |
| };
 |