Skip to content
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
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,11 @@ 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 `sessionStore` decorator of the `request` allows to get, save and delete sessions.
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) => {
const session = request.session;
request.sessionStore.destroy(session.sessionId, next);
request.session.destroy(next);
})
```

Expand Down Expand Up @@ -103,24 +102,37 @@ idGenerator: (request) => {

Allows to access or modify the session data.

#### request.destroySession(callback)
#### Session#destroy(callback)

Allows to destroy the session in the store

#### Session#touch()

Updates the `expires` property of the session.

#### Session#regenerate()
#### Session#regenerate(callback)

Regenerates the session by generating a new `sessionId`.
Regenerates the session by generating a new `sessionId` and persist it to the store.
```js
fastify.get('/regenerate', (request, reply) => {
request.session.regenerate();
reply.send(request.session.sessionId);
fastify.get('/regenerate', (request, reply, done) => {
request.session.regenerate(error => {
if (error) {
done(error);
return;
}
reply.send(request.session.sessionId);
});
});
```

#### Session#reload(callback)

Reloads the session data from the store and re-populates the `request.session` object.

#### Session#save(callback)

Save the session back to the store, replacing the contents on the store with the contents in memory.

#### Session#get(key)

Gets a value from the session
Expand Down
38 changes: 17 additions & 21 deletions lib/fastifySession.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ function session (fastify, options, next) {
})
fastify.decorateRequest('sessionStore', { getter: () => options.store })
fastify.decorateRequest('session', null)
fastify.decorateRequest('destroySession', destroySession)
fastify.addHook('onRequest', onRequest(options))
fastify.addHook('onSend', onSend(options))
next()
Expand Down Expand Up @@ -65,7 +64,22 @@ function decryptSession (sessionId, options, request, done) {
return
}
if (session && session.expires && session.expires <= Date.now()) {
options.store.destroy(sessionId, getDestroyCallback(secret, request, done, cookieOpts, idGenerator))
const restoredSession = Session.restore(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

doing a restore here to have access to the methods

request,
idGenerator,
cookieOpts,
secret,
session
)

restoredSession.destroy(err => {
if (err) {
done(err)
return
}

restoredSession.regenerate(done)
})
return
}
if (options.rolling) {
Expand Down Expand Up @@ -118,7 +132,7 @@ function onSend (options) {
done()
return
}
options.store.set(session.sessionId, session, (err) => {
session.save((err) => {
if (err) {
done(err)
return
Expand All @@ -133,29 +147,11 @@ function onSend (options) {
}
}

function getDestroyCallback (secret, request, done, cookieOpts, idGenerator) {
return function destroyCallback (err) {
if (err) {
done(err)
return
}
newSession(secret, request, cookieOpts, idGenerator, done)
}
}

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

function destroySession (done) {
const request = this
request.sessionStore.destroy(request.session.sessionId, (err) => {
request.session = null
done(err)
})
}

function checkOptions (options) {
if (!options.secret) {
return new Error('the secret option is required!')
Expand Down
38 changes: 34 additions & 4 deletions lib/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ const sign = Symbol('sign')
const addDataToSession = Symbol('addDataToSession')
const generateId = Symbol('generateId')
const requestKey = Symbol('request')
const cookieOptsKey = Symbol('cookieOpts')

module.exports = class Session {
constructor (request, idGenerator, cookieOpts, secret, prevSession = {}) {
this[generateId] = idGenerator
this.expires = null
this.cookie = new Cookie(cookieOpts)
this[cookieOptsKey] = cookieOpts
this[maxAge] = cookieOpts.maxAge
this[secretKey] = secret
this[addDataToSession](prevSession)
this[requestKey] = request
this.touch()
if (!this.sessionId) {
this.regenerate()
this.sessionId = this[generateId](this[requestKey])
this.encryptedSessionId = this[sign]()
}
}

Expand All @@ -32,9 +35,14 @@ module.exports = class Session {
}
}

regenerate () {
this.sessionId = this[generateId](this[requestKey])
this.encryptedSessionId = this[sign]()
regenerate (callback) {
const session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey])

this[requestKey].sessionStore.set(session.sessionId, session, error => {
this[requestKey].session = session

callback(error)
})
}

[addDataToSession] (prevSession) {
Expand All @@ -53,6 +61,28 @@ module.exports = class Session {
this[key] = value
}

destroy (callback) {
this[requestKey].sessionStore.destroy(this.sessionId, error => {
this[requestKey].session = null

callback(error)
})
}

reload (callback) {
this[requestKey].sessionStore.get(this.sessionId, (error, session) => {
this[requestKey].session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey], session)

callback(error)
})
}

save (callback) {
this[requestKey].sessionStore.set(this.sessionId, this, error => {
callback(error)
})
}

[sign] () {
return cookieSignature.sign(this.sessionId, this[secretKey])
}
Expand Down
110 changes: 103 additions & 7 deletions test/session.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test('should add session object to request', async (t) => {
test('should destroy the session', async (t) => {
t.plan(3)
const port = await testServer((request, reply) => {
request.destroySession((err) => {
request.session.destroy((err) => {
t.falsy(err)
t.is(request.session, null)
reply.send(200)
Expand Down Expand Up @@ -167,8 +167,13 @@ test('should generate new sessionId', async (t) => {
fastify.register(fastifySession, options)
fastify.get('/', (request, reply) => {
oldSessionId = request.session.sessionId
request.session.regenerate()
reply.send(200)
request.session.regenerate(error => {
if (error) {
reply.status(500).send('Error ' + error)
} else {
reply.send(200)
}
})
})
fastify.get('/check', (request, reply) => {
t.not(request.session.sessionId, oldSessionId)
Expand Down Expand Up @@ -271,6 +276,40 @@ test('should decryptSession with custom cookie options', async (t) => {
})
})

test('should bubble up errors with destroy call if session expired', async (t) => {
t.plan(2)
const fastify = Fastify()
const store = {
set (id, data, cb) { cb(null) },
get (id, cb) {
cb(null, { expires: Date.now() - 1000, cookie: { expires: Date.now() - 1000 } })
},
destroy (id, cb) { cb(new Error('No can do')) }
}

const options = {
secret: 'cNaoPYAwF60HZJzkcNaoPYAwF60HZJzk',
store,
cookie: { secure: false }
}

fastify.register(fastifyCookie)
fastify.register(fastifySession, options)

fastify.get('/', (request, reply) => {
reply.send(200)
})
await fastify.listen(0)
fastify.server.unref()

const { statusCode, body } = await request({
url: 'http://localhost:' + fastify.server.address().port,
headers: { cookie: 'sessionId=_TuQsCBgxtHB3bu6wsRpTXfjqR5sK-q_.3mu5mErW+QI7w+Q0V2fZtrztSvqIpYgsnnC8LQf6ERY;' }
})
t.is(statusCode, 500)
t.is(JSON.parse(body).message, 'No can do')
})

test('should not reset session cookie expiration if rolling is false', async (t) => {
t.plan(3)

Expand Down Expand Up @@ -361,8 +400,13 @@ test('should use custom sessionId generator if available (with request)', async
})
fastify.get('/login', (request, reply) => {
request.session.returningVisitor = true
request.session.regenerate()
reply.status(200).send('OK ' + request.session.sessionId)
request.session.regenerate(error => {
if (error) {
reply.status(500).send('Error ' + error)
} else {
reply.status(200).send('OK ' + request.session.sessionId)
}
})
})
await fastify.listen(0)
fastify.server.unref()
Expand Down Expand Up @@ -417,8 +461,13 @@ test('should use custom sessionId generator if available (with request and rolli
})
fastify.get('/login', (request, reply) => {
request.session.returningVisitor = true
request.session.regenerate()
reply.status(200).send('OK ' + request.session.sessionId)
request.session.regenerate(error => {
if (error) {
reply.status(500).send('Error ' + error)
} else {
reply.status(200).send('OK ' + request.session.sessionId)
}
})
})
await fastify.listen(0)
fastify.server.unref()
Expand All @@ -444,3 +493,50 @@ test('should use custom sessionId generator if available (with request and rolli
t.is(response3.statusCode, 200)
t.true(sessionBody3.startsWith('returningVisitor-'))
})

test('should reload the session', async (t) => {
t.plan(4)
const port = await testServer((request, reply) => {
request.session.someData = 'some-data'
t.is(request.session.someData, 'some-data')

request.session.reload((err) => {
t.falsy(err)

t.is(request.session.someData, undefined)

reply.send(200)
})
}, DEFAULT_OPTIONS)

const { response } = await request(`http://localhost:${port}`)

t.is(response.statusCode, 200)
})

test('should save the session', async (t) => {
t.plan(6)
const port = await testServer((request, reply) => {
request.session.someData = 'some-data'
t.is(request.session.someData, 'some-data')

request.session.save((err) => {
t.falsy(err)

t.is(request.session.someData, 'some-data')

// unlike previous test, here the session data remains after a save
request.session.reload((err) => {
t.falsy(err)

t.is(request.session.someData, 'some-data')

reply.send(200)
})
})
}, DEFAULT_OPTIONS)

const { response } = await request(`http://localhost:${port}`)

t.is(response.statusCode, 200)
})
14 changes: 10 additions & 4 deletions types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ declare module 'fastify' {

/** A session store. */
sessionStore: Readonly<FastifySessionPlugin.SessionStore>;

/** Allows to destroy the session in the store. */
destroySession(callback: (err: Error) => void): void;
}

interface Session extends SessionData {}
Expand All @@ -29,7 +26,16 @@ interface SessionData extends ExpressSessionData {
/**
* Regenerates the session by generating a new `sessionId`.
*/
regenerate(): void;
regenerate(callback: (err?: Error) => void): void;

/** Allows to destroy the session in the store. */
destroy(callback: (err?: Error) => void): void;

/** Reloads the session data from the store and re-populates the request.session object. */
reload(callback: (err?: Error) => void): void;

/** Save the session back to the store, replacing the contents on the store with the contents in memory. */
save(callback: (err?: Error) => void): void;

/** sets values in the session. */
set(key: string, value: unknown): void;
Expand Down
Loading