620 karma · joined June 24, 2021
In the first scenario, the attacker steals the token and uses it forever. In the second, the attacker steals the token and they'll get a fresh one near its expiry. They can still impersonate the user forever. The user might notice something when they get kicked out (because the attacker renewed the token, rendering the old one invalid) but it's unlikely. For good UX, you need a grace period anyway, otherwise legitimate users can have problems with parallel requests (request one causes a token refresh, request two gets rejected because it was initiated before the first one was completed).
You can use a second token (a refresh token) but it only pushes the risk to the second token. Now we need to worry about the second token being stolen and abused forever.
Refresh tokens are useful for not having to hit the database on every request though: Typically, the short lived session token can be validated without hitting the db (e.g. it's a signed JWT). But it means that you can't invalidate it when stolen, it will be valid until expiry time so the expiry time has to be short to limit the damage. For the refresh token, on the other hand, you do hit the db. Using a second token doesn't add any security, hitting the db does, because the refresh token can be invalidated (by deleting it from the db).
Lucia always hits the db (at least in their examples), so you can invalidate tokens anytime. To mitigate risks, you can allow the user to see and terminate their active sessions (preferably with time, location, and device info: "Logged in 12 AM yesterday from an iPhone in Copenhagen"). You could also notify the user when someone logs in from a new location or device.
That's about all you can do. There's simply no fully secure way of implementing long-lived sessions.
I don't thing there's one true right thing™ though. That's why explicit handling is necessary: The compiler doesn't have enough context to handle it for you. The programmer needs to decide what's the right way to handle it.
Finalizers shouldn't fail because they might be executed while another exception is already in flight. Three languages have three different behaviors when that happens but in my opinion they all do the wrong thing:
In C++, if you throw from a destructor while another exception is in flight, the program will be terminated. In Java, throwing from a `finally` block will "forget" the original exception. In Go (according to this article, I'm not familiar with it), error from `defer` will be ignored. None of these are ideal.
[1] https://sourceforge.net/projects/aspeqt. Better maintained fork: https://github.com/RespeQt/RespeQt
I believe it's now illegal in some jurisdictions.
Sounds like they had much more valid reasons than Bing being wrong about Annie Lennox's first band to me.
So I ended up writing a Basic program on the Atari to read data from the disk sector by sector and paint it on the screen (with the large 4-color pixels of graphics mode 3). The Atari was connected to the TV card of my PC and a Delphi program I wrote was running on the PC that kept taking screen shots and trying to decode the data from there. I quickly learned that empty sectors threw off my pixel position calibration so I added a mask pattern and a checksum. The sector address was also included. With that, I was able to transfer all my disk contents to my PC. To this day I consider it my greatest engineering achievement :)
Some ten years later, I went on to build an SIO2PC program called AspeQt. A more up-to-date community fork called RespeQt is still the most popular tool in that category used by the community. It even has its own subforum on AtariAge[1].
[1] https://forums.atariage.com/forum/184-respeqt-sio2pc-softwar...
Important clarification though: Hattip is _not_ Express compatible nor it aims to be. It uses the completely different and incompatible fetch API instead of the node:http API.