304 lines
9.1 KiB
TypeScript
304 lines
9.1 KiB
TypeScript
/*
|
|
* 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
|
|
*/
|
|
|
|
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';
|
|
|
|
const expires_seconds = 600;
|
|
const refresh_expires_seconds = 3600;
|
|
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 };
|
|
}
|
|
|
|
function check_token (token: string, type: string):void {
|
|
const v = 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 ();
|
|
|
|
const ah = create_auth_handler ((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 {
|
|
req.deny ();
|
|
}
|
|
}, {
|
|
cookie_name: 'cookie_jar',
|
|
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 (); }
|
|
}
|
|
}
|
|
});
|
|
|
|
server = http.createServer ((req: IncomingMessage, res: ServerResponse) => {
|
|
ah (req, res);
|
|
});
|
|
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 (`cookie_jar=${res1.at}`);
|
|
|
|
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'])
|
|
.toContain (`cookie_jar=${res2.at}`);
|
|
|
|
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' });
|
|
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 (`cookie_jar=${res1.at}`);
|
|
|
|
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);
|
|
});
|
|
|
|
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 (`cookie_jar=${res1.at}`);
|
|
|
|
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);
|
|
});
|
|
|
|
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);
|
|
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 (`cookie_jar=${res2.at}`);
|
|
|
|
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 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'
|
|
});
|
|
});
|
|
});
|