Skip to content

Latest commit

 

History

History
200 lines (160 loc) · 7.67 KB

express-session.md

File metadata and controls

200 lines (160 loc) · 7.67 KB

express-session

A quick guide for setting up and configuring express-session middleware.

const express = require('express')

// Sessions can be stored server-side (ex: user auth) or client-side
// (ex: shopping cart). express-session saves sessions in a store, and
// NOT in a cookie. To store sessions in a cookie, use cookie-session.
const session = require('express-session')

// The default in-memory store is not production-ready because it leaks
// memory and doesn't scale beyond once process. For production, we need
// a session store, like Redis, which we can wire up with connect-redis.
const RedisStore = require('connect-redis')(session)

const app = express()

// Redis is a key-value NoSQL data store. It's perfect for sessions,
// because they can be serialized to JSON and stored temporarily using
// SETEX to auto-expire (i.e. auto-remove) after maxAge in seconds.
const store = new RedisStore({
  host: 'localhost', port: 6379, pass: 'secret'
})

app.use(
  // Creates a session middleware with given options.
  session({

    // Defaults to MemoryStore, meaning sessions are stored as POJOs
    // in server memory, and are cleared out when the server restarts.
    store,

    // Name for the session ID cookie. Defaults to 'connect.sid'.
    name: 'sid',

    // Whether to force-save unitialized (new, but not modified) sessions
    // to the store. Defaults to true (deprecated). For login sessions, it
    // makes no sense to save empty sessions for unauthenticated requests,
    // because they are not associated with any valuable data yet, and would
    // waste storage. We'll only save the new session once the user logs in.
    saveUninitialized: false,

    // Whether to force-save the session back to the store, even if it wasn't
    // modified during the request. Default is true (deprecated). We don't
    // need to write to the store if the session didn't change.
    resave: false,

    // Whether to force-set a session ID cookie on every response. Default is
    // false. Enable this if you want to prolong session lifetime while the user
    // is still browsing the site. Beware that the module doesn't have an absolute
    // timeout option, so you'd need to handle indefinite sessions manually.
    // rolling: false,

    // Secret key to sign the session ID. The signature is used
    // to validate the cookie against any tampering client-side.
    secret: 'sssh, quiet! it\'s a secret!',

    // Settings object for the session ID cookie. The cookie holds a
    // session ID ref in the form of 's:{SESSION_ID}.{SIGNATURE}' as in
    // s%3A9vKnWqiZvuvVsIV1zmzJQeYUgINqXYeS.nK3p01vyu3Zw52x857ljClBrSBpQcc7OoDrpateKp%2Bc

    // It is signed and URL encoded, but NOT encrypted, because session ID is
    // merely a random string that serves as a reference to the session. Even
    // if encrypted, it still maintains a 1:1 relationship with the session.
    // OWASP: cookies only need to be encrypted if they contain valuable data.
    // See https://github.com/expressjs/session/issues/468

    cookie: {

      // Path attribute in Set-Cookie header. Defaults to the root path '/'.
      // path: '/',

      // Domain attribute in Set-Cookie header. There's no default, and
      // most browsers will only apply the cookie to the current domain.
      // domain: null,

      // HttpOnly flag in Set-Cookie header. Specifies whether the cookie can
      // only be read server-side, and not by JavaScript. Defaults to true.
      // httpOnly: true,

      // Expires attribute in Set-Cookie header. Set with a Date object, though
      // usually maxAge is used instead. There's no default, and the browsers will
      // treat it as a session cookie (and delete it when the window is closed).
      // expires: new Date(...)

      // Preferred way to set Expires attribute. Time in milliseconds until
      // the expiry. There's no default, so the cookie is non-persistent.
      maxAge: 1000 * 60 * 60 * 2,

      // SameSite attribute in Set-Cookie header. Controls how cookies are sent
      // with cross-site requests. Used to mitigate CSRF. Possible values are
      // 'strict' (or true), 'lax', and false (to NOT set SameSite attribute).
      // It only works in newer browsers, so CSRF prevention is still a concern.
      sameSite: true,

      // Secure attribute in Set-Cookie header. Whether the cookie can ONLY be
      // sent over HTTPS. Can be set to true, false, or 'auto'. Default is false.
      secure: process.env.NODE_ENV === 'production'
    }
  })
)

app.get('/', (req, res) => {
  // A new uninitialized session is created for each request (but not
  // persisted to the store if saveUninitialized is false). It's
  // automatically serialized to JSON and can look something like
  /*
    Session {
      cookie: {
        path: '/',
        _expires: 2018-11-18T01:33:01.043Z,
        originalMaxAge: 7200000,
        httpOnly: true,
        sameSite: true,
        secure: false
      }
    }
  */
  console.log(req.session)

  // You can also access the cookie object above directly with
  console.log(req.session.cookie)

  console.log(req.session.cookie.expires) // date of expiry
  console.log(req.session.cookie.maxAge) // milliseconds left until expiry

  // Unless a valid session ID cookie is sent with the request,
  // the session ID below will be different for each request.
  console.log(req.session.id) // ex: VdXZfzlLRNOU4AegYhNdJhSEquIdnvE-

  // Same as above. Alphanumeric ID that gets written to the cookie.
  // It's also SESSION_ID porton in 's:{SESSION_ID}.{SIGNATURE}'.
  console.log(req.sessionID)

  res.send(`<a href='/login'>Login</a>`)
})

app.get('/login', (req, res) => {
  // Unless we explicitly write to the session (and resave is false), the
  // store is never updated, even though a new session is generated on each
  // request. After we modify that session, it gets persisted. On subsequent
  // writes, its cookie.maxAge also gets updated and synced with the store.
  req.session.userId = 1

  // Note that userId has no special meaning. It makes sense to store a
  // unique reference, e.g. user ID, because we can then use it to look up
  // the user in the DB. But we could store anything in sessions.

  res.send(`
    <form method='post' action='/logout'>
      <button>Logout</button>
    </form>
  `)

  // By the end of each request, the session middleware will call session.touch()
  // to update maxAge (and thus expires). It might call session.save() to write
  // to the store but ONLY IF the data was altered manually, like above (with
  // the exception when resave is set to true).
})

app.post('/logout', (req, res) => {
  // Assuming the request was authenticated in /login above,
  /*
    Session {
      cookie: {
        path: '/',
        _expires: 2018-11-18T02:03:01.926Z,
        originalMaxAge: 7200000,
        httpOnly: true,
        secure: false,
        sameSite: true
      },
      userId: 1 <-- userId from /login
    }
  */
  console.log(req.session)

  console.log(req.session.id) // ex: 0kVkUn7KUX1UZGnjagDKd_NPerjXKJsA

  // Note that the portion between 's%3A' and '.' is the session ID above.
  // 's%3A' is URL encoded and decodes to 's:'. The last part is the signature.
  // sid=s%3A0kVkUn7KUX1UZGnjagDKd_NPerjXKJsA.senfzYOeNHCtGUNP4bv1%2BSdgSdZWFtoAaM73odYtLDo
  console.log(req.get('cookie'))

  // Upon logout, we can destroy the session and unset req.session.
  req.session.destroy(err => {
    // We can also clear out the cookie here. But even if we don't, the
    // session is already destroyed at this point, so either way, they
    // won't be able to authenticate with that same cookie again.
    res.clearCookie('sid')

    res.redirect('/')
  })
})

app.listen(3000, () => console.log('http://localhost:3000'))