auth-server-helper/test/spec/AuthHandler.ts

378 lines
12 KiB
TypeScript
Raw Normal View History

2021-01-03 14:51:07 +01:00
/*
* 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 <timo@scode.ovh>, January 2021
*/
/* eslint-disable max-lines */
2021-01-01 14:14:19 +01:00
import http, { IncomingMessage, ServerResponse } from 'http';
import { to_b64 } from '@sapphirecode/encoding-helper';
2020-12-30 19:39:49 +01:00
import auth from '../../lib/Authority';
2021-01-06 22:43:03 +01:00
import {
2021-01-07 15:43:54 +01:00
clock_finalize,
clock_setup,
2021-01-06 22:43:03 +01:00
get, modify_signature, Response
} from '../Helper';
2021-01-05 21:35:45 +01:00
import { create_auth_handler } from '../../lib/index';
2022-01-08 22:10:02 +01:00
import { build_cookie, extract_cookie } from '../../lib/cookie';
2020-12-30 19:39:49 +01:00
const expires_seconds = 600;
const refresh_expires_seconds = 3600;
2021-01-01 14:14:19 +01:00
const part_expires_seconds = 60;
interface CheckHeaderResult {
at: string;
rt?: string;
data: Record<string, unknown>;
}
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 };
}
2022-01-08 22:10:02 +01:00
function check_token (token: string|null, type: string): void {
const v = auth.verify (token || '');
2021-01-01 14:14:19 +01:00
expect (v.valid)
.toEqual (true);
expect (v.authorized)
.toEqual (type === 'access_token');
expect (v.type)
.toEqual (type);
expect (token)
.toMatch (/^[0-9a-z-._~+/]+$/ui);
}
2020-12-30 19:39:49 +01:00
// eslint-disable-next-line max-lines-per-function
2021-01-01 14:14:19 +01:00
describe ('auth handler', () => {
2020-12-30 19:39:49 +01:00
let server: http.Server|null = null;
2021-01-06 22:43:03 +01:00
// eslint-disable-next-line max-lines-per-function
2020-12-30 19:39:49 +01:00
beforeAll (() => {
2021-01-07 15:43:54 +01:00
clock_setup ();
2021-01-06 22:43:03 +01:00
// eslint-disable-next-line complexity, max-lines-per-function
const ah = create_auth_handler (async (req) => {
if (!req.is_basic && !req.is_bearer) {
2021-01-12 21:19:56 +01:00
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 ();
2021-01-01 14:14:19 +01:00
}
2021-01-03 14:51:07 +01:00
else if (req.user === 'foo' && req.password === 'bar') {
2021-01-01 14:14:19 +01:00
req.allow_access ({
access_token_expires_in: expires_seconds,
include_refresh_token: true,
refresh_token_expires_in: refresh_expires_seconds
});
}
2021-01-03 14:51:07 +01:00
else if (req.user === 'part' && req.password === 'bar') {
req.allow_part (part_expires_seconds, 'two_factor');
}
2021-05-10 12:26:56 +02:00
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
});
2022-01-04 15:01:33 +01:00
req.response.write ('custom response, ');
(req.response.connection as unknown as Record<string, unknown>)
.append_flag = true;
}
2021-01-03 14:51:07 +01:00
else {
req.deny ();
}
2021-01-01 14:14:19 +01:00
}, {
2022-01-10 10:06:54 +01:00
cookie: { name: 'cookie_jar' },
refresh_cookie: { name: 'mint_cookies' },
2022-01-04 21:32:04 +01:00
refresh: {
2021-01-01 14:14:19 +01:00
access_token_expires_in: expires_seconds,
refresh_token_expires_in: refresh_expires_seconds,
include_refresh_token: true
2021-01-03 14:51:07 +01:00
},
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 (); }
}
2021-01-01 14:14:19 +01:00
}
});
2022-01-04 15:01:33 +01:00
server = http.createServer (async (
req: IncomingMessage,
res: ServerResponse
) => {
const is_successful = await ah (req, res);
if ((res.connection as unknown as Record<string, unknown>).append_flag)
res.end (String (is_successful));
2020-12-30 19:39:49 +01:00
});
server.listen (3000);
});
2021-01-06 22:43:03 +01:00
afterAll (() => {
if (server === null)
throw new Error ('server is null');
server.close ();
2021-01-07 15:43:54 +01:00
clock_finalize ();
2021-01-06 22:43:03 +01:00
});
2021-01-01 14:14:19 +01:00
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'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
2022-01-04 21:32:04 +01:00
expect (resp1.headers['set-cookie'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string));
2021-01-01 14:14:19 +01:00
check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in)
.toEqual (expires_seconds);
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'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string));
2022-01-04 21:32:04 +01:00
expect (resp2.headers['set-cookie'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'mint_cookies' }, res2.rt as string));
2021-01-01 14:14:19 +01:00
check_token (res2.at as string, 'access_token');
expect (res2.data.expires_in)
.toEqual (expires_seconds);
expect (res2.at).not.toEqual (res1.at);
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' });
2020-12-30 19:39:49 +01:00
expect (resp.statusCode)
2021-01-01 14:14:19 +01:00
.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)
2020-12-30 19:39:49 +01:00
.toEqual (200);
2021-01-01 14:14:19 +01:00
const res1 = check_headers (resp1);
expect (res1.data.token_type)
2020-12-30 19:39:49 +01:00
.toEqual ('bearer');
2021-01-01 14:14:19 +01:00
expect (resp1.headers['set-cookie'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
2022-01-04 21:32:04 +01:00
expect (resp1.headers['set-cookie'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string));
2021-01-01 14:14:19 +01:00
check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in)
2020-12-30 19:39:49 +01:00
.toEqual (expires_seconds);
2021-01-01 14:14:19 +01:00
check_token (res1.rt as string, 'refresh_token');
expect (res1.data.refresh_expires_in)
2020-12-30 19:39:49 +01:00
.toEqual (refresh_expires_seconds);
2021-01-01 14:14:19 +01:00
});
2021-01-12 21:19:56 +01:00
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'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
2022-01-04 21:32:04 +01:00
expect (resp1.headers['set-cookie'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string));
2021-01-12 21:19:56 +01:00
check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in)
.toEqual (expires_seconds);
check_token (res1.rt as string, 'refresh_token');
expect (res1.data.refresh_expires_in)
.toEqual (refresh_expires_seconds);
});
2021-01-01 14:14:19 +01:00
it ('should reject invalid requests', async () => {
const resp1 = await get ();
expect (resp1.statusCode)
.toEqual (400);
2021-01-01 14:14:19 +01:00
const res1 = check_headers (resp1);
expect (res1.data)
.toEqual ({
error: 'invalid_request',
2021-01-12 21:19:56 +01:00
error_description: 'unknown authorization type'
});
2021-01-01 14:14:19 +01:00
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' });
});
2020-12-30 19:39:49 +01:00
2021-01-03 14:51:07 +01:00
it ('should process part token', async () => {
2021-01-01 14:14:19 +01:00
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);
check_token (res1.data.part_token as string, 'part_token');
const resp2 = await get (
2021-01-03 14:51:07 +01:00
{ authorization: `Bearer ${res1.data.part_token}` },
'letmein'
2021-01-01 14:14:19 +01:00
);
expect (resp2.statusCode)
.toEqual (200);
const res2 = check_headers (resp2);
expect (res2.data.token_type)
.toEqual ('bearer');
expect (resp2.headers['set-cookie'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string));
2022-01-04 21:32:04 +01:00
expect (resp2.headers['set-cookie'])
2022-01-10 10:06:54 +01:00
.toContain (build_cookie ({ name: 'mint_cookies' }, res2.rt as string));
2021-01-01 14:14:19 +01:00
check_token (res2.at as string, 'access_token');
expect (res2.data.expires_in)
.toEqual (expires_seconds);
expect (res2.at).not.toEqual (res1.at);
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);
2020-12-30 19:39:49 +01:00
});
2021-05-10 12:26:56 +02:00
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');
2022-01-08 22:10:02 +01:00
const signature = extract_cookie (
'cookie_jar',
(resp1.headers['set-cookie'] || []).join ('\n')
);
2021-05-10 12:26:56 +02:00
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)
2022-01-04 15:01:33 +01:00
.toEqual ('custom response, true');
2022-01-08 22:10:02 +01:00
const signature = extract_cookie (
'cookie_jar',
(resp1.headers['set-cookie'] || []).join ('\n')
);
expect (signature).not.toEqual ('');
check_token (signature, 'access_token');
});
2022-01-04 21:32:04 +01:00
it ('should disallow access and refresh cookies with the same name', () => {
expect (() => {
create_auth_handler (() => Promise.resolve (), {
2022-01-10 10:06:54 +01:00
cookie: { name: 'foo' },
refresh_cookie: { name: 'foo' }
2022-01-04 21:32:04 +01:00
});
})
.toThrowError ('access and refresh cookies cannot have the same name');
});
2020-12-30 19:39:49 +01:00
});