This commit is contained in:
		
							
								
								
									
										102
									
								
								lib/AuthHandler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								lib/AuthHandler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| import { IncomingMessage, ServerResponse } from 'http'; | ||||
| import auth from './Authority'; | ||||
|  | ||||
| interface AccessSettings { | ||||
|   access_token_expires_in?: number | ||||
|   include_refresh_token?: boolean | ||||
|   refresh_token_expires_in?: number | ||||
| } | ||||
|  | ||||
| class AuthRequest { | ||||
|   public request: IncomingMessage; | ||||
|   public response: ServerResponse; | ||||
|  | ||||
|   public constructor (req: IncomingMessage, res: ServerResponse) { | ||||
|     this.request = req; | ||||
|     this.response = res; | ||||
|   } | ||||
|  | ||||
|   private default_header () { | ||||
|     this.response.setHeader ('Cache-Control', 'no-store'); | ||||
|     this.response.setHeader ('Pragma', 'no-cache'); | ||||
|     this.response.setHeader ('Content-Type', 'application/json'); | ||||
|   } | ||||
|  | ||||
|   public allow_access ({ | ||||
|     access_token_expires_in, | ||||
|     include_refresh_token, | ||||
|     refresh_token_expires_in | ||||
|   }: AccessSettings): void { | ||||
|     this.default_header (); | ||||
|     this.response.writeHead (200); | ||||
|  | ||||
|     const res = { | ||||
|       token_type:   'bearer', | ||||
|       access_token: auth.sign ('access_token', access_token_expires_in), | ||||
|       expires_in:   access_token_expires_in, | ||||
|       scope | ||||
|     }; | ||||
|  | ||||
|     if (include_refresh_token) { | ||||
|       res.refresh_token = auth.sign ('refresh_token', refresh_token_expires_in); | ||||
|       res.refresh_token_expires_in = refresh_token_expires_in; | ||||
|     } | ||||
|     this.response.end (JSON.stringify (res)); | ||||
|   } | ||||
|  | ||||
|   public invalid (error_description) { | ||||
|     this.default_header (); | ||||
|     this.response.writeHead (400); | ||||
|     this.response.end (JSON.stringify ({ | ||||
|       error: 'invalid_request', | ||||
|       error_description | ||||
|     })); | ||||
|   } | ||||
|  | ||||
|   public deny () { | ||||
|     this.default_header (); | ||||
|     this.response.writeHead (401); | ||||
|     this.response.end (JSON.stringify ({ error: 'invalid_client' })); | ||||
|   } | ||||
| } | ||||
|  | ||||
| type AuthRequestHandler = (req: AuthRequest) => void|Promise<void>; | ||||
|  | ||||
| interface CreateHandlerOptions { | ||||
|   refresh?: AccessSettings; | ||||
|   modules?: Record<string, AuthRequestHandler>; | ||||
| } | ||||
|  | ||||
| export default function create_auth_handler ( | ||||
|   default_handler: AuthRequestHandler, | ||||
|   { refresh, modules }: CreateHandlerOptions | ||||
| ) { | ||||
|   return function process_request ( | ||||
|     req: IncomingMessage, | ||||
|     res: ServerResponse | ||||
|   ): Promise<void>|void { | ||||
|     const request = new AuthRequest (req, res); | ||||
|     const token = (/Bearer (?<token>.+)/ui).exec (req.headers.authorization); | ||||
|     if (token === null) | ||||
|       return default_handler (request); | ||||
|  | ||||
|     const token_data = auth.verify (token.groups.token); | ||||
|  | ||||
|     if (!token_data.valid) { | ||||
|       request.deny (); | ||||
|       return Promise.resolve (); | ||||
|     } | ||||
|  | ||||
|     if (token_data.type === 'refresh_token') { | ||||
|       request.allow_access (refresh); | ||||
|       return Promise.resolve (); | ||||
|     } | ||||
|  | ||||
|     if (token_data.type === 'part_token' && Object.keys (modules) | ||||
|       .includes (token_data.next_module)) | ||||
|       return modules[token_data.next_module] (request); | ||||
|  | ||||
|     request.invalid ('invalid bearer token'); | ||||
|     return Promise.resolve (); | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										75
									
								
								test/spec/AuthHandler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								test/spec/AuthHandler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| import http from 'http'; | ||||
| import auth from '../../lib/Authority'; | ||||
| import { get } from '../Helper'; | ||||
|  | ||||
| const expires_seconds = 600; | ||||
| const refresh_expires_seconds = 3600; | ||||
|  | ||||
| // eslint-disable-next-line max-lines-per-function | ||||
| xdescribe ('auth handler', () => { | ||||
|   let server: http.Server|null = null; | ||||
|   beforeAll (() => { | ||||
|     server = http.createServer ((req, res) => { | ||||
|       res.writeHead (404); | ||||
|       res.end (); | ||||
|     }); | ||||
|     server.listen (3000); | ||||
|  | ||||
|     jasmine.clock () | ||||
|       .install (); | ||||
|     jasmine.clock () | ||||
|       .mockDate (new Date); | ||||
|   }); | ||||
|  | ||||
|   it ('should return a valid access and refresh token', async () => { | ||||
|     const resp = await get ({ authorization: 'Basic foo:bar' }); | ||||
|     expect (resp.statusCode) | ||||
|       .toEqual (200); | ||||
|     const data = JSON.parse (resp.body as string); | ||||
|     const at = data.access_token; | ||||
|     const rt = data.refresh_token; | ||||
|     expect (resp.headers['set-cookie']) | ||||
|       .toContain (`cookie_jar=${at}`); | ||||
|     expect (resp.headers['cache-control']) | ||||
|       .toEqual ('no-store'); | ||||
|     expect (resp.headers.pragma) | ||||
|       .toEqual ('no-cache'); | ||||
|     expect (data.token_type) | ||||
|       .toEqual ('bearer'); | ||||
|     expect (data.expires_in) | ||||
|       .toEqual (expires_seconds); | ||||
|     expect (data.refresh_expires_in) | ||||
|       .toEqual (refresh_expires_seconds); | ||||
|  | ||||
|     expect (at as string) | ||||
|       .toMatch (/^[0-9a-z-._~+/]+$/ui); | ||||
|     expect (rt as string) | ||||
|       .toMatch (/^[0-9a-z-._~+/]+$/ui); | ||||
|  | ||||
|     const atv = auth.verify (at as string); | ||||
|     expect (atv.valid) | ||||
|       .toEqual (true); | ||||
|     expect (atv.authorized) | ||||
|       .toEqual (true); | ||||
|     expect (atv.type) | ||||
|       .toEqual ('access_token'); | ||||
|  | ||||
|     const rtv = auth.verify (rt as string); | ||||
|     expect (rtv.valid) | ||||
|       .toEqual (true); | ||||
|     expect (rtv.authorized) | ||||
|       .toEqual (false); | ||||
|     expect (rtv.type) | ||||
|       .toEqual ('refresh_token'); | ||||
|   }); | ||||
|  | ||||
|   afterAll (() => { | ||||
|     if (server === null) | ||||
|       throw new Error ('server is null'); | ||||
|     server.close (); | ||||
|     jasmine.clock () | ||||
|       .tick (24 * 60 * 60 * 1000); | ||||
|     jasmine.clock () | ||||
|       .uninstall (); | ||||
|   }); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user