Repository navigation
Periodically delete inactive client sessions #943
Description
Activity
I'm not super sure about the machanics of this yet. Feels like we can leave more freedom in the exact heavior than a boolean switch and a hardcoded timeout.
I think there were a previous discussion about client tokens being not stored in plaintext (or at least not retrivable via a different client token), I agree with that opinion although I caved at that time because pragmatic concerns with existing workflows requiring pushing messages as a different app making it impossible to make it actually categorically more secure without introducing breaking changes.
I think it's okay keeping the app side unchanged for convienience not breaking existing workflows but I propose the client side being revamped due to the much higher exposure.
Since we are already transitioning from tokens to cookies let's actually use a more modern token system for them (so the legacy way could be called "API tokens" with original semantics and the new ways would be "sessions"). The difference in behavior would be:
- The token would no longer be retrievable via API call. (because it defeats the point of using an HttpOnly cookie)
- The token would be bound to a particular password hash.
This is IMO not that hard to implement if we are allowed to do breaking change to the scheme
When we want to issue a new session token:
- Prepare a small message
<user_id>|<client_id>|<created_at>|<random_salt> - Pull out the user's password, if the user is external generate a random string in place of the password field.
- Derive a token signing key key using HKDF of the password hash, (IKM is the password hash, salt is salt, info is constant "Input: password-hash\r\nPurpose: gotify-session\r\n" )
- Sign the message with the derived key using HMAC.
- Fill the "token" field with some dummy message to be returned in the /client API (like
<web_session_123>)
When an authenticated request is received we:
- Extract user_id and salt from the plaintext part of the token
- pull out the user's current password hash, redo the key derivation.
- use the derived key to verify the HMAC
- if it passed it means the token is not tampered with and has not changed password since.
This might leak what user IDs are available via timing info but doesn't seem like a big issue to me.
I think there were a previous discussion about client tokens being not stored in plaintext (or at least not retrivable via a different client token), I agree with that opinion although I caved at that time because pragmatic concerns with existing workflows requiring pushing messages as a different app making it impossible to make it actually categorically more secure without introducing breaking changes.
I think it's okay keeping the app side unchanged for convienience not breaking existing workflows but I propose the client side being revamped due to the much higher exposure.I'm okay with breaking this, if we do a major release then this should be fine. We could allow the /message endpoint with client / basic auth, and then require the request to include the appid (which is already returned by the api). This should remove the need to expose the application token in the api.
I agree that the tokens shouldn't be returned in the API, but I'm not really seeing the benefits for binding this to the user password. Do you know any other service that handles their session like this?
What are benefits in comparison to: store a hashed version of the token, and include the client id inside the token given to the user, something like
gtfy_clCLIENTID_TOKEN gtfy_cl123_5e46e469724a464c4fcf2de4d68562d9436ef8adIn gotify we parse the client id and then compare the TOKEN against the hashed value in the database.
I agree that the tokens shouldn't be returned in the API, but I'm not really seeing the benefits for binding this to the user password. Do you know any other service that handles their session like this?
I'm very certain changing your Google password will log you out of all sessions.
Reauthentication is critical when an account has experienced high-risk activity such as account recovery, password resets, or suspicious behavior patterns.
https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.htmlWhether changing password is a form of password reset or not seems to be not very clear cut. So I'm not going to insist this must be bound to the password but merely my personal preference.
What are benefits in comparison to: store a hashed version of the token, and include the client id inside the token given to the user,
The benefit is cryptographic flexibility. For example if we want to bind the token to the password, or we want to derive other specific purpose tokens. They can be implemented only as "soft" changes to the software layer only without having to change the database format every single time.
A practical example would be #103 and #678 , we can sign a scoped token that allows someone else to use this token to view a specific app or message switch policy details like whether these sharing tokens must be bound to client or not, without having to reveal the entire token or create an entire new database table to track these scoped tokens
We can probably try something in between, a hard bind of a token's verifiability to the exact password hash does seem like a little inflexible, but I prefer a lot to have one common backend-kept secret for each user and deriving purpose specific tokens using cryptography rather than more database fields for the flexibility and avoiding schema bloat benefits above
I'm very certain changing your Google password will log you out of all sessions.
Ahh yeah, this makes sense. I still had the client api-keys in mind. So this should then only invalidate session tokens, and not client api-keys?
The benefit is cryptographic flexibility. For example if we want to bind the token to the password, or we want to derive other specific purpose tokens. They can be implemented only as "soft" changes to the software layer only without having to change the database format every single time.
How is this different to having a json column "permissions" on the client in the database? I'd say you have the same benefits/drawbacks to having a signed message with dynamic content.
It could be client independent, but I think if we want to support "last used date-time" then this has to be stored in the database either way.
we can sign a scoped token that allows someone else to use this token to view a specific app or message switch policy details like whether these sharing tokens must be bound to client or not, without having to reveal the entire token or create an entire new database table to track these scoped tokens
If we'd have such a client independent token, how would revocation work? E.g. when the user logs out. Do you have to invalidate all tokens by changing the user secret?
How is this different to having a json column "permissions" on the client in the database?
Benefits are:
- stateless and infinitely scalable
- generally speaking, easier to implement and reason about than fully fledged RBAC schemes like online drive services have
- if properly designed you can introspect (what user is this token for) or issue them offline
- incremental feature changes can be done without touching the database
Drawback:
- there is no centralized registry of "active" tokens by default
- if modifications or renewals are needed you need to exchange the token for a new one. I think that's really it.
If we'd have such a client independent token, how would revocation work?
You would have one single table of blacklisted token IDs for all future purposes.
To be honest, for a "sharing" content token you don't revoke them is my opinion. just like storage buckets you don't revoke individual tokens because they are designed to be transient and atomic. For example #678 asked for a feature to share individual message(s), there is no point revoking them because the overhead of tracking them is not very scalable plus the argument of incremental security of sending a token that only gets you view access specifically already immutable messages and then being able to revoke them is IMO not clear.
I agree with the statements if this is some kind of "share"-token but for user sessions this seems like more effort to implement.
If we'd have such tokens they'd need a expiry date, as some clients may not properly logout, and we don't want unused valid tokens. If that's the case, then gotify needs some kind of refresh mechanism, to request a new token with an existing token.
Given that we already have client/device/session tokens that can be easily invalidated by deleting them and the user is able to see which devices are connected with last used date etc. I don't see, that the solution with a signed token is an improvement here, given we already have the infrastructure from the other approach.
We could make this feature independent from the session tokens. Clients get a "expire after" and "expire after inactivity" duration, clients created by login/oidc will have a configurable default for this.
The benefits "stateless", "scalable", "issue offline" aren't really relevant in the gotify context I'd say, gotify doesn't try to be scalable and doesn't really have or need offline features.
So in short, the implementation effort for both solutions for session would be:
server side tokens:
- Invalidate tokens after a period of inactivity
signed tokens:
- Implement signed tokens
- with expiry
- add user secret
- adjust authentication logic to check for permissions
- Refresh tokens in webui / android app periodically
IMO, server side tokens seems much easier to implement as it's server side only, and we don't lose the visibility which active sessions are there. I'd also say this usually implemented this way for user sessions. E.g. GitHub, Jellyfin, or ProtonMail.
I'm not against having the user secret, but I think for the user sessions the client approach is better UX.
- added a commit that references this issue
on Sep 21, 2026
From #941 (comment)
With the changes in #941, we have a session cookie for both local login and OIDC login. The cookie expires after 7 days of inactivity. gotify should periodically delete clients of type session when there weren't used in the last 7 days.
LastUsed from the client is only set on use, so it may be nil when the client token was never used. We probably have to add a createdAt field to the client (and to be consistent to all other models). So it's possible check
createdAt < now - 7 days && (lastUsed is null or lastUsed < now - 7 days)for the delete query.