From e8a6d6cfe6a039baf79a038cdd150afe5eb24460 Mon Sep 17 00:00:00 2001 From: Iain Collins Date: Thu, 9 Feb 2017 16:07:43 +0000 Subject: [PATCH] Now silently revalidates sessions periodically This resolves issue #6. The client how checks 'clientMaxAge', which is set on the server (along side all the other session options) and uses that value to determine if it it should fetch the session data from the server or from the cache on disk. Note that the cache on disk is shared between tabs, so logging you out on one will cause you also to be logged out on the other pages when you next interact with the site regardless. This option just ensures the client will never stray too far from the data on the server (e.g. in the event a users session information is updated or forceably expired on the server). --- components/session.js | 21 +++++++++++++-------- index.js | 8 ++++---- package.json | 2 +- routes/auth.js | 32 ++++++++++++++++++++++---------- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/components/session.js b/components/session.js index 06ef2df..4f3469e 100644 --- a/components/session.js +++ b/components/session.js @@ -66,15 +66,18 @@ export default class Session { // Attempt to load session data from sessionStore on every call this._session = this._getSessionStore() + + //console.log("Time left till session expires in seconds: "+((this._session.expires - Date.now()) / 1000)) - if (window.session && this._session && Object.keys(this._session).length > 0 && forceUpdate !== true) { - // If we have a populated session object already AND forceUpdate is not - // set to true then return the session data we have already + // If session data exists, has not expired AND forceUpdate is not set then + // return the stored session we already have. + if (this._session && Object.keys(this._session).length > 0 && this._session.expires > Date.now() && forceUpdate !== true) { return new Promise((resolve) => { resolve(this._session) }) } else { - // If we don't have session data (or forceUpdate is true) then get it + // If we don't have session data, or it's expired, or forceUpdate is set + // to true then revalidate it by fetching it again from the server. return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest() xhr.open("GET", '/auth/session', true) @@ -84,12 +87,13 @@ export default class Session { // Update session with session info this._session = JSON.parse(xhr.responseText) + // Set a value we will use to check this client should silently + // revalidate based on the value of clientMaxAge set by the server + this._session.expires = Date.now() + this._session.clientMaxAge + // Save changes to session this._setSessionStore(this._session) - - // Save session to window.session object - window.session = this._session - + resolve(this._session) } else { reject(Error('XMLHttpRequest failed: Unable to get session')) @@ -149,6 +153,7 @@ export default class Session { // Set isLoggedIn to false and destory user object this._session.csrfToken = await Session.getCsrfToken() this._session.isLoggedIn = false + this._session.expires = Date.now() delete this._session.user // Save changes to session diff --git a/index.js b/index.js index 31eabd8..d3c3778 100644 --- a/index.js +++ b/index.js @@ -36,10 +36,10 @@ app.prepare() // Define user object const User = db.define("user", { - name : { type: "text" }, - email : { type: "text", unique: true }, - token : { type: "text" }, - verified : { type: "boolean", defaultValue: false } + name : { type: "text" }, + email : { type: "text", unique: true }, + token : { type: "text" }, + verified : { type: "boolean", defaultValue: false } }) // Create table diff --git a/package.json b/package.json index 7ebf1fb..bf4fa5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nextjs-starter", - "version": "2.4.3", + "version": "2.4.4", "description": "A starter Next.js project", "main": "index.js", "dependencies": { diff --git a/routes/auth.js b/routes/auth.js index de3923d..85d818f 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -27,25 +27,34 @@ exports.configure = (app, server, options) => { const User = options.db.models.user // Base path for auth URLs - const path = (options.path) ? options.path : '/auth' + const path = options.path || '/auth' // Directory for auth pages - const pages = (options.pages) ? options.pages : 'auth' + const pages = options.pages || 'auth' // The secret is used to encrypt/decrypt sessions (you should pass your own!) - const secret = (options.secret) ? options.secret : 'AAAA-BBBB-CCCC-DDDD' + const secret = options.secret || 'AAAA-BBBB-CCCC-DDDD' // Configure session store (defaults to using file system) - const store = (options.store) ? options.store : new FileStore({ path: '/tmp/sessions', secret: secret }) + const store = options.store || new FileStore({ path: '/tmp/sessions', secret: secret }) - // Max cookie age (default is 4 weeks) - const maxAge = (options.maxAge) ? options.maxAge : 3600000 * 24 * 7 * 4 + // Max session age in ms (default is 4 weeks) + // NB: With 'rolling: true' passed to session() the session expiry time will + // be reset every time a user visits the site again before it expires. + const maxAge = options.maxAge || 60000 * 60 * 24 * 7 * 4 + + // How often the client should revalidate the session in ms (default 60s) + // Does not impact the session life on the server, but causes the client to + // always refetch session info after N seconds has elapsed since last checked. + // Sensible values are between 0 (always check the server) and a few minutes. + const clientMaxAge = options.clientMaxAge || 60000 - // URL of the server (e.g. "http://www.example.com"), autodetects if null - const serverUrl = (options.serverUrl) ? options.serverUrl : null + // URL of the server (e.g. "http://www.example.com"). Used when sending + // sign-in emails. Autodetects current server hostname / domain if null. + const serverUrl = options.serverUrl || null - // Mailserver (defaults to sending from localhost) - const mailserver = (options.mailserver) ? options.mailserver : null + // Mailserver (defaults to sending from localhost if null) + const mailserver = options.mailserver || null // Load body parser to handle POST requests server.use(bodyParser.json()) @@ -77,9 +86,12 @@ exports.configure = (app, server, options) => { // Return session info server.get(path+'/session', (req, res) => { + // @TODO Instead of storing the "user" object in the sesssion, we should + // really just store the User ID and fetch the User object. return res.json({ user: req.session.user || null, isLoggedIn: (req.session.user) ? true : false, + clientMaxAge: clientMaxAge, csrfToken: res.locals._csrf }) })