diff --git a/lib/Authority.ts b/lib/Authority.ts index 5e8c469..8ec29a8 100644 --- a/lib/Authority.ts +++ b/lib/Authority.ts @@ -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 ); diff --git a/lib/Gateway.ts b/lib/Gateway.ts index 9c41db9..82f682d 100644 --- a/lib/Gateway.ts +++ b/lib/Gateway.ts @@ -5,11 +5,15 @@ * Created by Timo Hocker , 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; +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 = (/(?\w)+ (?.*)/u).exec (auth_header); + private get_header_auth (req: IncomingMessage): string | null { + const auth_header = req.headers.authorization; + const auth = (/(?\w+) (?.*)/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 ( - /[\^;](?[^;=]+)=(?[^;]+)/gu, - req.headers.get ('cookie'), - (res) => { - if (res.groups.name === this._options.cookie_name) - auth = res.groups.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 { + 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 { + ): 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 }; diff --git a/test/spec/Gateway.ts b/test/spec/Gateway.ts new file mode 100644 index 0000000..1ba7c2e --- /dev/null +++ b/test/spec/Gateway.ts @@ -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 { + 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'); + }); +});