Skip to content

Conversation

ronjalh
Copy link
Member

@ronjalh ronjalh commented Oct 17, 2026

Summary

The session part of the login: a 15-minute access token and a 14-day refresh token that is replaced at every use. Part of #70. The Google check and login are a separate pull request (#71, draft), because Sign in with Google needs https (except on localhost) and the course server has only http.

Stacked. The base is 74-graphql-codegen (#78), which is on #77 (calculator), which is on #65 (server). Change the base to dev when the one below is merged.

What's in it

  • refreshSession(refreshToken): gives a new access token and a new refresh token, and the old one stops working. Using an old one again means it was copied, so the whole login (the family of tokens) is revoked. logout(refreshToken) revokes the login and is true also for an unknown token.
  • me: the coach for a valid access token (JWT, jose, HS256, SESSION_SECRET), null for a missing, wrong, changed, foreign or expired token. Protected operations throw UNAUTHENTICATED.
  • Refresh tokens are random, and only their SHA-256 hash is stored in the new sessions table (migration included). Expired rows are deleted at each refresh.
  • login answers NOT_IMPLEMENTED until feat: login with Google #71 is merged. Until then the tokens can only be made by the code (the tests do it).
  • Schema: AuthPayload has accessToken and refreshToken (the old token is renamed), and refreshSession and logout are added.
  • Settings: SESSION_SECRET (at least 32 characters, the server does not start with a shorter one). Without it the catalog works and me is null.
  • vite.config.ts sets test.fileParallelism: false, because the session tests write to the local database while other tests check that the row counts do not change.
  • Docs: "Identifying the user" in docs/API.md (flow, lifetimes, what the client must do, the decision to return the tokens in the body and not in a cookie), docs/DATA_MODEL.md (Session), the README, backend/.env.example, docs/AI_USAGE.md.

Decisions to check

  • Both tokens are returned in the response body, not in a cookie. The refresh token can then be read by scripts on the page. An httpOnly cookie is safer and works when the app and the API share an origin; it can be added later (written in docs/API.md).
  • Two tabs that refresh with the same token at the same moment look like a stolen token and end the login, so the client must refresh from one place at a time (documented, and done in the Apollo Client PR feat: set up Apollo Client in the frontend #80).
  • There is no rate limit on any of this yet.

Testing

  • Lint, tsc --noEmit and 152 tests pass against a database (0 skipped). 30 are new: me with no/changed/foreign/expired token, refresh and rotation, reuse ends the login, expired and unknown refresh tokens, logout, two requests with the same token (only one gets through), a hash is stored and not the token, expired rows are removed, the Authorization header over HTTP, and the settings. The time is set by the tests, and the first tokens are made directly because there is no login yet.

🤖 Generated with Claude Code

The session part of the login. refreshSession replaces the refresh token at
every use, and using an old one again ends the whole login. logout revokes
the login. me returns the coach for a valid 15-minute access token and null
otherwise. Only a hash of the refresh token is stored, in the new sessions
table. The Google check and login are a separate pull request, so login
answers NOT_IMPLEMENTED until it is merged.
Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant