auth-server-helper/lib/AuthHandler.ts

361 lines
9.4 KiB
TypeScript
Raw Normal View History

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