Goals of our design
...
- User signs in
- We create a new session, session token, reauthToken
- We store the following Redis mappings:
- sessionToken ↝ userId
- userId ↝ session
- return the session with the sessionToken and reauthToken in the session
Authenticating a request
This doesn't change (it might change if we attempt to address concurrent sign in requests; see below).
...
- User reauthenticates with the reauthentication token
- We retrieve the N most recent records by their creation date, hash the token by the algorithm in each record, and compare to the hashed records looking for a match. A match is an authentication success
- If a session exists, we create a new session but keep the session token/internal session token;
- If a session does not exist, a new session includes new session token/internal session token;
- Persist a new record in the secrets table for the new reauthToken
- We store the following Redis mappings:
- sessionToken ↝ userId
- userId ↝ session
- return the session with the sessionToken and reauthToken in the session
Thus, the reauth tokens are rotated by successful reauthentication attempts, not by an expiration time. The session is not rotated once it exists (reauthentication does not rotate the session token if you do it before a session expires). If the session token rotated with each reauth request and we removed the validity of the last session token, concurrent reauthentication requests might capture the invalidated session token. The session token still expires after 12 hours.
Sign Out
- Delete the userId ↝ session mapping
- Delete the sessionToken ↝ userId mapping
- Delete the reauth secret records for this user in the secrets table
Concurrency issues
There are some issue we have identified when the client makes multiple requests to reauthenticate:
...
Addressing Concurrent Sign In Requests
Concurrent sign ins should be rarer because they involve human intervention (enter credentials, click on a link), but could still theoretically happen. There are two approaches to dealing with this.
The simpler option would be to record the timestamp when a session token is created, and reuse that token for a grace period on subsequent or concurrent sign ins. This is the simplest approach.
...
- issue new session token on each sign in, adding to a set of tokens in the session
- on access
- sessionToken ↝ userId
- userId ↝ session
- is token in session tokens set?
- NO: not authenticated
- YES: is there more than one token in session?
- NO: return session
- YES: replace set with a set consisting only of this token, write session to cache, return session
- is token in session tokens set?
Note that with this approach, we can later allow multiple clients to authenticate simultaneously by not stripping out other session tokens. Each token has an expiry due to the first sessionToken ↝ userId lookup, independent of the session expiry. We might also want to tie these session tokens to something like a UA header or an IP address to make it harder to hijack them.