This commit is contained in:
		| @@ -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'); | ||||
|   }); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user