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
## 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
- Allow refresh tokens to be sent on a separate cookie

View File

@ -1,6 +1,6 @@
# auth-server-helper
version: 2.2.x
version: 3.0.x
customizable and simple authentication
@ -23,8 +23,8 @@ const {create_gateway} = require('@sapphirecode/auth-server-helper');
const gateway = create_gateway({
redirect_url: '/auth', // if defined, unauthorized requests will be redirected
cookie_name: 'auth_cookie', // if defined, access tokens will be read from 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)
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_settings: {
// same as settings for allow_access under section 2
// 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
},
},
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
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
}
);
@ -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.
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
```js

View File

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

View File

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

View File

@ -3,8 +3,51 @@ import { debug } from './debug';
const logger = debug ('cookies');
function build_cookie (name: string, value: string): string {
return `${name}=${value}; Secure; HttpOnly; SameSite=Strict`;
type SameSiteValue = 'Lax' | 'None' | '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 (
@ -32,4 +75,4 @@ function extract_cookie (
return result;
}
export { build_cookie, extract_cookie };
export { build_cookie, extract_cookie, SameSiteValue, CookieSettings };

View File

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

View File

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

View File

@ -111,8 +111,8 @@ describe ('auth handler', () => {
req.deny ();
}
}, {
cookie_name: 'cookie_jar',
refresh_cookie_name: 'mint_cookies',
cookie: { name: 'cookie_jar' },
refresh_cookie: { name: 'mint_cookies' },
refresh: {
access_token_expires_in: expires_seconds,
refresh_token_expires_in: refresh_expires_seconds,
@ -159,9 +159,9 @@ describe ('auth handler', () => {
expect (res1.data.token_type)
.toEqual ('bearer');
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'])
.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');
expect (res1.data.expires_in)
@ -180,9 +180,9 @@ describe ('auth handler', () => {
expect (res2.data.token_type)
.toEqual ('bearer');
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'])
.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');
expect (res2.data.expires_in)
@ -212,9 +212,9 @@ describe ('auth handler', () => {
expect (res1.data.token_type)
.toEqual ('bearer');
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'])
.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');
expect (res1.data.expires_in)
@ -237,9 +237,9 @@ describe ('auth handler', () => {
expect (res1.data.token_type)
.toEqual ('bearer');
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'])
.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');
expect (res1.data.expires_in)
@ -310,9 +310,9 @@ describe ('auth handler', () => {
expect (res2.data.token_type)
.toEqual ('bearer');
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'])
.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');
expect (res2.data.expires_in)
@ -368,8 +368,8 @@ describe ('auth handler', () => {
it ('should disallow access and refresh cookies with the same name', () => {
expect (() => {
create_auth_handler (() => Promise.resolve (), {
cookie_name: 'foo',
refresh_cookie_name: 'foo'
cookie: { name: 'foo' },
refresh_cookie: { name: 'foo' }
});
})
.toThrowError ('access and refresh cookies cannot have the same name');

View File

@ -20,8 +20,8 @@ describe ('gateway', () => {
const g = create_gateway ({
redirect_url: 'http://localhost/auth',
cookie_name: 'cookie_jar',
refresh_cookie_name: 'mint_cookies',
cookie: { name: 'cookie_jar' },
refresh_cookie: { name: 'mint_cookies' },
refresh_settings: {
access_token_expires_in: 600,
include_refresh_token: true,
@ -162,7 +162,10 @@ describe ('gateway', () => {
it ('should disallow access and refresh cookies with the same name', () => {
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');
});

View File

@ -5,31 +5,102 @@
* 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';
describe ('cookie', () => {
it ('should create a cookie', () => {
const pairs = [
interface CreateCookie {
settings: CookieSettings
value: string
result: string
}
const create_cookie_pairs: CreateCookie[] = [
{
name: 'foo',
settings: { name: 'foo' },
value: 'bar',
result: 'foo=bar; Secure; HttpOnly; SameSite=Strict'
},
{
name: 'foäöüo',
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'
}
];
for (const pair of pairs) {
expect (build_cookie (pair.name, pair.value))
.toEqual (pair.result);
}
});
it ('should parse a cookie', () => {
const pairs = [
const parse_cookie_pairs = [
{
header: 'foo=bar; Secure; HttpOnly; SameSite=Strict',
name: 'foo',
@ -42,7 +113,16 @@ describe ('cookie', () => {
}
];
for (const pair of pairs) {
describe ('cookie', () => {
it ('should create a cookie', () => {
for (const pair of create_cookie_pairs) {
expect (build_cookie (pair.settings, pair.value))
.toEqual (pair.result);
}
});
it ('should parse a cookie', () => {
for (const pair of parse_cookie_pairs) {
expect (extract_cookie (pair.name, pair.header))
.toEqual (pair.value);
}