improved cookie security
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Timo Hocker 2022-01-08 22:10:02 +01:00
parent 8f047f2700
commit 3aaaf10fd9
Signed by: Timo
GPG Key ID: DFAC2CF4E1D1BEC9
6 changed files with 121 additions and 60 deletions

View File

@ -37,6 +37,10 @@ interface AccessResponse {
type AuthHandler = type AuthHandler =
(req: IncomingMessage, res: ServerResponse) => Promise<boolean>; (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
function build_cookie (name: string, value: string): string {
return `${name}=${value}; Secure; HttpOnly; SameSite=Strict`;
}
class AuthRequest { class AuthRequest {
public request: IncomingMessage; public request: IncomingMessage;
public response: ServerResponse; public response: ServerResponse;
@ -87,7 +91,7 @@ class AuthRequest {
this.response.setHeader ('Content-Type', 'application/json'); this.response.setHeader ('Content-Type', 'application/json');
} }
// eslint-disable-next-line max-lines-per-function // eslint-disable-next-line max-statements, max-lines-per-function
public async allow_access ({ public async allow_access ({
access_token_expires_in, access_token_expires_in,
include_refresh_token, include_refresh_token,
@ -116,7 +120,7 @@ class AuthRequest {
const cookies = []; const cookies = [];
if (typeof this._cookie_name === 'string') if (typeof this._cookie_name === 'string')
cookies.push (`${this._cookie_name}=${at.signature}`); cookies.push (build_cookie (this._cookie_name, at.signature));
if (include_refresh_token) { if (include_refresh_token) {
logger ('including refresh token'); logger ('including refresh token');
@ -132,7 +136,8 @@ class AuthRequest {
result.refresh_token_id = rt.id; result.refresh_token_id = rt.id;
if (typeof this._refresh_cookie_name === 'string') if (typeof this._refresh_cookie_name === 'string')
cookies.push (`${this._refresh_cookie_name}=${rt.signature}`); // eslint-disable-next-line max-len
cookies.push (build_cookie (this._refresh_cookie_name, rt.signature));
} }
if (cookies.length > 0) { if (cookies.length > 0) {

View File

@ -6,10 +6,10 @@
*/ */
import { IncomingMessage, ServerResponse } from 'http'; import { IncomingMessage, ServerResponse } from 'http';
import { run_regex } from '@sapphirecode/utilities';
import authority from './Authority'; import authority from './Authority';
import { AuthRequest, AccessSettings } from './AuthHandler'; import { AuthRequest, AccessSettings } from './AuthHandler';
import { debug } from './debug'; import { debug } from './debug';
import { extract_cookie } from './cookie';
const logger = debug ('gateway'); const logger = debug ('gateway');
@ -32,11 +32,6 @@ interface GatewayOptions {
refresh_settings?: RefreshSettings; refresh_settings?: RefreshSettings;
} }
interface AuthCookies {
access_cookie: string | null;
refresh_cookie: string | null;
}
class GatewayClass { class GatewayClass {
private _options: GatewayOptions; private _options: GatewayOptions;
@ -81,37 +76,11 @@ class GatewayClass {
return auth.groups?.data; return auth.groups?.data;
} }
public get_cookie_auth (req: IncomingMessage): AuthCookies {
logger ('extracting tokens from cookies');
const result: AuthCookies = {
access_cookie: null,
refresh_cookie: null
};
const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu;
run_regex (
cookie_regex,
req.headers.cookie,
(res: RegExpMatchArray) => {
logger ('parsing cookie %s', res.groups?.name);
if (res.groups?.name === this._options.cookie_name)
result.access_cookie = res.groups?.value as string;
else if (res.groups?.name === this._options.refresh_cookie_name)
result.refresh_cookie = res.groups?.value as string;
}
);
logger ('parsed cookies: %O', result);
return result;
}
public try_access (req: IncomingMessage): boolean { public try_access (req: IncomingMessage): boolean {
logger ('authenticating incoming request'); logger ('authenticating incoming request');
const cookies = this.get_cookie_auth (req);
let auth = this.get_header_auth (req); let auth = this.get_header_auth (req);
if (auth === null) if (auth === null)
auth = cookies.access_cookie; auth = extract_cookie (this._options.cookie_name, req.headers.cookie);
if (auth === null) { if (auth === null) {
logger ('found no auth token'); logger ('found no auth token');
return false; return false;
@ -139,7 +108,10 @@ class GatewayClass {
logger ('trying to apply refresh token'); logger ('trying to apply refresh token');
const refresh = this.get_cookie_auth (req).refresh_cookie; const refresh = extract_cookie (
this._options.refresh_cookie_name,
req.headers.cookie
);
if (refresh === null) { if (refresh === null) {
logger ('could not find refresh token'); logger ('could not find refresh token');
return false; return false;

35
lib/cookie.ts Normal file
View File

@ -0,0 +1,35 @@
import { run_regex } from '@sapphirecode/utilities';
import { debug } from './debug';
const logger = debug ('cookies');
function build_cookie (name: string, value: string): string {
return `${name}=${value}; Secure; HttpOnly; SameSite=Strict`;
}
function extract_cookie (
name: string|undefined,
header: string|undefined
): string| null {
logger (`extracting cookie ${name}`);
const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu;
let result = null;
run_regex (
cookie_regex,
header,
(res: RegExpMatchArray) => {
logger ('parsing cookie %s', res.groups?.name);
if (res.groups?.name === name) {
logger ('found cookie');
result = res.groups?.value as string;
}
}
);
return result;
}
export { build_cookie, extract_cookie };

View File

@ -1,6 +1,6 @@
{ {
"name": "@sapphirecode/auth-server-helper", "name": "@sapphirecode/auth-server-helper",
"version": "2.2.5", "version": "2.2.6",
"main": "dist/index.js", "main": "dist/index.js",
"author": { "author": {
"name": "Timo Hocker", "name": "Timo Hocker",

View File

@ -15,6 +15,7 @@ import {
get, modify_signature, Response get, modify_signature, Response
} from '../Helper'; } from '../Helper';
import { create_auth_handler } from '../../lib/index'; import { create_auth_handler } from '../../lib/index';
import { build_cookie, extract_cookie } from '../../lib/cookie';
const expires_seconds = 600; const expires_seconds = 600;
const refresh_expires_seconds = 3600; const refresh_expires_seconds = 3600;
@ -37,8 +38,8 @@ function check_headers (resp: Response): CheckHeaderResult {
return { data, at, rt }; return { data, at, rt };
} }
function check_token (token: string, type: string): void { function check_token (token: string|null, type: string): void {
const v = auth.verify (token); const v = auth.verify (token || '');
expect (v.valid) expect (v.valid)
.toEqual (true); .toEqual (true);
expect (v.authorized) expect (v.authorized)
@ -158,9 +159,9 @@ describe ('auth handler', () => {
expect (res1.data.token_type) expect (res1.data.token_type)
.toEqual ('bearer'); .toEqual ('bearer');
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (`cookie_jar=${res1.at}`); .toContain (build_cookie ('cookie_jar', res1.at as string));
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (`mint_cookies=${res1.rt}`); .toContain (build_cookie ('mint_cookies', res1.rt as string));
check_token (res1.at as string, 'access_token'); check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in) expect (res1.data.expires_in)
@ -179,9 +180,9 @@ describe ('auth handler', () => {
expect (res2.data.token_type) expect (res2.data.token_type)
.toEqual ('bearer'); .toEqual ('bearer');
expect (resp2.headers['set-cookie']) expect (resp2.headers['set-cookie'])
.toContain (`cookie_jar=${res2.at}`); .toContain (build_cookie ('cookie_jar', res2.at as string));
expect (resp2.headers['set-cookie']) expect (resp2.headers['set-cookie'])
.toContain (`mint_cookies=${res2.rt}`); .toContain (build_cookie ('mint_cookies', res2.rt as string));
check_token (res2.at as string, 'access_token'); check_token (res2.at as string, 'access_token');
expect (res2.data.expires_in) expect (res2.data.expires_in)
@ -211,9 +212,9 @@ describe ('auth handler', () => {
expect (res1.data.token_type) expect (res1.data.token_type)
.toEqual ('bearer'); .toEqual ('bearer');
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (`cookie_jar=${res1.at}`); .toContain (build_cookie ('cookie_jar', res1.at as string));
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (`mint_cookies=${res1.rt}`); .toContain (build_cookie ('mint_cookies', res1.rt as string));
check_token (res1.at as string, 'access_token'); check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in) expect (res1.data.expires_in)
@ -236,9 +237,9 @@ describe ('auth handler', () => {
expect (res1.data.token_type) expect (res1.data.token_type)
.toEqual ('bearer'); .toEqual ('bearer');
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (`cookie_jar=${res1.at}`); .toContain (build_cookie ('cookie_jar', res1.at as string));
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (`mint_cookies=${res1.rt}`); .toContain (build_cookie ('mint_cookies', res1.rt as string));
check_token (res1.at as string, 'access_token'); check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in) expect (res1.data.expires_in)
@ -309,9 +310,9 @@ describe ('auth handler', () => {
expect (res2.data.token_type) expect (res2.data.token_type)
.toEqual ('bearer'); .toEqual ('bearer');
expect (resp2.headers['set-cookie']) expect (resp2.headers['set-cookie'])
.toContain (`cookie_jar=${res2.at}`); .toContain (build_cookie ('cookie_jar', res2.at as string));
expect (resp2.headers['set-cookie']) expect (resp2.headers['set-cookie'])
.toContain (`mint_cookies=${res2.rt}`); .toContain (build_cookie ('mint_cookies', res2.rt as string));
check_token (res2.at as string, 'access_token'); check_token (res2.at as string, 'access_token');
expect (res2.data.expires_in) expect (res2.data.expires_in)
@ -330,11 +331,10 @@ describe ('auth handler', () => {
.toEqual (302); .toEqual (302);
expect (resp1.headers.location) expect (resp1.headers.location)
.toEqual ('/redirected'); .toEqual ('/redirected');
let signature = ''; const signature = extract_cookie (
for (const c of resp1.headers['set-cookie'] as string[]) { 'cookie_jar',
if (c.includes ('cookie_jar=')) (resp1.headers['set-cookie'] || []).join ('\n')
signature = c.replace ('cookie_jar=', ''); );
}
check_token (signature, 'access_token'); check_token (signature, 'access_token');
}); });
@ -357,11 +357,10 @@ describe ('auth handler', () => {
.toEqual ('text/plain'); .toEqual ('text/plain');
expect (resp1.body) expect (resp1.body)
.toEqual ('custom response, true'); .toEqual ('custom response, true');
let signature = ''; const signature = extract_cookie (
for (const c of resp1.headers['set-cookie'] as string[]) { 'cookie_jar',
if (c.includes ('cookie_jar=')) (resp1.headers['set-cookie'] || []).join ('\n')
signature = c.replace ('cookie_jar=', ''); );
}
expect (signature).not.toEqual (''); expect (signature).not.toEqual ('');
check_token (signature, 'access_token'); check_token (signature, 'access_token');
}); });

50
test/spec/cookie.ts Normal file
View File

@ -0,0 +1,50 @@
/*
* 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 2022
*/
import { build_cookie, extract_cookie } from '../../lib/cookie';
describe ('cookie', () => {
it ('should create a cookie', () => {
const pairs = [
{
name: 'foo',
value: 'bar',
result: 'foo=bar; Secure; HttpOnly; SameSite=Strict'
},
{
name: 'foäöüo',
value: 'baäöür',
result: 'foäöüo=baäöür; Secure; HttpOnly; SameSite=Strict'
}
];
for (const pair of pairs) {
expect (build_cookie (pair.name, pair.value))
.toEqual (pair.result);
}
});
it ('should parse a cookie', () => {
const pairs = [
{
header: 'foo=bar; Secure; HttpOnly; SameSite=Strict',
name: 'foo',
value: 'bar'
},
{
header: '134=567;foäöüo=baäöür;tesT=123',
name: 'foäöüo',
value: 'baäöür'
}
];
for (const pair of pairs) {
expect (extract_cookie (pair.name, pair.header))
.toEqual (pair.value);
}
});
});