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

This commit is contained in:
Timo Hocker 2022-01-10 10:06:54 +01:00
parent 3aaaf10fd9
commit cc8762e4ec
Signed by: Timo
GPG Key ID: DFAC2CF4E1D1BEC9
10 changed files with 248 additions and 90 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 3.0.0
- Allows Cookies Parameters to be set
BREAKING:
- All cookie_name and refresh_cookie_name properties have been renamed to cookie and refresh_cookie and are now a settings object instead of a string
## 2.2.0 ## 2.2.0
- Allow refresh tokens to be sent on a separate cookie - Allow refresh tokens to be sent on a separate cookie

View File

@ -1,6 +1,6 @@
# auth-server-helper # auth-server-helper
version: 2.2.x version: 3.0.x
customizable and simple authentication customizable and simple authentication
@ -23,8 +23,8 @@ const {create_gateway} = require('@sapphirecode/auth-server-helper');
const gateway = create_gateway({ const gateway = create_gateway({
redirect_url: '/auth', // if defined, unauthorized requests will be redirected redirect_url: '/auth', // if defined, unauthorized requests will be redirected
cookie_name: 'auth_cookie', // if defined, access tokens will be read from this cookie, cookie: { name: 'auth_cookie' }, // if defined, access tokens will be read from or written to this cookie,
refresh_cookie_name: 'refresh_cookie', // if defined, refresh tokens will be read and used to automatically refresh client tokens (requires the refresh_settings attribute) refresh_cookie: { name: 'refresh_cookie' }, // if defined, refresh tokens will be read and used to automatically refresh client tokens (requires the refresh_settings attribute)
refresh_settings: { refresh_settings: {
// same as settings for allow_access under section 2 // same as settings for allow_access under section 2
// the options data, redirect_to and leave_open are not supported here // the options data, redirect_to and leave_open are not supported here
@ -103,8 +103,8 @@ const handler = create_auth_handler(
// the same works in handlers after the gateway, information is always stored in request.connection.auth // the same works in handlers after the gateway, information is always stored in request.connection.auth
}, },
}, },
cookie_name: 'auth_cookie', // if defined, access tokens will be stored in this cookie, cookie: { name: 'auth_cookie' }, // if defined, access tokens will be stored in this cookie,
refresh_cookie_name: 'refresh_cookie' // if defined, refresh tokens will be stored in this cookie refresh_cookie: { name: 'refresh_cookie' } // if defined, refresh tokens will be stored in this cookie
} }
); );
@ -138,6 +138,27 @@ deny(true);
if this flag is set, no data will be written to the response body and no data will be sent. if this flag is set, no data will be written to the response body and no data will be sent.
Status code and Headers will still be set. Status code and Headers will still be set.
### Defining Custom Cookie Settings
By default all cookies will be sent with 'Secure; HttpOnly; SameSite=Strict' Attributes
In the appropriate settings object, you can set the following options:
```js
{
name: 'foo', // name of the cookies
secure: true, // option to enable or disable the Secure option default: true
http_only: true, // option to enable or disable HttpOnly default: true
same_site: 'Strict', // SameSite property (Strict, Lax or None) default: 'Strict'. Set this to null to disable
expires: 'Mon, 10 Jan 2022 09:28:00 GMT', // Expiry date of the cookie
max_age: 600, // Maximum age in Seconds
domain: 'example.com', // Domain property
path: '/cookies_here' // Path property
}
```
For Documentation on the different Cookie Attributes see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#creating_cookies>
### Invalidating tokens after they are delivered to the client ### Invalidating tokens after they are delivered to the client
```js ```js

View File

@ -9,6 +9,7 @@ import { IncomingMessage, ServerResponse } from 'http';
import { to_utf8 } from '@sapphirecode/encoding-helper'; import { to_utf8 } from '@sapphirecode/encoding-helper';
import auth from './Authority'; import auth from './Authority';
import { debug } from './debug'; import { debug } from './debug';
import { build_cookie, CookieSettings } from './cookie';
const logger = debug ('auth'); const logger = debug ('auth');
@ -37,10 +38,6 @@ 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;
@ -56,8 +53,8 @@ class AuthRequest {
public body: string; public body: string;
private _cookie_name?: string; private _cookie?: CookieSettings;
private _refresh_cookie_name?: string; private _refresh_cookie?: CookieSettings;
private _is_successful: boolean; private _is_successful: boolean;
public get is_successful (): boolean { public get is_successful (): boolean {
@ -68,8 +65,8 @@ class AuthRequest {
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse, res: ServerResponse,
body: string, body: string,
cookie?: string, cookie?: CookieSettings,
refresh_cookie?: string refresh_cookie?: CookieSettings
) { ) {
this.request = req; this.request = req;
this.response = res; this.response = res;
@ -78,8 +75,8 @@ class AuthRequest {
this.is_bearer = false; this.is_bearer = false;
this.user = ''; this.user = '';
this.password = ''; this.password = '';
this._cookie_name = cookie; this._cookie = cookie;
this._refresh_cookie_name = refresh_cookie; this._refresh_cookie = refresh_cookie;
this._is_successful = false; this._is_successful = false;
logger ('started processing new auth request'); logger ('started processing new auth request');
} }
@ -106,7 +103,6 @@ class AuthRequest {
const at = await auth.sign ( const at = await auth.sign (
'access_token', 'access_token',
access_token_expires_in, access_token_expires_in,
{ data } { data }
); );
const result: AccessResult = { access_token_id: at.id }; const result: AccessResult = { access_token_id: at.id };
@ -119,8 +115,8 @@ class AuthRequest {
const cookies = []; const cookies = [];
if (typeof this._cookie_name === 'string') if (typeof this._cookie !== 'undefined')
cookies.push (build_cookie (this._cookie_name, at.signature)); cookies.push (build_cookie (this._cookie, at.signature));
if (include_refresh_token) { if (include_refresh_token) {
logger ('including refresh token'); logger ('including refresh token');
@ -135,9 +131,8 @@ class AuthRequest {
res.refresh_expires_in = refresh_token_expires_in; res.refresh_expires_in = refresh_token_expires_in;
result.refresh_token_id = rt.id; result.refresh_token_id = rt.id;
if (typeof this._refresh_cookie_name === 'string') if (typeof this._refresh_cookie !== 'undefined')
// eslint-disable-next-line max-len cookies.push (build_cookie (this._refresh_cookie, rt.signature));
cookies.push (build_cookie (this._refresh_cookie_name, rt.signature));
} }
if (cookies.length > 0) { if (cookies.length > 0) {
@ -228,8 +223,8 @@ type AuthRequestHandler = (req: AuthRequest) => Promise<void> | void;
interface CreateHandlerOptions { interface CreateHandlerOptions {
refresh?: AccessSettings; refresh?: AccessSettings;
modules?: Record<string, AuthRequestHandler>; modules?: Record<string, AuthRequestHandler>;
cookie_name?: string; cookie?: CookieSettings;
refresh_cookie_name?: string; refresh_cookie?: CookieSettings;
} }
// eslint-disable-next-line max-lines-per-function // eslint-disable-next-line max-lines-per-function
@ -308,8 +303,9 @@ export default function create_auth_handler (
): AuthHandler { ): AuthHandler {
logger ('creating new auth handler'); logger ('creating new auth handler');
if ( if (
typeof options?.cookie_name !== 'undefined' typeof options?.cookie !== 'undefined'
&& options.cookie_name === options.refresh_cookie_name && typeof options?.refresh_cookie !== 'undefined'
&& options.cookie.name === options.refresh_cookie.name
) )
throw new Error ('access and refresh cookies cannot have the same name'); throw new Error ('access and refresh cookies cannot have the same name');
@ -331,8 +327,8 @@ export default function create_auth_handler (
req, req,
res, res,
body, body,
options?.cookie_name, options?.cookie,
options?.refresh_cookie_name options?.refresh_cookie
); );
const token = (/(?<type>\S+) (?<token>.+)/ui) const token = (/(?<type>\S+) (?<token>.+)/ui)
.exec (req.headers.authorization as string); .exec (req.headers.authorization as string);

View File

@ -9,7 +9,7 @@ import { IncomingMessage, ServerResponse } from 'http';
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'; import { extract_cookie, CookieSettings } from './cookie';
const logger = debug ('gateway'); const logger = debug ('gateway');
@ -27,8 +27,8 @@ interface RefreshSettings extends AccessSettings {
interface GatewayOptions { interface GatewayOptions {
redirect_url?: string; redirect_url?: string;
cookie_name?: string; cookie?: CookieSettings;
refresh_cookie_name?: string; refresh_cookie?: CookieSettings;
refresh_settings?: RefreshSettings; refresh_settings?: RefreshSettings;
} }
@ -38,8 +38,9 @@ class GatewayClass {
public constructor (options: GatewayOptions = {}) { public constructor (options: GatewayOptions = {}) {
logger ('creating new gateway'); logger ('creating new gateway');
if ( if (
typeof options.cookie_name === 'string' typeof options?.cookie !== 'undefined'
&& options.cookie_name === options.refresh_cookie_name && typeof options?.refresh_cookie !== 'undefined'
&& options.cookie.name === options.refresh_cookie.name
) )
throw new Error ('access and refresh cookies cannot have the same name'); throw new Error ('access and refresh cookies cannot have the same name');
@ -80,7 +81,7 @@ class GatewayClass {
logger ('authenticating incoming request'); logger ('authenticating incoming request');
let auth = this.get_header_auth (req); let auth = this.get_header_auth (req);
if (auth === null) if (auth === null)
auth = extract_cookie (this._options.cookie_name, req.headers.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;
@ -101,7 +102,7 @@ class GatewayClass {
res: ServerResponse res: ServerResponse
): Promise<boolean> { ): Promise<boolean> {
if ( if (
typeof this._options.refresh_cookie_name === 'undefined' typeof this._options.refresh_cookie === 'undefined'
|| typeof this._options.refresh_settings === 'undefined' || typeof this._options.refresh_settings === 'undefined'
) )
return false; return false;
@ -109,7 +110,7 @@ class GatewayClass {
logger ('trying to apply refresh token'); logger ('trying to apply refresh token');
const refresh = extract_cookie ( const refresh = extract_cookie (
this._options.refresh_cookie_name, this._options.refresh_cookie.name,
req.headers.cookie req.headers.cookie
); );
if (refresh === null) { if (refresh === null) {
@ -124,8 +125,8 @@ class GatewayClass {
req, req,
res, res,
'' ''
, this._options.cookie_name, , this._options.cookie,
this._options.refresh_cookie_name this._options.refresh_cookie
); );
const refresh_result = await auth_request.allow_access ({ const refresh_result = await auth_request.allow_access ({
...this._options.refresh_settings, ...this._options.refresh_settings,

View File

@ -3,8 +3,51 @@ import { debug } from './debug';
const logger = debug ('cookies'); const logger = debug ('cookies');
function build_cookie (name: string, value: string): string { type SameSiteValue = 'Lax' | 'None' | 'Strict';
return `${name}=${value}; Secure; HttpOnly; SameSite=Strict`;
interface CookieSettings {
name: string;
secure?: boolean;
http_only?: boolean;
same_site?: SameSiteValue|null;
expires?: string;
max_age?: number;
domain?: string;
path?: string;
}
const default_settings: Omit<CookieSettings, 'name'> = {
secure: true,
http_only: true,
same_site: 'Strict'
};
function build_cookie (
settings: CookieSettings,
value: string
): string {
const local_settings = { ...default_settings, ...settings };
const sections = [ `${local_settings.name}=${value}` ];
if (local_settings.secure)
sections.push ('Secure');
if (local_settings.http_only)
sections.push ('HttpOnly');
if (
typeof local_settings.same_site !== 'undefined'
&& local_settings.same_site !== null
)
sections.push (`SameSite=${local_settings.same_site}`);
if (typeof local_settings.expires !== 'undefined')
sections.push (`Expires=${local_settings.expires}`);
if (typeof local_settings.max_age !== 'undefined')
sections.push (`Max-Age=${local_settings.max_age}`);
if (typeof local_settings.domain !== 'undefined')
sections.push (`Domain=${local_settings.domain}`);
if (typeof local_settings.path !== 'undefined')
sections.push (`Path=${local_settings.path}`);
return sections.join ('; ');
} }
function extract_cookie ( function extract_cookie (
@ -32,4 +75,4 @@ function extract_cookie (
return result; return result;
} }
export { build_cookie, extract_cookie }; export { build_cookie, extract_cookie, SameSiteValue, CookieSettings };

View File

@ -34,6 +34,10 @@ import keystore, {
KeyStore, KeyStoreExport, KeyStore, KeyStoreExport,
LabelledKey, Key LabelledKey, Key
} from './KeyStore'; } from './KeyStore';
import {
CookieSettings,
SameSiteValue
} from './cookie';
export { export {
create_gateway, create_gateway,
@ -63,5 +67,7 @@ export {
KeyStore, KeyStore,
KeyStoreExport, KeyStoreExport,
LabelledKey, LabelledKey,
Key Key,
CookieSettings,
SameSiteValue
}; };

View File

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

View File

@ -111,8 +111,8 @@ describe ('auth handler', () => {
req.deny (); req.deny ();
} }
}, { }, {
cookie_name: 'cookie_jar', cookie: { name: 'cookie_jar' },
refresh_cookie_name: 'mint_cookies', refresh_cookie: { name: 'mint_cookies' },
refresh: { refresh: {
access_token_expires_in: expires_seconds, access_token_expires_in: expires_seconds,
refresh_token_expires_in: refresh_expires_seconds, refresh_token_expires_in: refresh_expires_seconds,
@ -159,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 (build_cookie ('cookie_jar', res1.at as string)); .toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (build_cookie ('mint_cookies', res1.rt as string)); .toContain (build_cookie ({ name: '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)
@ -180,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 (build_cookie ('cookie_jar', res2.at as string)); .toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string));
expect (resp2.headers['set-cookie']) expect (resp2.headers['set-cookie'])
.toContain (build_cookie ('mint_cookies', res2.rt as string)); .toContain (build_cookie ({ name: '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)
@ -212,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 (build_cookie ('cookie_jar', res1.at as string)); .toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (build_cookie ('mint_cookies', res1.rt as string)); .toContain (build_cookie ({ name: '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)
@ -237,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 (build_cookie ('cookie_jar', res1.at as string)); .toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
expect (resp1.headers['set-cookie']) expect (resp1.headers['set-cookie'])
.toContain (build_cookie ('mint_cookies', res1.rt as string)); .toContain (build_cookie ({ name: '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)
@ -310,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 (build_cookie ('cookie_jar', res2.at as string)); .toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string));
expect (resp2.headers['set-cookie']) expect (resp2.headers['set-cookie'])
.toContain (build_cookie ('mint_cookies', res2.rt as string)); .toContain (build_cookie ({ name: '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)
@ -368,8 +368,8 @@ describe ('auth handler', () => {
it ('should disallow access and refresh cookies with the same name', () => { it ('should disallow access and refresh cookies with the same name', () => {
expect (() => { expect (() => {
create_auth_handler (() => Promise.resolve (), { create_auth_handler (() => Promise.resolve (), {
cookie_name: 'foo', cookie: { name: 'foo' },
refresh_cookie_name: 'foo' refresh_cookie: { name: 'foo' }
}); });
}) })
.toThrowError ('access and refresh cookies cannot have the same name'); .toThrowError ('access and refresh cookies cannot have the same name');

View File

@ -19,9 +19,9 @@ describe ('gateway', () => {
clock_setup (); clock_setup ();
const g = create_gateway ({ const g = create_gateway ({
redirect_url: 'http://localhost/auth', redirect_url: 'http://localhost/auth',
cookie_name: 'cookie_jar', cookie: { name: 'cookie_jar' },
refresh_cookie_name: 'mint_cookies', refresh_cookie: { name: 'mint_cookies' },
refresh_settings: { refresh_settings: {
access_token_expires_in: 600, access_token_expires_in: 600,
include_refresh_token: true, include_refresh_token: true,
@ -162,7 +162,10 @@ describe ('gateway', () => {
it ('should disallow access and refresh cookies with the same name', () => { it ('should disallow access and refresh cookies with the same name', () => {
expect (() => { expect (() => {
create_gateway ({ cookie_name: 'foo', refresh_cookie_name: 'foo' }); create_gateway ({
cookie: { name: 'foo' },
refresh_cookie: { name: 'foo' }
});
}) })
.toThrowError ('access and refresh cookies cannot have the same name'); .toThrowError ('access and refresh cookies cannot have the same name');
}); });

View File

@ -5,44 +5,124 @@
* Created by Timo Hocker <timo@scode.ovh>, January 2022 * Created by Timo Hocker <timo@scode.ovh>, January 2022
*/ */
import { build_cookie, extract_cookie } from '../../lib/cookie'; import { build_cookie, CookieSettings, extract_cookie } from '../../lib/cookie';
interface CreateCookie {
settings: CookieSettings
value: string
result: string
}
const create_cookie_pairs: CreateCookie[] = [
{
settings: { name: 'foo' },
value: 'bar',
result: 'foo=bar; Secure; HttpOnly; SameSite=Strict'
},
{
settings: { name: 'foäöüo' },
value: 'baäöür',
result: 'foäöüo=baäöür; Secure; HttpOnly; SameSite=Strict'
},
{
settings: {
name: 'foo',
secure: true,
http_only: false,
same_site: null
},
value: 'bar',
result: 'foo=bar; Secure'
},
{
settings: {
name: 'foo',
secure: false,
http_only: true,
same_site: null
},
value: 'bar',
result: 'foo=bar; HttpOnly'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: 'Lax'
},
value: 'bar',
result: 'foo=bar; SameSite=Lax'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
expires: 'Tomorrow'
},
value: 'bar',
result: 'foo=bar; Expires=Tomorrow'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
max_age: 600
},
value: 'bar',
result: 'foo=bar; Max-Age=600'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
domain: 'example.com'
},
value: 'bar',
result: 'foo=bar; Domain=example.com'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
path: '/test'
},
value: 'bar',
result: 'foo=bar; Path=/test'
}
];
const parse_cookie_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'
}
];
describe ('cookie', () => { describe ('cookie', () => {
it ('should create a cookie', () => { it ('should create a cookie', () => {
const pairs = [ for (const pair of create_cookie_pairs) {
{ expect (build_cookie (pair.settings, pair.value))
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); .toEqual (pair.result);
} }
}); });
it ('should parse a cookie', () => { it ('should parse a cookie', () => {
const pairs = [ for (const pair of parse_cookie_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)) expect (extract_cookie (pair.name, pair.header))
.toEqual (pair.value); .toEqual (pair.value);
} }