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

Remove session id and encrypted session id from session data #114

Conversation

rclmenezes
Copy link
Contributor

@rclmenezes rclmenezes commented Aug 10, 2022

Picking up where @SimenB left off here: #101

  • First, I rebased feat: remove session id from session data #101 on main. Needed to fix some Ava -> Tap stuff
  • Next, I cleaned up the tests a little bit. There are some tests that modify a session or set expiration when that isn't the part of the test. It's best to keep tests more focused because otherwise bugs and slip through, as I'll talk about later.
  • @SimenB had some questions about the "multiple secrets" tests. The expiration was a red herring. The real issue was that sessionId/expiredSessionId wasn't in the session store, so the test always reset the sessionId. I made it cleaner.
  • Finally, I found a bug in the original PR where saveUninitialized: true and maxAge wouldn't set a new cookie. It was because the hashing mechanism for dates converted all of them to the string "{}" by accident.

Checklist

Benchmarks

Running locally. Before:

$ node benchmark/bench.js
╔══════════════╤═════════╤═══════════════╤═══════════╤═════════════════════════╗
║ Slower tests │ Samples │        Result │ Tolerance │ Difference with slowest ║
╟──────────────┼─────────┼───────────────┼───────────┼─────────────────────────╢
║ file         │      25 │  32.68 op/sec │ ±  7.20 % │                         ║
║ redis        │      25 │ 139.43 op/sec │ ± 18.75 % │ + 326.64 %              ║
╟──────────────┼─────────┼───────────────┼───────────┼─────────────────────────╢
║ Fastest test │ Samples │        Result │ Tolerance │ Difference with slowest ║
╟──────────────┼─────────┼───────────────┼───────────┼─────────────────────────╢
║ memory       │      25 │ 760.16 op/sec │ ± 38.32 % │ + 2225.96 %             ║
╚══════════════╧═════════╧═══════════════╧═══════════╧═════════════════════════╝

✨  Done in 27.04s.

After:

$ node benchmark/bench.js
╔══════════════╤═════════╤═══════════════╤═══════════╤═════════════════════════╗
║ Slower tests │ Samples │        Result │ Tolerance │ Difference with slowest ║
╟──────────────┼─────────┼───────────────┼───────────┼─────────────────────────╢
║ file         │      25 │  27.58 op/sec │ ± 15.34 % │                         ║
║ redis        │      25 │  80.42 op/sec │ ± 25.62 % │ + 191.56 %              ║
╟──────────────┼─────────┼───────────────┼───────────┼─────────────────────────╢
║ Fastest test │ Samples │        Result │ Tolerance │ Difference with slowest ║
╟──────────────┼─────────┼───────────────┼───────────┼─────────────────────────╢
║ memory       │      25 │ 781.85 op/sec │ ± 37.90 % │ + 2734.57 %             ║
╚══════════════╧═════════╧═══════════════╧═══════════╧═════════════════════════╝

✨  Done in 31.01s.

SimenB and others added 12 commits August 10, 2022 12:05
This meant that the test passed even though the session was not actually expired
- Need saveUnitialized: false and https for this condition to happen.
- Problem was that we need to hash dates correctly.
- Test revealed other problem: dates were not actually Date objects many times. Made all tests use a Date object for expires or removed expires entirely if the test did not need it
@rclmenezes rclmenezes force-pushed the remove-session-id-and-encrypted-session-id-from-session-data branch from c123ed6 to 96c180c Compare August 10, 2022 22:44
@@ -62,13 +62,14 @@ function decryptSession (sessionId, options, request, done) {
newSession(secret, request, cookieOpts, idGenerator, done)
return
}
if (session?.expires && session.expires <= Date.now()) {
if (session?.cookie?.expires && session.cookie.expires <= new Date()) {
const restoredSession = Session.restore(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should re-evaluate:

  • Whether or not to use object params. Once we get to like 8 it gets pretty confusing
  • Whether Session.restore should exist at all. At this point, it's mostly a wrapper around prevSession

lib/session.js Outdated Show resolved Hide resolved
test/base.test.js Show resolved Hide resolved
test/base.test.js Show resolved Hide resolved
test/base.test.js Show resolved Hide resolved
test/session.test.js Show resolved Hide resolved
this[originalHash] = this[hash]()
this.touch() // Needs to happen after originalHash is set, in case maxAge forces an update
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a bug in the original PR. Added a test against it

Copy link
Member

Choose a reason for hiding this comment

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

nice!

test/base.test.js Show resolved Hide resolved
@@ -309,7 +290,7 @@ test('should bubble up errors with destroy call if session expired', async (t) =
const store = {
set (id, data, cb) { cb(null) },
get (id, cb) {
cb(null, { expires: Date.now() - 1000, cookie: { expires: Date.now() - 1000 } })
cb(null, { cookie: { expires: new Date(Date.now() - 1000) } })
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As I explain elsewhere, expires is supposed to be a Date, not a number

Copy link
Member

@SimenB SimenB left a comment

Choose a reason for hiding this comment

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

yay, thanks for picking this up!

lib/fastifySession.js Outdated Show resolved Hide resolved
this[originalHash] = this[hash]()
this.touch() // Needs to happen after originalHash is set, in case maxAge forces an update
Copy link
Member

Choose a reason for hiding this comment

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

nice!

Copy link
Contributor

@Uzlopak Uzlopak left a comment

Choose a reason for hiding this comment

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

I want to make a proper review. So dont merge it

@Uzlopak
Copy link
Contributor

Uzlopak commented Aug 11, 2022

@rclmenezes

What is up with the failing unit test?

@rclmenezes
Copy link
Contributor Author

Interesting -- failing test works locally. It appears to be a race condition somewhere.

My guess is that touch() is running so quickly in CI that session.isModified() is returning true. One moment...

@rclmenezes
Copy link
Contributor Author

Ugh, I'm dumb. The cookie wasn't in fastify.inject so the test was wrong 🤦‍♂️

So it was making a new session. This would have a fresh cookies.expires value. Then .touch() would be called and either:

  • .touch() would happen quickly enough where the fresh Date.now() was the same. Nothing modified so the cookie wouldn't change. This happened in CI.
  • .touch() would take too long and the Date.now() was different, resulting in session.isModified() and triggering a set-cookie header. This happened locally and was what the test expected.

@Uzlopak
Copy link
Contributor

Uzlopak commented Aug 11, 2022

To be honest this PR is hard to digest. Can we please fix one issue at a time?

@rclmenezes
Copy link
Contributor Author

@Uzlopak happily! This PR is hard for me to digest too.

There were some tests that didn't do what they advertised, which caused a lot of grief for me. I can split it up into a PR that cleans up tests and a PR that does this change, if that helps?

Comment on lines +166 to +172
return Boolean(
this[originalHash] !== this[hash]() ||
// If maxAge is set, the session is modified.
// This is necessary because we don't read the cookie's expiration from the cookie itself.
// session.cookie.expires is initially set from Cookie(cookieOpts), so we cannot detect if it changed from maxAge alone.
this[maxAge]
)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return Boolean(
this[originalHash] !== this[hash]() ||
// If maxAge is set, the session is modified.
// This is necessary because we don't read the cookie's expiration from the cookie itself.
// session.cookie.expires is initially set from Cookie(cookieOpts), so we cannot detect if it changed from maxAge alone.
this[maxAge]
)
if (this[originalHash] !== this[hash]()) {
return true
}
// If maxAge is set, the session is modified.
// This is necessary because we don't read the cookie's expiration from the cookie itself.
// session.cookie.expires is initially set from Cookie(cookieOpts), so we cannot detect if it changed from maxAge alone.
return this[maxAge] != null

However, this seems wrong. Just setting a max age should never make isModified return true every single time

Copy link
Member

Choose a reason for hiding this comment

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

This is necessary because we don't read the cookie's expiration from the cookie itself.

We should probably do that, and isn't that what happens with your return sess.cookie.expires?.getTime() in the hash function?

Copy link
Contributor Author

@rclmenezes rclmenezes Aug 12, 2022

Choose a reason for hiding this comment

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

and isn't that what happens with your... hash function?

I wish!

cookie.expires is set using maxAge upon initialization anyway:
https://github.com/fastify/session/blob/master/lib/cookie.js#L42-L44

But you're right, maxAge shouldn't auto-trigger a modification. I realized that if maxAge is set but rolling is false, we want it to stay.

Also, we probably want to eventually support session stores that implement the touch method. In this case, the session would not be modified, just the TTL would.

Reverting this but I'm not quite sure how to implement this otherwise... need to think about it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah -- I'm mistaken. This code is confusing :)

If rolling is false, Session.restore will be called, which addresses this.

@Uzlopak
Copy link
Contributor

Uzlopak commented Aug 12, 2022

Guys... really split this PR in some logical parts. I am investigating some time on having some reliable benchmark as the current benchmark sucks currently. But yeah. Please lets split it.If you like we could first agree which are the parts and who wants to implement this or that feature/bug fix.

@rclmenezes
Copy link
Contributor Author

Okay I'm going to split this PR up into a bunch of parts:

  1. I'm gonna clean up the tests. Add some constants, etc. Also I'll add the missing documentation for rolling.
  2. touch should be implemented differently. First, it should not be called in the Session constructor; sessions should not be touched if rolling is false or maxAge is not set. I'll make isTouched a boolean property of Session and if it's true, the session will get saved (for now). Eventually, if isTouched && !isModified fastifySession can invoke sessionStore.touch to update TTL instead of sessionStore.save
  3. I'll remove expires, sessionid and encryptedSessionid from the session state, like in the original PR.

Sound good?

@Uzlopak
Copy link
Contributor

Uzlopak commented Aug 12, 2022

Maybe simpler. would say following parts:

  1. Add documentation for rolling
  2. clean up tests, maybe another PR for for adding some constans
  3. touch
  4. expires are Date
  5. sessionId?
  6. expiredSessionId?

@Uzlopak
Copy link
Contributor

Uzlopak commented Aug 12, 2022

Just get the PRs as small as possible keep them coming.

@rclmenezes
Copy link
Contributor Author

rclmenezes commented Aug 12, 2022

Sounds great. Will update this comments as the PRs come in

  1. Docs: Add missing documentation for README.md #116
  2. Test clean up: Clean up tests #117
  3. Re-signing multiple secrets bugfix: Bugfix: when there are multiple secrets, we should re-sign with latest #118
  4. Removing expires from being stored outside the cookie in the session.
  5. Bugfix for multiple secrets
  6. Removing sessionid and encryptedSessionId from the session

@rclmenezes
Copy link
Contributor Author

Closing in favor of:
#121

@rclmenezes rclmenezes closed this Aug 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants