testing gateway
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Timo Hocker 2020-12-28 16:53:08 +01:00
parent 051c2bdbbd
commit a8fb92b367
3 changed files with 192 additions and 20 deletions

View File

@ -37,7 +37,14 @@ class Authority {
}; };
const data = verify_signature_get_info ( const data = verify_signature_get_info (
key, key,
(info) => keystore.get_key (info.iat / 1000), (info) => {
try {
return keystore.get_key (info.iat / 1000);
}
catch {
return '';
}
},
(info) => info.valid_for * 1000 (info) => info.valid_for * 1000
); );

View File

@ -5,11 +5,15 @@
* Created by Timo Hocker <timo@scode.ovh>, December 2020 * Created by Timo Hocker <timo@scode.ovh>, December 2020
*/ */
import { IncomingMessage, ServerResponse } from 'http';
import { run_regex } from '@sapphirecode/utilities'; import { run_regex } from '@sapphirecode/utilities';
import authority from './Authority'; import authority from './Authority';
type AnyFunc = (...args: unknown) => unknown; type AnyFunc = (...args: unknown[]) => unknown;
type Gateway = (req: Request, res: Response, next: AnyFunc) => Promise<void>; type Gateway = (
req: IncomingMessage,
res: ServerResponse, next: AnyFunc
) => unknown;
interface GatewayOptions { interface GatewayOptions {
redirect_url: string; redirect_url: string;
@ -23,38 +27,38 @@ class GatewayClass {
this._options = options; this._options = options;
} }
private redirect (res): void { private redirect (res: ServerResponse): void {
res.statusCode = 302; res.statusCode = 302;
res.setHeader ('Location', this._options.redirect_url); res.setHeader ('Location', this._options.redirect_url);
res.end (); res.end ();
} }
private get_header_auth (req: Request): string | null { private get_header_auth (req: IncomingMessage): string | null {
const auth_header = req.headers.get ('Authorization'); const auth_header = req.headers.authorization;
const auth = (/(?<type>\w)+ (?<data>.*)/u).exec (auth_header); const auth = (/(?<type>\w+) (?<data>.*)/u).exec (auth_header || '');
if (auth === null) if (auth === null)
return null; return null;
if (auth.groups.type !== 'Bearer') if (auth.groups?.type !== 'Bearer')
return null; 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') if (typeof this._options.cookie_name === 'undefined')
return null; return null;
let auth = null; let auth = null;
run_regex ( run_regex (
/[\^;](?<name>[^;=]+)=(?<value>[^;]+)/gu, /(?:^|;)(?<name>[^;=]+)=(?<value>[^;]+)/gu,
req.headers.get ('cookie'), req.headers.cookie,
(res) => { (res: RegExpMatchArray) => {
if (res.groups.name === this._options.cookie_name) if (res.groups?.name === this._options.cookie_name)
auth = res.groups.value; auth = res.groups?.value;
} }
); );
return auth; return auth;
} }
private authenticate (req: Request): Promise<boolean> { private authenticate (req: IncomingMessage): boolean {
let auth = this.get_header_auth (req); let auth = this.get_header_auth (req);
if (auth === null) if (auth === null)
auth = this.get_cookie_auth (req); auth = this.get_cookie_auth (req);
@ -65,10 +69,10 @@ class GatewayClass {
} }
public process_request ( public process_request (
req: Request, req: IncomingMessage,
res: Response, res: ServerResponse,
next: AnyFunc next: AnyFunc
): Promise<void> { ): unknown {
if (this.authenticate (req)) if (this.authenticate (req))
return next (); return next ();
return this.redirect (res); return this.redirect (res);
@ -77,5 +81,7 @@ class GatewayClass {
export default function create_gateway (options: GatewayOptions): Gateway { export default function create_gateway (options: GatewayOptions): Gateway {
const g = new GatewayClass (options); 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
View 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');
});
});