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';
|
|
|
|
|
|
|
|
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>;
|
|
|
|
|
2022-01-08 22:10:02 +01:00
|
|
|
function build_cookie (name: string, value: string): string {
|
|
|
|
return `${name}=${value}; Secure; HttpOnly; SameSite=Strict`;
|
|
|
|
}
|
|
|
|
|
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;
|
2021-01-06 11:38:56 +01:00
|
|
|
|
|
|
|
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-06 11:38:56 +01:00
|
|
|
|
2021-01-03 14:51:07 +01:00
|
|
|
public body: string;
|
2021-01-01 14:14:19 +01:00
|
|
|
|
|
|
|
private _cookie_name?: string;
|
2022-01-04 21:32:04 +01:00
|
|
|
private _refresh_cookie_name?: string;
|
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-04 21:32:04 +01:00
|
|
|
cookie?: string,
|
|
|
|
refresh_cookie?: string
|
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;
|
2021-01-06 11:38:56 +01:00
|
|
|
this.is_bearer = false;
|
2021-01-01 14:14:19 +01:00
|
|
|
this.user = '';
|
|
|
|
this.password = '';
|
|
|
|
this._cookie_name = cookie;
|
2022-01-04 21:32:04 +01:00
|
|
|
this._refresh_cookie_name = refresh_cookie;
|
2022-01-03 15:40:13 +01:00
|
|
|
this._is_successful = false;
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('allowed access');
|
2022-01-04 13:39:00 +01:00
|
|
|
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 = [];
|
|
|
|
|
|
|
|
if (typeof this._cookie_name === 'string')
|
2022-01-08 22:10:02 +01:00
|
|
|
cookies.push (build_cookie (this._cookie_name, at.signature));
|
2021-01-01 14:14:19 +01:00
|
|
|
|
2020-12-30 19:39:49 +01:00
|
|
|
if (include_refresh_token) {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('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
|
|
|
|
|
|
|
if (typeof this._refresh_cookie_name === 'string')
|
2022-01-08 22:10:02 +01:00
|
|
|
// eslint-disable-next-line max-len
|
|
|
|
cookies.push (build_cookie (this._refresh_cookie_name, rt.signature));
|
2022-01-04 21:32:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (cookies.length > 0) {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('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
|
|
|
|
2022-01-04 13:39:00 +01:00
|
|
|
this._is_successful = true;
|
|
|
|
|
2021-05-10 12:26:56 +02:00
|
|
|
if (typeof redirect_to === 'string') {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('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) {
|
|
|
|
logger ('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>;
|
2021-01-01 14:14:19 +01:00
|
|
|
cookie_name?: string;
|
2022-01-04 21:32:04 +01:00
|
|
|
refresh_cookie_name?: string;
|
2020-12-30 19:39:49 +01:00
|
|
|
}
|
|
|
|
|
2022-01-03 15:40:13 +01:00
|
|
|
// eslint-disable-next-line max-lines-per-function
|
|
|
|
function process_request (
|
|
|
|
request: AuthRequest,
|
|
|
|
token: RegExpExecArray | null,
|
|
|
|
default_handler: AuthRequestHandler,
|
|
|
|
options?: CreateHandlerOptions
|
|
|
|
): Promise<void> | void {
|
|
|
|
if (token === null)
|
|
|
|
return default_handler (request);
|
|
|
|
|
|
|
|
if ((/Basic/ui).test (token?.groups?.type as string)) {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('found bearer login data');
|
2022-01-03 15:40:13 +01:00
|
|
|
request.is_bearer = true;
|
|
|
|
request.token = token?.groups?.token;
|
|
|
|
|
|
|
|
const token_data = auth.verify (request.token as string);
|
|
|
|
|
|
|
|
if (!token_data.valid)
|
|
|
|
return default_handler (request);
|
|
|
|
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('bearer token is valid');
|
|
|
|
|
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-01-05 12:32:04 +01:00
|
|
|
logger ('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
|
|
|
) {
|
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('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-01-05 12:32:04 +01:00
|
|
|
logger ('creating new auth handler');
|
2022-01-04 21:32:04 +01:00
|
|
|
if (
|
|
|
|
typeof options?.cookie_name !== 'undefined'
|
|
|
|
&& options.cookie_name === options.refresh_cookie_name
|
|
|
|
)
|
|
|
|
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> => {
|
2021-01-03 14:51:07 +01:00
|
|
|
const body: string = await new Promise ((resolve) => {
|
|
|
|
let data = '';
|
|
|
|
req.on ('data', (c) => {
|
|
|
|
data += c;
|
|
|
|
});
|
|
|
|
req.on ('end', () => {
|
|
|
|
resolve (data);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-01-04 21:32:04 +01:00
|
|
|
const request = new AuthRequest (
|
|
|
|
req,
|
|
|
|
res,
|
|
|
|
body,
|
|
|
|
options?.cookie_name,
|
|
|
|
options?.refresh_cookie_name
|
|
|
|
);
|
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
|
|
|
};
|