improved cookie security
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
8f047f2700
commit
3aaaf10fd9
@ -37,6 +37,10 @@ interface AccessResponse {
|
||||
type AuthHandler =
|
||||
(req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
||||
|
||||
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) {
|
||||
|
@ -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*(?<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 {
|
||||
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;
|
||||
|
35
lib/cookie.ts
Normal file
35
lib/cookie.ts
Normal 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 };
|
@ -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",
|
||||
|
@ -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');
|
||||
});
|
||||
|
50
test/spec/cookie.ts
Normal file
50
test/spec/cookie.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user