diff --git a/lib/AuthHandler.ts b/lib/AuthHandler.ts new file mode 100644 index 0000000..e22e9bf --- /dev/null +++ b/lib/AuthHandler.ts @@ -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; + +interface CreateHandlerOptions { + refresh?: AccessSettings; + modules?: Record; +} + +export default function create_auth_handler ( + default_handler: AuthRequestHandler, + { refresh, modules }: CreateHandlerOptions +) { + return function process_request ( + req: IncomingMessage, + res: ServerResponse + ): Promise|void { + const request = new AuthRequest (req, res); + const token = (/Bearer (?.+)/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 (); + }; +} diff --git a/test/spec/AuthHandler.ts b/test/spec/AuthHandler.ts new file mode 100644 index 0000000..61dcdab --- /dev/null +++ b/test/spec/AuthHandler.ts @@ -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 (); + }); +});