/* * Copyright (C) Sapphirecode - All Rights Reserved * This file is part of Auth-Server-Helper which is released under MIT. * See file 'LICENSE' for full license details. * Created by Timo Hocker , January 2021 */ /* eslint-disable max-lines */ import http, { IncomingMessage, ServerResponse } from 'http'; import { to_b64 } from '@sapphirecode/encoding-helper'; import auth from '../../lib/Authority'; import { clock_finalize, clock_setup, get, modify_signature, Response } from '../Helper'; import { create_auth_handler } from '../../lib/index'; import { build_cookie, extract_cookie } from '../../lib/cookie'; const expires_seconds = 600; const refresh_expires_seconds = 3600; const part_expires_seconds = 60; interface CheckHeaderResult { at: string; rt?: string; data: Record; } function check_headers (resp: Response): CheckHeaderResult { const data = JSON.parse (resp.body as string); const at = data.access_token; const rt = data.refresh_token; expect (resp.headers['cache-control']) .toEqual ('no-store'); expect (resp.headers.pragma) .toEqual ('no-cache'); return { data, at, rt }; } async function check_token (token: string|null, type: string): Promise { const v = await auth.verify (token || ''); expect (v.valid) .toEqual (true); expect (v.authorized) .toEqual (type === 'access_token'); expect (v.type) .toEqual (type); expect (token) .toMatch (/^[0-9a-z-._~+/]+$/ui); } // eslint-disable-next-line max-lines-per-function describe ('auth handler', () => { let server: http.Server|null = null; // eslint-disable-next-line max-lines-per-function beforeAll (() => { clock_setup (); // eslint-disable-next-line complexity, max-lines-per-function const ah = create_auth_handler (async (req) => { if (!req.is_basic && !req.is_bearer) { let body_auth = false; try { const data = JSON.parse (req.body); if (data.username === 'foo' && data.password === 'bar') { req.allow_access ({ access_token_expires_in: expires_seconds, include_refresh_token: true, refresh_token_expires_in: refresh_expires_seconds }); body_auth = true; } } catch { body_auth = false; } if (!body_auth) req.invalid ('unknown authorization type'); } else if (req.is_bearer) { req.deny (); } else if (req.user === 'foo' && req.password === 'bar') { req.allow_access ({ access_token_expires_in: expires_seconds, include_refresh_token: true, refresh_token_expires_in: refresh_expires_seconds }); } else if (req.user === 'part' && req.password === 'bar') { req.allow_part (part_expires_seconds, 'two_factor'); } else if (req.user === 'red' && req.password === 'irect') { req.allow_access ({ access_token_expires_in: expires_seconds, redirect_to: '/redirected' }); } else if (req.user === 'leave' && req.password === 'open') { req.response.setHeader ('Content-Type', 'text/plain'); await req.allow_access ({ access_token_expires_in: expires_seconds, leave_open: true }); req.response.write ('custom response, '); (req.response.connection as unknown as Record) .append_flag = true; } else { req.deny (); } }, { cookie: { name: 'cookie_jar' }, refresh_cookie: { name: 'mint_cookies' }, refresh: { access_token_expires_in: expires_seconds, refresh_token_expires_in: refresh_expires_seconds, include_refresh_token: true }, modules: { two_factor (request) { if (request.body === 'letmein') { request.allow_access ({ access_token_expires_in: expires_seconds, include_refresh_token: true, refresh_token_expires_in: refresh_expires_seconds }); } else { request.deny (); } } }, parse_body: true }); server = http.createServer (async ( req: IncomingMessage, res: ServerResponse ) => { const is_successful = await ah (req, res); if ((res.connection as unknown as Record).append_flag) res.end (String (is_successful)); }); server.listen (3000); }); afterAll (() => { if (server === null) throw new Error ('server is null'); server.close (); clock_finalize (); }); it ('auth test sequence', async () => { // get initial access and refresh tokens const resp1 = await get ({ authorization: 'Basic foo:bar' }); expect (resp1.statusCode) .toEqual (200); const res1 = check_headers (resp1); expect (res1.data.token_type) .toEqual ('bearer'); expect (resp1.headers['set-cookie']) .toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string)); expect (resp1.headers['set-cookie']) .toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string)); await check_token (res1.at as string, 'access_token'); expect (res1.data.expires_in) .toEqual (expires_seconds); await check_token (res1.rt as string, 'refresh_token'); expect (res1.data.refresh_expires_in) .toEqual (refresh_expires_seconds); // get refresh token const resp2 = await get ({ authorization: `Bearer ${res1.rt}` }); expect (resp2.statusCode) .toEqual (200); const res2 = check_headers (resp2); expect (res2.data.token_type) .toEqual ('bearer'); expect (resp2.headers['set-cookie']) .toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string)); expect (resp2.headers['set-cookie']) .toContain (build_cookie ({ name: 'mint_cookies' }, res2.rt as string)); await check_token (res2.at as string, 'access_token'); expect (res2.data.expires_in) .toEqual (expires_seconds); expect (res2.at).not.toEqual (res1.at); await check_token (res2.rt as string, 'refresh_token'); expect (res2.data.refresh_expires_in) .toEqual (refresh_expires_seconds); expect (res2.rt).not.toEqual (res1.rt); }); it ('should return the correct denial message', async () => { const resp = await get ({ authorization: 'Basic bar:baz' }); expect (resp.statusCode) .toEqual (401); const res = check_headers (resp); expect (res.data) .toEqual ({ error: 'invalid_client' }); }); it ('should allow base64 login', async () => { const resp1 = await get ({ authorization: `Basic ${to_b64 ('foo:bar')}` }); expect (resp1.statusCode) .toEqual (200); const res1 = check_headers (resp1); expect (res1.data.token_type) .toEqual ('bearer'); expect (resp1.headers['set-cookie']) .toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string)); expect (resp1.headers['set-cookie']) .toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string)); await check_token (res1.at as string, 'access_token'); expect (res1.data.expires_in) .toEqual (expires_seconds); await check_token (res1.rt as string, 'refresh_token'); expect (res1.data.refresh_expires_in) .toEqual (refresh_expires_seconds); }); it ('should allow body login', async () => { const resp1 = await get ( // eslint-disable-next-line @typescript-eslint/naming-convention { 'Content-Type': 'application/json' }, JSON.stringify ({ username: 'foo', password: 'bar' }) ); expect (resp1.statusCode) .toEqual (200); const res1 = check_headers (resp1); expect (res1.data.token_type) .toEqual ('bearer'); expect (resp1.headers['set-cookie']) .toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string)); expect (resp1.headers['set-cookie']) .toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string)); await check_token (res1.at as string, 'access_token'); expect (res1.data.expires_in) .toEqual (expires_seconds); await check_token (res1.rt as string, 'refresh_token'); expect (res1.data.refresh_expires_in) .toEqual (refresh_expires_seconds); }); it ('should reject invalid requests', async () => { const resp1 = await get (); expect (resp1.statusCode) .toEqual (400); const res1 = check_headers (resp1); expect (res1.data) .toEqual ({ error: 'invalid_request', error_description: 'unknown authorization type' }); const resp2a = await get ({ authorization: 'Basic foo:bar' }); const res2a = check_headers (resp2a); const resp2b = await get ( { authorization: `Bearer ${res2a.at}` } ); expect (resp2b.statusCode) .toEqual (400); const res2 = check_headers (resp2b); expect (res2.data) .toEqual ({ error: 'invalid_request', error_description: 'invalid bearer type' }); }); it ('should reject an invalid token', async () => { const resp1 = await get ({ authorization: 'Basic foo:bar' }); const res1 = check_headers (resp1); const resp2 = await get ( { authorization: `Bearer ${modify_signature (res1.at)}` } ); expect (resp2.statusCode) .toEqual (401); const res2 = check_headers (resp2); expect (res2.data) .toEqual ({ error: 'invalid_client' }); }); it ('should process part token', async () => { const resp1 = await get ({ authorization: 'Basic part:bar' }); expect (resp1.statusCode) .toEqual (200); const res1 = check_headers (resp1); expect (res1.data.token_type) .toEqual ('bearer'); expect (res1.data.expires_in) .toEqual (part_expires_seconds); await check_token (res1.data.part_token as string, 'part_token'); const resp2 = await get ( { authorization: `Bearer ${res1.data.part_token}` }, 'letmein' ); expect (resp2.statusCode) .toEqual (200); const res2 = check_headers (resp2); expect (res2.data.token_type) .toEqual ('bearer'); expect (resp2.headers['set-cookie']) .toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string)); expect (resp2.headers['set-cookie']) .toContain (build_cookie ({ name: 'mint_cookies' }, res2.rt as string)); await check_token (res2.at as string, 'access_token'); expect (res2.data.expires_in) .toEqual (expires_seconds); expect (res2.at).not.toEqual (res1.at); await check_token (res2.rt as string, 'refresh_token'); expect (res2.data.refresh_expires_in) .toEqual (refresh_expires_seconds); expect (res2.rt).not.toEqual (res1.rt); }); it ('should do immediate redirect', async () => { const resp1 = await get ({ authorization: 'Basic red:irect' }); expect (resp1.statusCode) .toEqual (302); expect (resp1.headers.location) .toEqual ('/redirected'); const signature = extract_cookie ( 'cookie_jar', (resp1.headers['set-cookie'] || []).join ('\n') ); await check_token (signature, 'access_token'); }); it ('should handle any authorization type', async () => { const resp = await get ({ authorization: 'Foo asdefg' }); expect (resp.statusCode) .toEqual (400); expect (JSON.parse (resp.body as string)) .toEqual ({ error: 'invalid_request', error_description: 'unknown authorization type' }); }); it ('should not set content-type when leave-open is specified', async () => { const resp1 = await get ({ authorization: 'Basic leave:open' }); expect (resp1.statusCode) .toEqual (200); expect (resp1.headers['content-type']) .toEqual ('text/plain'); expect (resp1.body) .toEqual ('custom response, true'); const signature = extract_cookie ( 'cookie_jar', (resp1.headers['set-cookie'] || []).join ('\n') ); expect (signature).not.toEqual (''); await check_token (signature, 'access_token'); }); it ('should disallow access and refresh cookies with the same name', () => { expect (() => { create_auth_handler (() => Promise.resolve (), { cookie: { name: 'foo' }, refresh_cookie: { name: 'foo' } }); }) .toThrowError ('access and refresh cookies cannot have the same name'); }); });