This commit is contained in:
parent
051c2bdbbd
commit
a8fb92b367
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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
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