/* * 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'; interface AccessSettings { access_token_expires_in: number include_refresh_token?: boolean refresh_token_expires_in?: number data?: Record } 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; } class AuthRequest { public request: IncomingMessage; public response: ServerResponse; public is_basic: boolean; public user: string; public password: string; public token_data?: unknown; public token_id?: string; public body: string; private _cookie_name?: string; public constructor ( req: IncomingMessage, res: ServerResponse, body: string, cookie?: string ) { this.request = req; this.response = res; this.body = body; this.is_basic = false; this.user = ''; this.password = ''; this._cookie_name = cookie; } private default_header () { this.response.setHeader ('Cache-Control', 'no-store'); this.response.setHeader ('Pragma', 'no-cache'); this.response.setHeader ('Content-Type', 'application/json'); } public allow_access ({ access_token_expires_in, include_refresh_token, refresh_token_expires_in, data }: AccessSettings): AccessResult { this.default_header (); const at = auth.sign ('access_token', access_token_expires_in, { data }); const result: AccessResult = { access_token_id: at.id }; const res: AccessResponse = { token_type: 'bearer', access_token: at.signature, expires_in: access_token_expires_in }; if (typeof this._cookie_name === 'string') { this.response.setHeader ( 'Set-Cookie', `${this._cookie_name}=${at.signature}` ); } if (include_refresh_token) { if (typeof refresh_token_expires_in !== 'number') throw new Error ('no expiry time defined for refresh tokens'); const rt = 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; } this.response.writeHead (200); this.response.end (JSON.stringify (res)); return result; } public allow_part ( part_token_expires_in: number, next_module: string, data?: Record ): string { this.default_header (); const pt = 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 }; this.response.writeHead (200); this.response.end (JSON.stringify (res)); return pt.id; } public invalid (error_description?: string) { this.default_header (); this.response.writeHead (400); this.response.end (JSON.stringify ({ error: 'invalid_request', error_description })); } public deny () { this.default_header (); this.response.writeHead (401); this.response.end (JSON.stringify ({ error: 'invalid_client' })); } } type AuthRequestHandler = (req: AuthRequest) => void|Promise; interface CreateHandlerOptions { refresh?: AccessSettings; modules?: Record; cookie_name?: string; } // eslint-disable-next-line max-lines-per-function export default function create_auth_handler ( default_handler: AuthRequestHandler, options?: CreateHandlerOptions ) { // eslint-disable-next-line max-lines-per-function return async function process_request ( req: IncomingMessage, res: ServerResponse ): Promise { const body: string = 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_name); const token = (/(?\S+) (?.+)/ui) .exec (req.headers.authorization as string); if (token === null) { request.deny (); return Promise.resolve (); } if ((/Basic/ui).test (token?.groups?.type as string)) { 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); } const token_data = auth.verify (token?.groups?.token as string); if (!token_data.valid) { request.deny (); return Promise.resolve (); } 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' ) { 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) ) return options.modules[token_data.next_module] (request); request.invalid ('invalid bearer type'); return Promise.resolve (); }; }