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';
|
|
|
|
|
|
|
|
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-01-03 15:13:03 +01:00
|
|
|
data?: Record<string, unknown>
|
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;
|
|
|
|
}
|
|
|
|
|
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-03 15:32:29 +01:00
|
|
|
public token_data?: Record<string, unknown>;
|
|
|
|
public token_id?: string;
|
2021-01-03 14:51:07 +01:00
|
|
|
public body: string;
|
2021-01-01 14:14:19 +01:00
|
|
|
|
|
|
|
private _cookie_name?: string;
|
|
|
|
|
|
|
|
public constructor (
|
|
|
|
req: IncomingMessage,
|
|
|
|
res: ServerResponse,
|
2021-01-03 14:51:07 +01:00
|
|
|
body: string,
|
2021-01-01 14:14:19 +01:00
|
|
|
cookie?: string
|
|
|
|
) {
|
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.user = '';
|
|
|
|
this.password = '';
|
|
|
|
this._cookie_name = cookie;
|
2020-12-30 19:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2021-01-03 15:13:03 +01:00
|
|
|
refresh_token_expires_in,
|
|
|
|
data
|
2021-01-01 14:14:19 +01:00
|
|
|
}: AccessSettings): AccessResult {
|
2020-12-30 19:39:49 +01:00
|
|
|
this.default_header ();
|
|
|
|
|
2021-01-03 15:13:03 +01:00
|
|
|
const at = 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
|
|
|
};
|
|
|
|
|
2021-01-01 14:14:19 +01:00
|
|
|
if (typeof this._cookie_name === 'string') {
|
|
|
|
this.response.setHeader (
|
|
|
|
'Set-Cookie',
|
|
|
|
`${this._cookie_name}=${at.signature}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-12-30 19:39:49 +01:00
|
|
|
if (include_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-03 15:13:03 +01:00
|
|
|
const rt = auth.sign (
|
|
|
|
'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;
|
2020-12-30 19:39:49 +01:00
|
|
|
}
|
2021-01-01 14:14:19 +01:00
|
|
|
this.response.writeHead (200);
|
2020-12-30 19:39:49 +01:00
|
|
|
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-03 15:13:03 +01:00
|
|
|
public allow_part (
|
|
|
|
part_token_expires_in: number,
|
|
|
|
next_module: string,
|
|
|
|
data?: Record<string, unknown>
|
|
|
|
): string {
|
2021-01-03 14:51:07 +01:00
|
|
|
this.default_header ();
|
|
|
|
|
2021-01-03 15:13:03 +01:00
|
|
|
const pt = auth.sign (
|
|
|
|
'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
|
|
|
|
};
|
|
|
|
|
|
|
|
this.response.writeHead (200);
|
|
|
|
this.response.end (JSON.stringify (res));
|
|
|
|
|
|
|
|
return pt.id;
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:14:19 +01:00
|
|
|
public invalid (error_description?: string) {
|
2020-12-30 19:39:49 +01:00
|
|
|
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<void>;
|
|
|
|
|
|
|
|
interface CreateHandlerOptions {
|
|
|
|
refresh?: AccessSettings;
|
|
|
|
modules?: Record<string, AuthRequestHandler>;
|
2021-01-01 14:14:19 +01:00
|
|
|
cookie_name?: string;
|
2020-12-30 19:39:49 +01:00
|
|
|
}
|
|
|
|
|
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
|
2020-12-30 19:39:49 +01:00
|
|
|
) {
|
2021-01-03 14:51:07 +01:00
|
|
|
// eslint-disable-next-line max-lines-per-function
|
|
|
|
return async function process_request (
|
2020-12-30 19:39:49 +01:00
|
|
|
req: IncomingMessage,
|
|
|
|
res: ServerResponse
|
2021-01-03 14:51:07 +01:00
|
|
|
): Promise<void> {
|
|
|
|
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);
|
2021-01-01 14:14:19 +01:00
|
|
|
const token = (/(?<type>\S+) (?<token>.+)/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];
|
|
|
|
|
2020-12-30 19:39:49 +01:00
|
|
|
return default_handler (request);
|
2021-01-01 14:14:19 +01:00
|
|
|
}
|
2020-12-30 19:39:49 +01:00
|
|
|
|
2021-01-01 14:14:19 +01:00
|
|
|
const token_data = auth.verify (token?.groups?.token as string);
|
2020-12-30 19:39:49 +01:00
|
|
|
|
|
|
|
if (!token_data.valid) {
|
|
|
|
request.deny ();
|
|
|
|
return Promise.resolve ();
|
|
|
|
}
|
|
|
|
|
2021-01-03 15:32:29 +01:00
|
|
|
request.token_data = token_data.data;
|
|
|
|
request.token_id = token_data.id;
|
|
|
|
|
2021-01-01 14:14:19 +01:00
|
|
|
if (
|
|
|
|
typeof options !== 'undefined'
|
|
|
|
&& typeof options.refresh !== 'undefined'
|
|
|
|
&& token_data.type === 'refresh_token'
|
|
|
|
) {
|
|
|
|
request.allow_access (options.refresh);
|
2020-12-30 19:39:49 +01:00
|
|
|
return Promise.resolve ();
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:14:19 +01:00
|
|
|
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');
|
2020-12-30 19:39:49 +01:00
|
|
|
return Promise.resolve ();
|
|
|
|
};
|
|
|
|
}
|