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

App session settings #68

Merged
merged 6 commits into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 11 additions & 4 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Please see the [Getting Started section of the README](https://github.com/auth0/

The `auth()` middleware has a few configuration keys that are required for initialization.

- **`appSessionSecret`** - The secret used to derive an encryption key for the user identity in a session cookie. It must be a string, an array of strings, or `false` to skip this internal storage and provide your own session mechanism in `getUser`. When array is provided the first member is used for signing and other members can be used for decrypting old cookies, this is to enable appSessionSecret rotation. This can be set automatically with an `APP_SESSION_SECRET` variable in your environment.
- **`appSession.secret`** - The secret used to derive an encryption key for the user identity in a session cookie. It must be a string or an array of strings to use the built-in encrypted cookie session. When an array is provided, the first member is used for signing and other members can be used for decrypting old cookies (to enable secret rotation). This can be set automatically with an `APP_SESSION_SECRET` variable in your environment.
- **`baseURL`** - The root URL for the application router. This can be set automatically with a `BASE_URL` variable in your environment.
- **`clientID`** - The Client ID for your application. This can be set automatically with a `CLIENT_ID` variable in your environment.
- **`issuerBaseURL`** - The root URL for the token issuer with no trailing slash. In Auth0, this is your Application's **Domain** prepended with `https://`. This can be set automatically with an `ISSUER_BASE_URL` variable in your environment.
Expand All @@ -21,9 +21,16 @@ 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`, `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`.
- **`appSession`** - Object defining application session configuration. If this is set to `false`, the internal storage will not be used (see [this example](https://github.com/auth0/express-openid-connect/blob/master/EXAMPLES.md#4-custom-user-session-handling) for how to provide your own session mechanism). Otherwise, the `secret` key is required (see above).
- **`appSession.secret`** - See the **Required Keys** section above.
- **`appSession.duration`** - Integer value, in seconds, for application session duration. Default is 7 days.
- **`appSession.name`** - String value for the cookie name used for the internal session. This value must only include letters, numbers, and underscores. Default is `appSession`.
- **`appSession.cookieTransient`** - Sets the application session cookie expiration to `0` to create a transient cookie. Set to `false` by default.
- **`appSession.cookieDomain`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `domain`.
- **`appSession.cookieHttpOnly`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `httponly`. Set to `true` by default.
- **`appSession.cookiePath`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `path`.
- **`appSession.cookieSecure`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `secure`.
- **`appSession.cookieSameSite`** - Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `samesite`. Set to `"Lax"` by default.
- **`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.
- **`clockTolerance`** - Integer value for the system clock's tolerance (leeway) in seconds for ID token verification. Default is `60`.
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

This release includes important changes to user session and token handling which will require an update for all applications.

First, a new, required configuration key - `appSessionSecret`- has been added. The value here will be used to generate keys which are in turn used to encrypt the user identity returned from the identity provider. This encrypted and signed identity is stored in a cookie and used to populate the `req.openid.user` property, as before. This key should be set to either a secure, random value to use this built-in session or `false` to provide [your own custom application session handling](https://github.com/auth0/express-openid-connect/blob/master/EXAMPLES.md#4-custom-user-session-handling). A value for this can be generated with `openssl` like so:
First, a new, required configuration key - `appSessionSecret` (changed to `appSession.secret` in v0.8.0) - has been added. The value here will be used to generate keys which are in turn used to encrypt the user identity returned from the identity provider. This encrypted and signed identity is stored in a cookie and used to populate the `req.openid.user` property, as before. This key should be set to either a secure, random value to use this built-in session or `false` to provide [your own custom application session handling](https://github.com/auth0/express-openid-connect/blob/master/EXAMPLES.md#4-custom-user-session-handling). A value for this can be generated with `openssl` like so:

```
❯ openssl rand -hex 32
Expand Down
12 changes: 6 additions & 6 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ app.use(session({

app.use(auth({
// Setting this configuration key to false will turn off internal session handling.
appSessionSecret: false,
appSession: false,
handleCallback: async function (req, res, next) {
// This will store the user identity claims in the session
req.session.userIdentity = req.openidTokens.claims();
Expand Down Expand Up @@ -206,9 +206,9 @@ app.get('/route-that-calls-an-api', async (req, res, next) => {
req.session.openidTokens = tokenSet;

// You can also refresh the session with a returned ID token.
// The req property below is the same as appSessionName, which defaults to "identity".
// The req property below is the same as appSession.name, which defaults to "appSession".
// If you're using custom session handling, the claims might be stored elsewhere.
req.identity.claims = tokenSet.claims();
req.appSession.claims = tokenSet.claims();
}

// Check for and use tokenSet.access_token for the API call ...
Expand All @@ -217,15 +217,15 @@ app.get('/route-that-calls-an-api', async (req, res, next) => {

## 7. Calling userinfo

If your application needs to call the userinfo endpoint for the user's identity instead of the ID token used by default, add a `handleCallback` function during initialization that will make this call. Save the claims retrieved from the userinfo endpoint to the `appSessionName` on the request object (default is `identity`):
If your application needs to call the userinfo endpoint for the user's identity instead of the ID token used by default, add a `handleCallback` function during initialization that will make this call. Save the claims retrieved from the userinfo endpoint to the `appSession.name` on the request object (default is `appSession`):

```js
app.use(auth({
handleCallback: async function (req, res, next) {
const client = req.openid.client;
req.identity = req.identity || {};
req.appSession = req.appSession || {};
try {
req.identity.claims = await client.userinfo(req.openidTokens);
req.appSession.claims = await client.userinfo(req.openidTokens);
next();
} catch(e) {
next(e);
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ app.use(
issuerBaseURL: "https://YOUR_DOMAIN",
baseURL: "https://YOUR_APPLICATION_ROOT_URL",
clientID: "YOUR_CLIENT_ID",
appSessionSecret: "LONG_RANDOM_STRING"
appSession: {
secret: "LONG_RANDOM_STRING"
}
})
);
```
Expand Down
68 changes: 35 additions & 33 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,7 @@ interface ConfigParams {
/**
* Object defining application session cookie attributes.
*/
appSessionCookie?: SessionCookieConfigParams;

/**
* Integer value, in seconds, for application session duration.
*/
appSessionDuration?: number;

/**
* String value for the cookie name used for the internal session.
*/
appSessionName?: string;

/**
* REQUIRED. The secret(s) used to derive an encryption key for the user identity in a session cookie.
* Use a single string key or array of keys for an encrypted session cookie or false to skip.
* Can use env key APP_SESSION_SECRET instead.
*/
appSessionSecret?: boolean | string | string[];
appSession: boolean | AppSessionConfigParams;

/**
* Boolean value to enable Auth0's logout feature.
Expand Down Expand Up @@ -166,42 +149,61 @@ interface ConfigParams {
}

/**
* Configuration parameters used in appSessionCookie.
*
* @see https://expressjs.com/en/api.html#res.cookie
* Configuration parameters used for the application session.
*/
interface SessionCookieConfigParams {
interface AppSessionConfigParams {
/**
* REQUIRED. The secret(s) used to derive an encryption key for the user identity in a session cookie.
* Use a single string key or array of keys for an encrypted session cookie.
* Can use env key APP_SESSION_SECRET instead.
*/
secret?: string | Array<string>;

/**
* String value for the cookie name used for the internal session.
* This value must only include letters, numbers, and underscores.
* Default is `appSession`.
*/
name?: string;

/**
* Integer value, in seconds, for application session duration.
* Default is 604800 seconds (7 days).
*/
duration?: number

/**
* Domain name for the cookie.
*/
domain?: string;
cookieDomain?: string;

/**
* Set to true to use an ephemeral cookie.
* Defaults to false which will use appSessionDuration as the cookie expiration.
* Set to true to use a transient cookie (cookie without an explicit expiration).
* Defaults to `false` which will use appSession.duration as the cookie expiration.
*/
ephemeral?: boolean;
cookieTransient?: boolean;

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

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

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

/**
* Value of the “SameSite” Set-Cookie attribute.
* Value of the SameSite Set-Cookie attribute.
* Defaults to "Lax" but will be adjusted based on response_type.
*/
sameSite?: string;
cookieSameSite?: string;
}

export function auth(params?: ConfigParams): RequestHandler;
Expand Down
49 changes: 27 additions & 22 deletions lib/appSession.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
const { strict: assert } = require('assert');
Copy link
Contributor Author

@joshcanhelp joshcanhelp Feb 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No intended changes to session or encryption functionality in this file.


const { JWK, JWKS, JWE } = require('jose');
const onHeaders = require('on-headers');
const cookie = require('cookie');
const hkdf = require('futoin-hkdf');

const { sessionNameDefault, sessionDurationDefault } = require('./config');

const deriveKey = (secret) => hkdf(secret, 32, { info: 'JWE CEK', hash: 'SHA-256' });
const epoch = () => Date.now() / 1000 | 0;

module.exports = ({ name, secret, duration, cookieOptions = {} }) => {
module.exports = (sessionConfig) => {
let current;

const COOKIES = Symbol('cookies');
const alg = 'dir';
const enc = 'A256GCM';
const sessionSecrets = Array.isArray(sessionConfig.secret) ? sessionConfig.secret : [sessionConfig.secret];
const sessionName = sessionConfig.name || sessionNameDefault;
const sessionDuration = sessionConfig.duration || sessionDurationDefault;

let keystore = new JWKS.KeyStore();

if (!Array.isArray(secret)) {
secret = [secret];
}

secret.forEach((secretString, i) => {
sessionSecrets.forEach((secretString, i) => {
const key = JWK.asKey(deriveKey(secretString));
if (i === 0) {
current = key;
Expand All @@ -41,20 +41,25 @@ module.exports = ({ name, secret, duration, cookieOptions = {} }) => {
return JWE.decrypt(jwe, keystore, { complete: true, algorithms: [enc] });
}

function setCookie (req, res, { uat = epoch(), iat = uat, exp = uat + duration }) {
if ((!req[name] || !Object.keys(req[name]).length) && name in req[COOKIES]) {
res.clearCookie(name);
function setCookie (req, res, { uat = epoch(), iat = uat, exp = uat + sessionDuration }) {
if ((!req[sessionName] || !Object.keys(req[sessionName]).length) && sessionName in req[COOKIES]) {
res.clearCookie(sessionName);
return;
}

if (req[name] && Object.keys(req[name]).length > 0) {
const value = encrypt(JSON.stringify(req[name]), { iat, uat, exp });
if (req[sessionName] && Object.keys(req[sessionName]).length > 0) {
const value = encrypt(JSON.stringify(req[sessionName]), { iat, uat, exp });

const cookieOptions = {};
Object.keys(sessionConfig).filter(key => /^cookie/.test(key)).forEach(function(key) {
const cookieOptionKey = key.replace('cookie', '').toLowerCase();
cookieOptions[cookieOptionKey] = sessionConfig[key];
});

const thisCookieOptions = Object.assign({}, cookieOptions);
thisCookieOptions.expires = cookieOptions.ephemeral ? 0 : new Date(exp * 1000);
delete thisCookieOptions.ephemeral;
cookieOptions.expires = cookieOptions.transient ? 0 : new Date(exp * 1000);
delete cookieOptions.transient;

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

Expand All @@ -63,7 +68,7 @@ module.exports = ({ name, secret, duration, cookieOptions = {} }) => {
req[COOKIES] = cookie.parse(req.get('cookie') || '');
}

if (req.hasOwnProperty(name)) {
if (req.hasOwnProperty(sessionName)) {
return next();
}

Expand All @@ -72,15 +77,15 @@ module.exports = ({ name, secret, duration, cookieOptions = {} }) => {

try {

if (req[COOKIES].hasOwnProperty(name)) {
const { protected: header, cleartext } = decrypt(req[COOKIES][name]);
if (req[COOKIES].hasOwnProperty(sessionName)) {
const { protected: header, cleartext } = decrypt(req[COOKIES][sessionName]);
({ iat, exp } = header);
assert(exp > epoch());
req[name] = JSON.parse(cleartext);
req[sessionName] = JSON.parse(cleartext);
}
} finally {
if (!req.hasOwnProperty(name) || !req[name]) {
req[name] = {};
if (!req.hasOwnProperty(sessionName) || !req[sessionName]) {
req[sessionName] = {};
}
}

Expand Down
43 changes: 27 additions & 16 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ const { defaultState: getLoginState } = require('./hooks/getLoginState');
const getUser = require('./hooks/getUser');
const handleCallback = require('./hooks/handleCallback');

const sessionDurationDefault = (7 * 24 * 60 * 60); // 7 days
const sessionNameDefault = 'appSession';

const paramsSchema = Joi.object({
appSessionCookie: Joi.object({
domain: Joi.string().optional(),
ephemeral: Joi.boolean().optional().default(false),
httpOnly: Joi.boolean().optional().default(true),
path: Joi.string().optional(),
sameSite: Joi.string().valid('Lax', 'Strict', 'None').optional().default('Lax'),
secure: Joi.boolean().optional()
}).optional().unknown(false).default(),
appSessionDuration: Joi.number().integer().optional().default(7 * 24 * 60 * 60),
appSessionName: Joi.string().token().optional().default('identity'),
appSessionSecret: Joi.alternatives([
Joi.string(),
Joi.array().items(Joi.string()),
Joi.boolean().valid(false)
appSession: Joi.alternatives([
Joi.boolean().valid(false),
Joi.object({
secret: Joi.alternatives([
Joi.string().min(8),
Joi.array().items(Joi.string().min(8))
]).required(),
duration: Joi.number().integer().optional().default(sessionDurationDefault),
name: Joi.string().token().optional().default(sessionNameDefault),
cookieDomain: Joi.string().optional(),
cookieTransient: Joi.boolean().optional().default(false),
cookieHttpOnly: Joi.boolean().optional().default(true),
cookiePath: Joi.string().optional(),
cookieSameSite: Joi.string().valid('Lax', 'Strict', 'None').optional().default('Lax'),
cookieSecure: Joi.boolean().optional()
}).unknown(false)
]).required(),
auth0Logout: Joi.boolean().optional().default(false),
authorizationParams: Joi.object({
Expand Down Expand Up @@ -81,14 +86,20 @@ module.exports.get = function(params) {
issuerBaseURL: process.env.ISSUER_BASE_URL,
baseURL: process.env.BASE_URL,
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
appSessionSecret: process.env.APP_SESSION_SECRET,
clientSecret: process.env.CLIENT_SECRET
}, config);

if (process.env.APP_SESSION_SECRET && typeof config.appSession === 'object') {
config.appSession.secret = config.appSession.secret || process.env.APP_SESSION_SECRET;
}

const paramsValidation = paramsSchema.validate(config);
if (paramsValidation.error) {
throw new Error(paramsValidation.error.details[0].message);
}

return paramsValidation.value;
};

module.exports.sessionDurationDefault = sessionDurationDefault;
module.exports.sessionNameDefault = sessionNameDefault;
13 changes: 8 additions & 5 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,24 +103,27 @@ class ResponseContext {
const next = cb(this._next).once();
const req = this._req;
const res = this._res;
const config = this._config;

let returnURL = params.returnTo || req.query.returnTo || this._config.postLogoutRedirectUri;
let returnURL = params.returnTo || req.query.returnTo || config.postLogoutRedirectUri;

if (url.parse(returnURL).host === null) {
returnURL = urlJoin(this._config.baseURL, returnURL);
returnURL = urlJoin(config.baseURL, returnURL);
}

if (!req.isAuthenticated()) {
return res.redirect(returnURL);
}

req[this._config.appSessionName] = undefined;
if (config.appSession) {
req[config.appSession.name] = undefined;
}

if (!this._config.idpLogout) {
if (!config.idpLogout) {
return res.redirect(returnURL);
}

const client = this._req.openid.client;
const client = req.openid.client;
try {
returnURL = client.endSessionUrl({
post_logout_redirect_uri: returnURL,
Expand Down
Loading