This commit is contained in:
parent
051c2bdbbd
commit
a8fb92b367
@ -37,7 +37,14 @@ class Authority {
|
||||
};
|
||||
const data = verify_signature_get_info (
|
||||
key,
|
||||
(info) => keystore.get_key (info.iat / 1000),
|
||||
(info) => {
|
||||
try {
|
||||
return keystore.get_key (info.iat / 1000);
|
||||
}
|
||||
catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
(info) => info.valid_for * 1000
|
||||
);
|
||||
|
||||
|
@ -5,11 +5,15 @@
|
||||
* Created by Timo Hocker <timo@scode.ovh>, December 2020
|
||||
*/
|
||||
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
import { run_regex } from '@sapphirecode/utilities';
|
||||
import authority from './Authority';
|
||||
|
||||
type AnyFunc = (...args: unknown) => unknown;
|
||||
type Gateway = (req: Request, res: Response, next: AnyFunc) => Promise<void>;
|
||||
type AnyFunc = (...args: unknown[]) => unknown;
|
||||
type Gateway = (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse, next: AnyFunc
|
||||
) => unknown;
|
||||
|
||||
interface GatewayOptions {
|
||||
redirect_url: string;
|
||||
@ -23,38 +27,38 @@ class GatewayClass {
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
private redirect (res): void {
|
||||
private redirect (res: ServerResponse): void {
|
||||
res.statusCode = 302;
|
||||
res.setHeader ('Location', this._options.redirect_url);
|
||||
res.end ();
|
||||
}
|
||||
|
||||
private get_header_auth (req: Request): string | null {
|
||||
const auth_header = req.headers.get ('Authorization');
|
||||
const auth = (/(?<type>\w)+ (?<data>.*)/u).exec (auth_header);
|
||||
private get_header_auth (req: IncomingMessage): string | null {
|
||||
const auth_header = req.headers.authorization;
|
||||
const auth = (/(?<type>\w+) (?<data>.*)/u).exec (auth_header || '');
|
||||
if (auth === null)
|
||||
return null;
|
||||
if (auth.groups.type !== 'Bearer')
|
||||
if (auth.groups?.type !== 'Bearer')
|
||||
return null;
|
||||
return auth.groups.data;
|
||||
return auth.groups?.data;
|
||||
}
|
||||
|
||||
private get_cookie_auth (req: Request): string | null {
|
||||
private get_cookie_auth (req: IncomingMessage): string | null {
|
||||
if (typeof this._options.cookie_name === 'undefined')
|
||||
return null;
|
||||
let auth = null;
|
||||
run_regex (
|
||||
/[\^;](?<name>[^;=]+)=(?<value>[^;]+)/gu,
|
||||
req.headers.get ('cookie'),
|
||||
(res) => {
|
||||
if (res.groups.name === this._options.cookie_name)
|
||||
auth = res.groups.value;
|
||||
/(?:^|;)(?<name>[^;=]+)=(?<value>[^;]+)/gu,
|
||||
req.headers.cookie,
|
||||
(res: RegExpMatchArray) => {
|
||||
if (res.groups?.name === this._options.cookie_name)
|
||||
auth = res.groups?.value;
|
||||
}
|
||||
);
|
||||
return auth;
|
||||
}
|
||||
|
||||
private authenticate (req: Request): Promise<boolean> {
|
||||
private authenticate (req: IncomingMessage): boolean {
|
||||
let auth = this.get_header_auth (req);
|
||||
if (auth === null)
|
||||
auth = this.get_cookie_auth (req);
|
||||
@ -65,10 +69,10 @@ class GatewayClass {
|
||||
}
|
||||
|
||||
public process_request (
|
||||
req: Request,
|
||||
res: Response,
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: AnyFunc
|
||||
): Promise<void> {
|
||||
): unknown {
|
||||
if (this.authenticate (req))
|
||||
return next ();
|
||||
return this.redirect (res);
|
||||
@ -77,5 +81,7 @@ class GatewayClass {
|
||||
|
||||
export default function create_gateway (options: GatewayOptions): Gateway {
|
||||
const g = new GatewayClass (options);
|
||||
return g.process_request;
|
||||
return g.process_request.bind (g);
|
||||
}
|
||||
|
||||
export { Gateway, AnyFunc };
|
||||
|
159
test/spec/Gateway.ts
Normal file
159
test/spec/Gateway.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import http from 'http';
|
||||
import gateway from '../../lib/Gateway';
|
||||
import authority from '../../lib/Authority';
|
||||
import blacklist from '../../lib/Blacklist';
|
||||
|
||||
interface Response {
|
||||
body: string
|
||||
status?: number
|
||||
location?: string
|
||||
}
|
||||
|
||||
|
||||
function get (
|
||||
url: string,
|
||||
token?: string,
|
||||
mode = 0
|
||||
): Promise<Response> {
|
||||
const headers: http.OutgoingHttpHeaders = {};
|
||||
if (mode === 1)
|
||||
headers.cookie = `cookie_jar=${token}`;
|
||||
else if (mode === 0 && typeof token === 'string')
|
||||
headers.authorization = `Bearer ${token}`;
|
||||
else if (mode === 2)
|
||||
headers.authorization = `Basic ${token}`;
|
||||
return new Promise ((resolve) => {
|
||||
http.get (url, { headers }, (res) => {
|
||||
let body = '';
|
||||
res.on ('data', (d) => {
|
||||
body += d;
|
||||
});
|
||||
res.on ('end', () => {
|
||||
resolve ({
|
||||
body,
|
||||
status: res.statusCode,
|
||||
location: res.headers.location
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
describe ('gateway', () => {
|
||||
let server: http.Server|null = null;
|
||||
|
||||
beforeAll (() => {
|
||||
jasmine.clock ()
|
||||
.install ();
|
||||
jasmine.clock ()
|
||||
.mockDate (new Date);
|
||||
|
||||
const g = gateway ({
|
||||
redirect_url: 'http://localhost/auth',
|
||||
cookie_name: 'cookie_jar'
|
||||
});
|
||||
|
||||
server = http.createServer ((req, res) => {
|
||||
const passed_handler = () => {
|
||||
res.writeHead (200);
|
||||
res.end ('passed');
|
||||
};
|
||||
g (req, res, passed_handler);
|
||||
});
|
||||
server.listen (3000);
|
||||
});
|
||||
|
||||
afterAll (() => {
|
||||
if (server === null)
|
||||
throw new Error ('server is null');
|
||||
server.close ();
|
||||
|
||||
jasmine.clock ()
|
||||
.tick (24 * 60 * 60 * 1000);
|
||||
jasmine.clock ()
|
||||
.uninstall ();
|
||||
});
|
||||
|
||||
it ('should redirect any unauthorized request', async () => {
|
||||
const resp = await get ('http://localhost:3000');
|
||||
expect (resp.status)
|
||||
.toEqual (302);
|
||||
expect (resp.location)
|
||||
.toEqual ('http://localhost/auth');
|
||||
});
|
||||
|
||||
it ('should allow a valid access token', async () => {
|
||||
const token = authority.sign ('access_token', 60);
|
||||
const resp = await get ('http://localhost:3000', token.signature);
|
||||
expect (resp.status)
|
||||
.toEqual (200);
|
||||
expect (resp.body)
|
||||
.toEqual ('passed');
|
||||
});
|
||||
|
||||
it ('should allow a valid access token using cookies', async () => {
|
||||
const token = authority.sign ('access_token', 60);
|
||||
const resp = await get ('http://localhost:3000', token.signature, 1);
|
||||
expect (resp.status)
|
||||
.toEqual (200);
|
||||
expect (resp.body)
|
||||
.toEqual ('passed');
|
||||
});
|
||||
|
||||
it ('should reject an outdated access token', async () => {
|
||||
const token = authority.sign ('access_token', 60);
|
||||
jasmine.clock ()
|
||||
.tick (70000);
|
||||
const resp = await get ('http://localhost:3000', token.signature);
|
||||
expect (resp.status)
|
||||
.toEqual (302);
|
||||
expect (resp.location)
|
||||
.toEqual ('http://localhost/auth');
|
||||
});
|
||||
|
||||
it ('should reject a blacklisted access token', async () => {
|
||||
const token = authority.sign ('access_token', 60);
|
||||
blacklist.add_signature (token.id);
|
||||
const resp = await get ('http://localhost:3000', token.signature);
|
||||
expect (resp.status)
|
||||
.toEqual (302);
|
||||
expect (resp.location)
|
||||
.toEqual ('http://localhost/auth');
|
||||
});
|
||||
|
||||
it ('should reject any refresh_token', async () => {
|
||||
const token = authority.sign ('refresh_token', 60);
|
||||
const resp = await get ('http://localhost:3000', token.signature);
|
||||
expect (resp.status)
|
||||
.toEqual (302);
|
||||
expect (resp.location)
|
||||
.toEqual ('http://localhost/auth');
|
||||
});
|
||||
|
||||
it ('should reject any part_token', async () => {
|
||||
const token = authority.sign ('part_token', 60);
|
||||
const resp = await get ('http://localhost:3000', token.signature);
|
||||
expect (resp.status)
|
||||
.toEqual (302);
|
||||
expect (resp.location)
|
||||
.toEqual ('http://localhost/auth');
|
||||
});
|
||||
|
||||
it ('should reject any noname token', async () => {
|
||||
const token = authority.sign ('none', 60);
|
||||
const resp = await get ('http://localhost:3000', token.signature);
|
||||
expect (resp.status)
|
||||
.toEqual (302);
|
||||
expect (resp.location)
|
||||
.toEqual ('http://localhost/auth');
|
||||
});
|
||||
|
||||
it ('should reject non-bearer auth', async () => {
|
||||
const resp = await get ('http://localhost:3000', 'foo:bar', 2);
|
||||
expect (resp.status)
|
||||
.toEqual (302);
|
||||
expect (resp.location)
|
||||
.toEqual ('http://localhost/auth');
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user