Skip to content

Implement server-side session invalidation using token_invalid_before#1593

Open
Oykunle wants to merge 19 commits into
OpenEnergyDashboard:developmentfrom
Oykunle:oed-session-invalidation-clean2
Open

Implement server-side session invalidation using token_invalid_before#1593
Oykunle wants to merge 19 commits into
OpenEnergyDashboard:developmentfrom
Oykunle:oed-session-invalidation-clean2

Conversation

@Oykunle
Copy link
Copy Markdown

@Oykunle Oykunle commented Mar 31, 2026

Description

This PR implements server-side session invalidation for JWT authentication using a token_invalid_before timestamp stored on the users table.

Previously, issued JWTs remained valid until expiration, even after logout. This meant that a token could still be reused after the user had logged out.

This change adds a server-side invalidation mechanism by comparing the token’s iat (issued-at timestamp) against the user’s token_invalid_before value. Any token issued before that timestamp is rejected.


Type of change

  • Note merging this changes the database configuration.
  • This change requires a documentation update

Changes Made

Database

  • Added token_invalid_before column to the users table
  • Implemented migration under 1.0.0 → 2.0.0 as requested

Authentication / Security

  • Updated authentication logic to reject tokens where:
token.iat < token_invalid_before
  • Ensures invalidated tokens cannot be reused
  • Applies validation consistently across middleware and verification routes

Logout

  • Refactored logout into /api/login/logout
  • Logout updates token_invalid_before to invalidate all existing tokens
  • Ensures idempotent behavior (repeated logout is safe)

Code Improvements

  • Improved consistency of authentication checks across routes
  • Reduced duplication in token validation logic

Tests

Added automated tests to validate session invalidation behavior:

  • Token is valid before logout
  • Token becomes invalid after logout
  • Invalidated token is rejected on protected routes
  • Repeated logout calls succeed safely
  • Logout validation rejects malformed requests

All tests pass successfully in the test environment.


Security Impact

  • Prevents reuse of old or stolen JWT tokens after logout
  • Adds server-side control over token validity
  • Improves session security without changing authentication flow

Limitations

  • Invalidates all tokens globally per user (not per-device/session)
  • Requires database migration for existing deployments

huss

This comment was marked as resolved.

@Oykunle

This comment was marked as resolved.

@Oykunle

This comment was marked as resolved.

huss

This comment was marked as resolved.

@Oykunle

This comment was marked as resolved.

@huss

This comment was marked as resolved.

@Oykunle

This comment was marked as resolved.

huss

This comment was marked as outdated.

@huss huss mentioned this pull request Apr 16, 2026
5 tasks
huss

This comment was marked as outdated.

@Oykunle

This comment was marked as resolved.

@Oykunle

This comment was marked as resolved.

@Oykunle

This comment was marked as resolved.

@huss

This comment was marked as outdated.

huss

This comment was marked as outdated.

@Oykunle

This comment was marked as outdated.

@Oykunle

This comment was marked as resolved.

@Oykunle

This comment was marked as outdated.

@Oykunle

This comment was marked as resolved.

@Oykunle

This comment was marked as resolved.

@Oykunle
Copy link
Copy Markdown
Author

Oykunle commented Apr 28, 2026

I see you added a comment but I'm not completely satisfied with the code. Is there a reason that the exact time for the token date cannot be gotten? I would think then a proper comparison (such as using moment) would not have an issue within one second. Can you please help me understand this.

Great question.

JWT stores the iat value in seconds, while the database timestamp (token_invalid_before) is stored with millisecond precision. Because of this difference in precision, a direct comparison at second-level granularity can lead to edge cases when issuance and invalidation occur within the same second.

To address this, I converted the JWT iat value to milliseconds and performed the comparison at millisecond precision. This avoids edge cases without requiring additional libraries and keeps the comparison consistent with the database format.

@Oykunle

This comment was marked as resolved.

@Oykunle

This comment was marked as outdated.

Copy link
Copy Markdown
Member

@huss huss left a comment

Choose a reason for hiding this comment

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

@Oykunle First, I want to say I'm sorry this took so long. I communicated with you earlier about why and I appreciate your patience.

This is looking good. I merged in development to get rid of the DB startup issue from a recently merged PR as this sat for a while due to me. Testing after that did not see issues. I also went through all the previous comments and hid all the resolved ones so it would be easier to see what is left. I think all the old ones are done (thanks) and there are three new ones to consider. I don't think they are complex but let me know if you have questions/thoughts.

I know this is past the time you intended to work on OED so let me know if you want someone else to address the final comments.

I will want to do a careful read of the code once it looks done to be sure this security patch is all good.

Comment thread src/server/app.js
const meters = require('./routes/meters');
const preferences = require('./routes/preferences');
const login = require('./routes/login');
const login = require('./routes/loginLogout');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

While I think I see the appeal of leaving the name as login so all the current routes work, I think making the actual server route file differ from the route name is confusing and prone to mistakes. The only other case of doing this is for readings where the actual name is used in the app.use(). I think this was an attempt to decouple the two names but it was not done in any consistent way. Something that can be fixed. For now, I think it should become loginLogout with all uses updated including app.use below.

.send({ token });

expect(beforeVerify).to.have.status(HTTP_CODES.OK);
expect(beforeVerify.body).to.have.property('success', true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Above has a check for no message property. Should it be here (and I think there is another usage below)?


expect(secondLogoutRes).to.have.status(HTTP_CODES.OK);
expect(secondLogoutRes.body).to.have.property('success', true);
expect(secondLogoutRes.body).to.have.property('message', 'Logout successful.');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here you check the message but not right above for the first attempt. Is there a reason?

Comment thread src/server/routes/verification.js Outdated
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.

2 participants