Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional allowed cookieOptions #53

Merged
merged 4 commits into from
Jan 23, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ If you are using a response type that includes `code` (typically combined with a

Additional configuration keys that can be passed to `auth()` on initialization:

- **`appSessionCookie`** - Object defining application session cookie attributes. Allowed keys are `domain`, `httpOnly`, `path`, `secure`, and `sameSite`. Defaults are `true` for `httpOnly` and `Lax` for `sameSite`.
- **`appSessionDuration`** - Integer value, in seconds, for application session duration. Set to `0` to indicate the cookie should be ephemeral (no expiration). Default is 7 days.
- **`appSessionCookie`** - Object defining application session cookie attributes. Allowed keys are `domain`, `ephemeral`, `httpOnly`, `path`, `secure`, and `sameSite`. Defaults are `true` for `httpOnly`, `Lax` for `sameSite`, and `false` for `ephemeral`. See the [Express Response documentation](https://expressjs.com/en/api.html#res.cookie) for more information on all properties except `ephemeral`.
- **`appSessionDuration`** - Integer value, in seconds, for application session duration. Default is 7 days.
- **`appSessionName`** - String value for the cookie name used for the internal session. This value must only include letters, numbers, and underscores. Default is `identity`.
- **`auth0Logout`** - Boolean value to enable Auth0's logout feature. Default is `false`.
- **`authorizationParams`** - Object that describes the authorization server request. [See below](#authorization-params-key) for defaults and more details.
Expand Down
36 changes: 35 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import { AuthorizationParameters, TokenSet, UserinfoResponse } from 'openid-client';
import { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express';

/**
* Configuration parameters passed to the auth() middleware.
*/
interface ConfigParams {
/**
* Object defining application session cookie attributes.
Expand Down Expand Up @@ -131,12 +134,43 @@ interface ConfigParams {
routes?: boolean;
}

/**
* Configuration parameters used in appSessionCookie.
*
* @see https://expressjs.com/en/api.html#res.cookie
*/
interface SessionCookieConfigParams {
/**
* Domain name for the cookie.
*/
domain?: string;

/**
* Set to true to use an ephemeral cookie.
* Defaults to false which will use appSessionDuration as the cookie expiration.
*/
ephemeral?: boolean;

/**
* Flags the cookie to be accessible only by the web server.
* Set to `true` by default in lib/config.
*/
httpOnly?: boolean;

/**
* Path for the cookie.
*/
path?: string;
sameSite?: string;

/**
* Marks the cookie to be used with HTTPS only.
*/
secure?: boolean;

/**
* Value of the “SameSite” Set-Cookie attribute.
*/
sameSite?: string;
}

export function auth(params?: ConfigParams): RequestHandler;
Expand Down
7 changes: 5 additions & 2 deletions lib/appSession.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ module.exports = ({ name, secret, duration, cookieOptions = {} }) => {

if (req[name] && Object.keys(req[name]).length > 0) {
const value = encrypt(JSON.stringify(req[name]), { iat, uat, exp });
const expires = !duration ? 0 : new Date(exp * 1000);

res.cookie(name, value, {expires, ...cookieOptions});
let thisCookieOptions = Object.assign({}, cookieOptions);
joshcanhelp marked this conversation as resolved.
Show resolved Hide resolved
thisCookieOptions.expires = cookieOptions.ephemeral ? 0 : new Date(exp * 1000);
delete thisCookieOptions.ephemeral;

res.cookie(name, value, thisCookieOptions);
}
}

Expand Down
9 changes: 8 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@ const authorizationParamsSchema = Joi.object().keys({
scope: Joi.string().required()
}).unknown(true);

const defaultAppSessionCookie = {
httpOnly: true,
sameSite: 'Lax',
ephemeral: false
};

const appSessionCookieSchema = Joi.object().keys({
domain: Joi.string().optional(),
ephemeral: Joi.boolean().optional(),
httpOnly: Joi.boolean().optional(),
path: Joi.string().optional(),
sameSite: Joi.string().valid(['Lax', 'Strict', 'None']).optional(),
Expand Down Expand Up @@ -90,7 +97,7 @@ If the user provides authorizationParams then
function buildAppSessionCookieConfig(cookieConfig) {

cookieConfig = cookieConfig && Object.keys(cookieConfig).length ? cookieConfig : {};
cookieConfig = Object.assign({ httpOnly: true }, cookieConfig);
cookieConfig = Object.assign({}, defaultAppSessionCookie, cookieConfig);
const cookieConfigValidation = Joi.validate(cookieConfig, appSessionCookieSchema);

if(cookieConfigValidation.error) {
Expand Down
85 changes: 79 additions & 6 deletions test/appSession.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@ const sessionEncryption = require('./fixture/sessionEncryption');
const defaultConfig = {
name: 'identity',
secret: '__test_secret__',
duration: 1234567890,
duration: 3155760000, // 100 years
cookieOptions: {}
};

const req = {
get: (key) => key
};
const res = {};
const next = () => true;

describe('appSession', function() {

describe('no session cookies, no session property', () => {
const appSessionMw = appSession(defaultConfig);
const result = appSessionMw(req, res, next);
const result = appSessionMw(req, {}, next);

it('should call next', function() {
assert.ok(result);
Expand All @@ -33,7 +32,7 @@ describe('appSession', function() {
describe('no session cookies, existing session property', () => {
const appSessionMw = appSession(defaultConfig);
const thisReq = Object.assign({}, req, {identity: {sub: '__test_existing_sub__'}});
const result = appSessionMw(thisReq, res, next);
const result = appSessionMw(thisReq, {}, next);

it('should call next', function() {
assert.ok(result);
Expand All @@ -49,7 +48,7 @@ describe('appSession', function() {
const thisReq = {get: () => 'identity=__invalid_identity__'};

it('should error with malformed identity', function() {
assert.throws(() => appSessionMw(thisReq, res, next), Error, 'JWE malformed or invalid serialization');
assert.throws(() => appSessionMw(thisReq, {}, next), Error, 'JWE malformed or invalid serialization');
});
});

Expand All @@ -58,9 +57,83 @@ describe('appSession', function() {
const thisReq = {get: () => 'identity=' + sessionEncryption.encrypted};

it('should set the identity on req', function() {
const result = appSessionMw(thisReq, res, next);
const result = appSessionMw(thisReq, {}, next);
assert.ok(result);
assert.equal(thisReq.identity.sub, '__test_sub__');

});
});

describe('sessioncookie options', () => {
let cookieArgs;
const thisRes = {
cookie: function cookie() {cookieArgs = JSON.parse(JSON.stringify(arguments)); },
writeHead: () => null,
setHeader: () => null
};

beforeEach(function() {
cookieArgs = {};
});

it('should set the correct cookie by default', function() {
const thisReq = {get: () => 'identity=' + sessionEncryption.encrypted};
const appSessionMw = appSession(defaultConfig);
const result = appSessionMw(thisReq, thisRes, next);
thisRes.writeHead();

assert.ok(result);
assert.equal(cookieArgs['0'], 'identity');
assert.isNotEmpty(cookieArgs['1']);
assert.isObject(cookieArgs['2']);
assert.hasAllKeys(cookieArgs['2'], ['expires']);

const expDate = new Date(cookieArgs['2'].expires);
assert.equal( (expDate.getFullYear() - (new Date()).getFullYear()), 100);
});

it('should set the correct custom cookie name', function() {
const thisReq = {get: () => 'customName=' + sessionEncryption.encrypted};
const customConfig = Object.assign({}, defaultConfig, {name: 'customName'});
const appSessionMw = appSession(customConfig);
const result = appSessionMw(thisReq, thisRes, next);
thisRes.writeHead();

assert.ok(result);
assert.equal(cookieArgs['0'], 'customName');
});

it('should set an ephemeral cookie', function() {
const thisReq = {get: () => 'identity=' + sessionEncryption.encrypted};
const customConfig = Object.assign({}, defaultConfig, {cookieOptions: {ephemeral: true}});
const appSessionMw = appSession(customConfig);
const result = appSessionMw(thisReq, thisRes, next);
thisRes.writeHead();

assert.ok(result);
assert.equal(cookieArgs['2'].expires, 0);
});

it('should pass custom cookie options', function() {
const thisReq = {get: () => 'identity=' + sessionEncryption.encrypted};
const cookieOptConfig = {cookieOptions: {
domain: '__test_domain__',
path: '__test_path__',
secure: true,
httpOnly: false,
sameSite: '__test_samesite__',
}};
const customConfig = Object.assign({}, defaultConfig, cookieOptConfig);
const appSessionMw = appSession(customConfig);
const result = appSessionMw(thisReq, thisRes, next);
thisRes.writeHead();

assert.ok(result);
assert.equal(cookieArgs['2'].domain, '__test_domain__');
assert.equal(cookieArgs['2'].path, '__test_path__');
assert.equal(cookieArgs['2'].secure, true);
assert.equal(cookieArgs['2'].httpOnly, false);
assert.equal(cookieArgs['2'].sameSite, '__test_samesite__');
});
});
});
Loading