/* * 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 , January 2021 */ import { IncomingMessage, ServerResponse } from 'http'; import { to_utf8 } from '@sapphirecode/encoding-helper'; import auth from './Authority'; import { debug } from './debug'; import { build_cookie, CookieSettings } from './cookie'; const logger = debug ('auth'); interface AccessSettings { access_token_expires_in: number include_refresh_token?: boolean refresh_token_expires_in?: number redirect_to?: string data?: unknown, leave_open?: boolean permissions?: string[] } interface AccessResult { access_token_id: string; refresh_token_id?: string; } interface AccessResponse { token_type: string; access_token: string; expires_in: number; refresh_token?: string; refresh_expires_in?: number; } type AuthHandler = (req: IncomingMessage, res: ServerResponse) => Promise; class AuthRequest { public request: IncomingMessage; public response: ServerResponse; public is_basic: boolean; public user: string; public password: string; public is_bearer: boolean; public token?: string; public token_data?: unknown; public token_id?: string; public body: string; private _cookie?: CookieSettings; private _refresh_cookie?: CookieSettings; private _is_successful: boolean; public get is_successful (): boolean { return this._is_successful; } public constructor ( req: IncomingMessage, res: ServerResponse, body: string, cookie?: CookieSettings, refresh_cookie?: CookieSettings ) { this.request = req; this.response = res; this.body = body; this.is_basic = false; this.is_bearer = false; this.user = ''; this.password = ''; this._cookie = cookie; this._refresh_cookie = refresh_cookie; this._is_successful = false; logger.extend ('constructor') ('started processing new auth request'); } private default_header (set_content = true) { this.response.setHeader ('Cache-Control', 'no-store'); this.response.setHeader ('Pragma', 'no-cache'); if (set_content) this.response.setHeader ('Content-Type', 'application/json'); } // eslint-disable-next-line max-statements, max-lines-per-function public async allow_access ({ access_token_expires_in, include_refresh_token, refresh_token_expires_in, redirect_to, data, leave_open, permissions }: AccessSettings): Promise { const log = logger.extend ('allow_access'); log ('allowed access'); this.default_header (typeof redirect_to !== 'string' && !leave_open); const at = await auth.sign ( 'access_token', access_token_expires_in, { data, permissions } ); const result: AccessResult = { access_token_id: at.id }; const res: AccessResponse = { token_type: 'bearer', access_token: at.signature, expires_in: access_token_expires_in }; const cookies = []; if (typeof this._cookie !== 'undefined') cookies.push (build_cookie (this._cookie, at.signature)); if (include_refresh_token) { log ('including refresh token'); if (typeof refresh_token_expires_in !== 'number') throw new Error ('no expiry time defined for refresh tokens'); const rt = await auth.sign ( 'refresh_token', refresh_token_expires_in, { data } ); res.refresh_token = rt.signature; res.refresh_expires_in = refresh_token_expires_in; result.refresh_token_id = rt.id; if (typeof this._refresh_cookie !== 'undefined') cookies.push (build_cookie (this._refresh_cookie, rt.signature)); } if (cookies.length > 0) { log ('sending %d cookies', cookies.length); this.response.setHeader ( 'Set-Cookie', cookies ); } this._is_successful = true; if (typeof redirect_to === 'string') { log ('redirecting to %s', redirect_to); this.response.setHeader ('Location', redirect_to); this.response.statusCode = 302; if (!leave_open) this.response.end (); return result; } if (!leave_open) { log ('finishing http request'); this.response.writeHead (200); this.response.end (JSON.stringify (res)); } return result; } public async allow_part ( part_token_expires_in: number, next_module: string, data?: Record, leave_open = false ): Promise { const log = logger.extend ('allow_part'); log ('allowed part token'); this.default_header (); const pt = await auth.sign ( 'part_token', part_token_expires_in, { next_module, data } ); const res = { token_type: 'bearer', part_token: pt.signature, expires_in: part_token_expires_in }; if (!leave_open) { log ('finishing http request'); this.response.writeHead (200); this.response.end (JSON.stringify (res)); } this._is_successful = true; return pt.id; } public invalid (error_description?: string, leave_open = false): void { const log = logger.extend ('invalid'); log ('rejecting invalid request'); this.default_header (); this.response.statusCode = 400; if (!leave_open) { log ('finishing http request'); this.response.end (JSON.stringify ({ error: 'invalid_request', error_description })); } } public deny (leave_open = false): void { const log = logger.extend ('deny'); log ('denied access'); this.default_header (); this.response.statusCode = 401; if (!leave_open) { log ('finishing http request'); this.response.end (JSON.stringify ({ error: 'invalid_client' })); } } } type AuthRequestHandler = (req: AuthRequest) => Promise | void; interface CreateHandlerOptions { refresh?: AccessSettings; modules?: Record; cookie?: CookieSettings; refresh_cookie?: CookieSettings; parse_body?: boolean; } type ProcessRequestOptions = Omit // eslint-disable-next-line max-lines-per-function, max-statements async function process_request ( request: AuthRequest, token: RegExpExecArray | null, default_handler: AuthRequestHandler, options?: ProcessRequestOptions ): Promise { const log = logger.extend ('process_request'); if (token === null) return default_handler (request); if ((/Basic/ui).test (token?.groups?.type as string)) { log ('found basic login data'); request.is_basic = true; let login = token?.groups?.token as string; if (!login.includes (':')) login = to_utf8 (login, 'base64'); const login_data = login.split (':'); request.user = login_data[0]; request.password = login_data[1]; return default_handler (request); } if ((/Bearer/ui).test (token?.groups?.type as string)) { log ('found bearer login data'); request.is_bearer = true; request.token = token?.groups?.token; const token_data = await auth.verify (request.token as string); if (!token_data.valid) return default_handler (request); log ('bearer token is valid'); request.token_data = token_data.data; request.token_id = token_data.id; if ( typeof options !== 'undefined' && typeof options.refresh !== 'undefined' && token_data.type === 'refresh_token' ) { log ('found refresh token, emitting new access token'); request.allow_access (options.refresh); return Promise.resolve (); } if ( typeof options !== 'undefined' && typeof options.modules !== 'undefined' && token_data.type === 'part_token' && typeof token_data.next_module !== 'undefined' && Object.keys (options.modules) .includes (token_data.next_module) ) { log ('processing module %s', token_data.next_module); return options.modules[token_data.next_module] (request); } request.invalid ('invalid bearer type'); return Promise.resolve (); } log ('no matching login method, triggering default handler'); return default_handler (request); } // eslint-disable-next-line max-lines-per-function export default function create_auth_handler ( default_handler: AuthRequestHandler, options?: CreateHandlerOptions ): AuthHandler { logger.extend ('create_auth_handler') ('creating new auth handler'); 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'); return async ( req: IncomingMessage, res: ServerResponse ): Promise => { const body: string = options?.parse_body ? await new Promise ((resolve) => { let data = ''; req.on ('data', (c) => { data += c; }); req.on ('end', () => { resolve (data); }); }) : ''; const request = new AuthRequest ( req, res, body, options?.cookie, options?.refresh_cookie ); const token = (/(?\S+) (?.+)/ui) .exec (req.headers.authorization as string); await process_request (request, token, default_handler, options); return request.is_successful; }; } export { AccessSettings, AccessResult, AccessResponse, AuthRequest, AuthRequestHandler, CreateHandlerOptions, AuthHandler };