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