Skip to content
Closed
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
16 changes: 3 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ npm i @fastify/session
```js
const fastify = require('fastify');
const fastifySession = require('@fastify/session');
const fastifyCookie = require('fastify-cookie');
const fastifyCookie = require('@fastify/cookie');

const app = fastify();
app.register(fastifyCookie);
app.register(fastifySession, {secret: 'a secret with minimum length of 32 characters'});
app.register(fastifyCookie, {secret: 'a secret with minimum length of 32 characters'});
app.register(fastifySession);
```
Store data in the session by adding it to the `session` decorator at the `request`:
```js
app.register(fastifySession, {secret: 'a secret with minimum length of 32 characters'});
app.addHook('preHandler', (request, reply, next) => {
request.session.user = {name: 'max'};
next();
Expand All @@ -37,7 +36,6 @@ app.addHook('preHandler', (request, reply, next) => {
**NOTE**: For all unencrypted (HTTP) connections, you need to set the `secure` cookie option to `false`. See below for all cookie options and their details.
The `session` object has methods that allow you to get, save, reload and delete sessions.
```js
app.register(fastifySession, {secret: 'a secret with minimum length of 32 characters'});
app.addHook('preHandler', (request, reply, next) => {
request.session.destroy(next);
})
Expand All @@ -51,14 +49,6 @@ app.addHook('preHandler', (request, reply, next) => {
### session(fastify, options, next)
The session plugin accepts the following options. It decorates the request with the `sessionStore` and a `session` object. The session data is stored server-side using the configured session store.
#### options
##### secret (required)
The secret used to sign the cookie. Must be an array of strings or a string with a length of 32 or greater.

If an array, the first secret is used to sign new cookies and is the first to be checked for incoming cookies.
Further secrets in the array are used to check incoming cookies in the order specified.

Note that the rest of the application may manipulate the array during its life cycle. This can be done by storing the array in a separate variable that is later used with mutating methods like unshift(), pop(), splice(), etc.
This can be used to rotate the signing secret at regular intervals. A secret should remain somewhere in the array as long as there are active sessions with cookies signed by it. Secrets management is left up to the rest of the application.
##### cookieName (optional)
The name of the session cookie. Defaults to `sessionId`.
##### cookiePrefix (optional)
Expand Down
242 changes: 101 additions & 141 deletions lib/fastifySession.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,8 @@ const fp = require('fastify-plugin')
const idGenerator = require('./idGenerator')()
const Store = require('./store')
const Session = require('./session')
const cookieSignature = require('cookie-signature')

function session (fastify, options, next) {
const error = checkOptions(options)
if (error) {
return next(error)
}

options = ensureDefaults(options)

// Decorator function takes cookieOpts so we can customize on per-session basis.
Expand All @@ -30,44 +24,38 @@ function session (fastify, options, next) {
fastify.addHook('onRequest', onRequest(options))
fastify.addHook('onSend', onSend(options))
next()
}

function decryptSession (sessionId, options, request, done) {
const cookieOpts = options.cookie
const idGenerator = options.idGenerator
const secrets = options.secret
const secretsLength = secrets.length
const secret = secrets[0]

let decryptedSessionId = false
for (let i = 0; i < secretsLength; ++i) {
decryptedSessionId = cookieSignature.unsign(sessionId, secrets[i])
if (decryptedSessionId !== false) {
break
function decryptSession (sessionId, options, request, done) {
const cookieOpts = options.cookie
const idGenerator = options.idGenerator

const unsignedCookie = request.unsignCookie(sessionId)
if (unsignedCookie.valid === false) {
request.session = new Session(request, idGenerator, cookieOpts)
done()
return
}
}
if (decryptedSessionId === false) {
newSession(secret, request, cookieOpts, idGenerator, done)
} else {
const decryptedSessionId = unsignedCookie.value
options.store.get(decryptedSessionId, (err, session) => {
if (err) {
if (err.code === 'ENOENT') {
newSession(secret, request, cookieOpts, idGenerator, done)
request.session = new Session(request, idGenerator, cookieOpts)
done()
} else {
done(err)
}
return
}
if (!session) {
newSession(secret, request, cookieOpts, idGenerator, done)
request.session = new Session(request, idGenerator, cookieOpts)
done()
return
}
if (session.cookie?.expires && session.cookie.expires <= Date.now()) {
const restoredSession = Session.restore(
request,
idGenerator,
cookieOpts,
secret,
session
)

Expand All @@ -86,159 +74,131 @@ function decryptSession (sessionId, options, request, done) {
request,
idGenerator,
cookieOpts,
secret,
session
)
} else {
request.session = Session.restore(
request,
idGenerator,
cookieOpts,
secret,
session
)
}
done()
})
}
}

function onRequest (options) {
const unsignSignedCookie = options.unsignSignedCookie
const cookieOpts = options.cookie
const cookieName = options.cookieName
const idGenerator = options.idGenerator
const cookiePrefix = options.cookiePrefix
const hasCookiePrefix = typeof cookiePrefix === 'string' && cookiePrefix.length !== 0
const cookiePrefixLength = hasCookiePrefix && cookiePrefix.length
function onRequest (options) {
const cookieOpts = options.cookie
const cookieName = options.cookieName
const idGenerator = options.idGenerator
const cookiePrefix = options.cookiePrefix
const hasCookiePrefix = typeof cookiePrefix === 'string' && cookiePrefix.length !== 0
const cookiePrefixLength = hasCookiePrefix && cookiePrefix.length

return function handleSession (request, reply, done) {
request.session = {}
return function handleSession (request, reply, done) {
request.session = {}

const url = request.raw.url
if (url.indexOf(cookieOpts.path || '/') !== 0) {
done()
return
}
let sessionId = request.cookies[cookieName]
if (sessionId && hasCookiePrefix && sessionId.startsWith(cookiePrefix)) {
sessionId = sessionId.slice(cookiePrefixLength)
}
const secret = options.secret[0]
if (!sessionId) {
newSession(secret, request, cookieOpts, idGenerator, done)
} else {
let sessionToDecrypt = sessionId

if (unsignSignedCookie) {
const unsignedCookie = reply.unsignCookie(sessionId)
if (unsignedCookie.valid) {
sessionToDecrypt = unsignedCookie.value
}
const url = request.raw.url
if (url.indexOf(cookieOpts.path || '/') !== 0) {
done()
return
}
let sessionId = request.cookies[cookieName]
if (sessionId && hasCookiePrefix && sessionId.startsWith(cookiePrefix)) {
sessionId = sessionId.slice(cookiePrefixLength)
}
if (!sessionId) {
request.session = new Session(request, idGenerator, cookieOpts)
done()
} else {
decryptSession(sessionId, options, request, done)
}

decryptSession(sessionToDecrypt, options, request, done)
}
}
}

function onSend (options) {
const cookieOpts = options.cookie
const cookieName = options.cookieName
const cookiePrefix = options.cookiePrefix
const saveUninitialized = options.saveUninitialized
const hasCookiePrefix = typeof cookiePrefix === 'string' && cookiePrefix.length !== 0

return function saveSession (request, reply, payload, done) {
const session = request.session
if (!session || !session.sessionId) {
done()
return
}
function onSend (options) {
const cookieOpts = options.cookie
const cookieName = options.cookieName
const cookiePrefix = options.cookiePrefix
const saveUninitialized = options.saveUninitialized
const hasCookiePrefix = typeof cookiePrefix === 'string' && cookiePrefix.length !== 0

return function saveSession (request, reply, payload, done) {
const session = request.session
if (!session || !session.sessionId) {
done()
return
}

let encryptedSessionId = session.encryptedSessionId
if (encryptedSessionId && hasCookiePrefix) {
encryptedSessionId = `${cookiePrefix}${encryptedSessionId}`
}
let encryptedSessionId = session.encryptedSessionId
if (encryptedSessionId && hasCookiePrefix) {
encryptedSessionId = `${cookiePrefix}${encryptedSessionId}`
}

if (!shouldSaveSession(request, cookieOpts, saveUninitialized)) {
if (!shouldSaveSession(request, cookieOpts, saveUninitialized)) {
// if a session cookie is set, but has a different ID, clear it
if (request.cookies[cookieName] && request.cookies[cookieName] !== encryptedSessionId) {
reply.clearCookie(cookieName)
}
done()
return
}
session.save((err) => {
if (err) {
done(err)
if (request.cookies[cookieName] && request.cookies[cookieName] !== encryptedSessionId) {
reply.clearCookie(cookieName)
}
done()
return
}
reply.setCookie(
cookieName,
encryptedSessionId,
session.cookie.options(isConnectionSecure(request))
)
done()
})
session.save((err) => {
if (err) {
done(err)
return
}
reply.setCookie(
cookieName,
encryptedSessionId,
session.cookie.options(isConnectionSecure(request))
)
done()
})
}
}
}

function newSession (secret, request, cookieOpts, idGenerator, done) {
request.session = new Session(request, idGenerator, cookieOpts, secret)
done()
}

function checkOptions (options) {
if (!options.secret) {
return new Error('the secret option is required!')
}
if (typeof options.secret === 'string' && options.secret.length < 32) {
return new Error('the secret must have length 32 or greater')
function ensureDefaults (options) {
options.store = options.store || new Store()
options.idGenerator = options.idGenerator || idGenerator
options.cookieName = options.cookieName || 'sessionId'
options.cookie = options.cookie || {}
options.cookie.secure = option(options.cookie, 'secure', true)
options.rolling = option(options, 'rolling', true)
options.saveUninitialized = option(options, 'saveUninitialized', true)
options.cookiePrefix = option(options, 'cookiePrefix', '')
return options
}
if (Array.isArray(options.secret) && options.secret.length === 0) {
return new Error('at least one secret is required')
}
}

function ensureDefaults (options) {
options.store = options.store || new Store()
options.idGenerator = options.idGenerator || idGenerator
options.cookieName = options.cookieName || 'sessionId'
options.unsignSignedCookie = options.unsignSignedCookie || false
options.cookie = options.cookie || {}
options.cookie.secure = option(options.cookie, 'secure', true)
options.rolling = option(options, 'rolling', true)
options.saveUninitialized = option(options, 'saveUninitialized', true)
options.secret = Array.isArray(options.secret) ? options.secret : [options.secret]
options.cookiePrefix = option(options, 'cookiePrefix', '')
return options
}

function isConnectionSecure (request) {
return (
request.raw.socket?.encrypted === true ||
function isConnectionSecure (request) {
return (
request.raw.socket?.encrypted === true ||
request.headers['x-forwarded-proto'] === 'https'
)
}

function shouldSaveSession (request, cookieOpts, saveUninitialized) {
if (!saveUninitialized && !request.session.isModified()) {
return false
)
}
if (cookieOpts.secure !== true || cookieOpts.secure === 'auto') {
return true

function shouldSaveSession (request, cookieOpts, saveUninitialized) {
if (!saveUninitialized && !request.session.isModified()) {
return false
}
if (cookieOpts.secure !== true || cookieOpts.secure === 'auto') {
return true
}
return isConnectionSecure(request)
}
return isConnectionSecure(request)
}

function option (options, key, def) {
return options[key] === undefined ? def : options[key]
function option (options, key, def) {
return options[key] === undefined ? def : options[key]
}
}

module.exports = fp(session, {
fastify: '4.x',
name: '@fastify/session',
decorators: {
request: ['signCookie', 'unsignCookie']
},
dependencies: [
'@fastify/cookie'
]
Expand Down
Loading