From 3aaaf10fd9ef73c798025b654edecb27c7305796 Mon Sep 17 00:00:00 2001 From: Timo Hocker <35867059+TimoHocker@users.noreply.github.com> Date: Sat, 8 Jan 2022 22:10:02 +0100 Subject: [PATCH] improved cookie security --- lib/AuthHandler.ts | 11 ++++++--- lib/Gateway.ts | 40 +++++--------------------------- lib/cookie.ts | 35 ++++++++++++++++++++++++++++ package.json | 2 +- test/spec/AuthHandler.ts | 43 +++++++++++++++++----------------- test/spec/cookie.ts | 50 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 60 deletions(-) create mode 100644 lib/cookie.ts create mode 100644 test/spec/cookie.ts diff --git a/lib/AuthHandler.ts b/lib/AuthHandler.ts index 92eeca4..fc992fc 100644 --- a/lib/AuthHandler.ts +++ b/lib/AuthHandler.ts @@ -37,6 +37,10 @@ interface AccessResponse { type AuthHandler = (req: IncomingMessage, res: ServerResponse) => Promise; +function build_cookie (name: string, value: string): string { + return `${name}=${value}; Secure; HttpOnly; SameSite=Strict`; +} + class AuthRequest { public request: IncomingMessage; public response: ServerResponse; @@ -87,7 +91,7 @@ class AuthRequest { 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 ({ access_token_expires_in, include_refresh_token, @@ -116,7 +120,7 @@ class AuthRequest { const cookies = []; 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) { logger ('including refresh token'); @@ -132,7 +136,8 @@ class AuthRequest { result.refresh_token_id = rt.id; 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) { diff --git a/lib/Gateway.ts b/lib/Gateway.ts index 82f49b0..082b22e 100644 --- a/lib/Gateway.ts +++ b/lib/Gateway.ts @@ -6,10 +6,10 @@ */ import { IncomingMessage, ServerResponse } from 'http'; -import { run_regex } from '@sapphirecode/utilities'; import authority from './Authority'; import { AuthRequest, AccessSettings } from './AuthHandler'; import { debug } from './debug'; +import { extract_cookie } from './cookie'; const logger = debug ('gateway'); @@ -32,11 +32,6 @@ interface GatewayOptions { refresh_settings?: RefreshSettings; } -interface AuthCookies { - access_cookie: string | null; - refresh_cookie: string | null; -} - class GatewayClass { private _options: GatewayOptions; @@ -81,37 +76,11 @@ class GatewayClass { 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*(?[^;=]+)=(?[^;]+)/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 { logger ('authenticating incoming request'); - const cookies = this.get_cookie_auth (req); let auth = this.get_header_auth (req); if (auth === null) - auth = cookies.access_cookie; + auth = extract_cookie (this._options.cookie_name, req.headers.cookie); if (auth === null) { logger ('found no auth token'); return false; @@ -139,7 +108,10 @@ class GatewayClass { 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) { logger ('could not find refresh token'); return false; diff --git a/lib/cookie.ts b/lib/cookie.ts new file mode 100644 index 0000000..f1c5c3f --- /dev/null +++ b/lib/cookie.ts @@ -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*(?[^;=]+)=(?[^;]+)/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 }; diff --git a/package.json b/package.json index 741a7f1..a80a3e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sapphirecode/auth-server-helper", - "version": "2.2.5", + "version": "2.2.6", "main": "dist/index.js", "author": { "name": "Timo Hocker", diff --git a/test/spec/AuthHandler.ts b/test/spec/AuthHandler.ts index 2004ca1..ae969b8 100644 --- a/test/spec/AuthHandler.ts +++ b/test/spec/AuthHandler.ts @@ -15,6 +15,7 @@ import { 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; @@ -37,8 +38,8 @@ function check_headers (resp: Response): CheckHeaderResult { return { data, at, rt }; } -function check_token (token: string, type: string): void { - const v = auth.verify (token); +function check_token (token: string|null, type: string): void { + const v = auth.verify (token || ''); expect (v.valid) .toEqual (true); expect (v.authorized) @@ -158,9 +159,9 @@ describe ('auth handler', () => { expect (res1.data.token_type) .toEqual ('bearer'); expect (resp1.headers['set-cookie']) - .toContain (`cookie_jar=${res1.at}`); + .toContain (build_cookie ('cookie_jar', res1.at as string)); 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'); expect (res1.data.expires_in) @@ -179,9 +180,9 @@ describe ('auth handler', () => { expect (res2.data.token_type) .toEqual ('bearer'); expect (resp2.headers['set-cookie']) - .toContain (`cookie_jar=${res2.at}`); + .toContain (build_cookie ('cookie_jar', res2.at as string)); 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'); expect (res2.data.expires_in) @@ -211,9 +212,9 @@ describe ('auth handler', () => { expect (res1.data.token_type) .toEqual ('bearer'); expect (resp1.headers['set-cookie']) - .toContain (`cookie_jar=${res1.at}`); + .toContain (build_cookie ('cookie_jar', res1.at as string)); 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'); expect (res1.data.expires_in) @@ -236,9 +237,9 @@ describe ('auth handler', () => { expect (res1.data.token_type) .toEqual ('bearer'); expect (resp1.headers['set-cookie']) - .toContain (`cookie_jar=${res1.at}`); + .toContain (build_cookie ('cookie_jar', res1.at as string)); 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'); expect (res1.data.expires_in) @@ -309,9 +310,9 @@ describe ('auth handler', () => { expect (res2.data.token_type) .toEqual ('bearer'); expect (resp2.headers['set-cookie']) - .toContain (`cookie_jar=${res2.at}`); + .toContain (build_cookie ('cookie_jar', res2.at as string)); 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'); expect (res2.data.expires_in) @@ -330,11 +331,10 @@ describe ('auth handler', () => { .toEqual (302); expect (resp1.headers.location) .toEqual ('/redirected'); - let signature = ''; - for (const c of resp1.headers['set-cookie'] as string[]) { - if (c.includes ('cookie_jar=')) - signature = c.replace ('cookie_jar=', ''); - } + const signature = extract_cookie ( + 'cookie_jar', + (resp1.headers['set-cookie'] || []).join ('\n') + ); check_token (signature, 'access_token'); }); @@ -357,11 +357,10 @@ describe ('auth handler', () => { .toEqual ('text/plain'); expect (resp1.body) .toEqual ('custom response, true'); - let signature = ''; - for (const c of resp1.headers['set-cookie'] as string[]) { - if (c.includes ('cookie_jar=')) - signature = c.replace ('cookie_jar=', ''); - } + const signature = extract_cookie ( + 'cookie_jar', + (resp1.headers['set-cookie'] || []).join ('\n') + ); expect (signature).not.toEqual (''); check_token (signature, 'access_token'); }); diff --git a/test/spec/cookie.ts b/test/spec/cookie.ts new file mode 100644 index 0000000..859a39b --- /dev/null +++ b/test/spec/cookie.ts @@ -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 , 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); + } + }); +});