Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Goals of our design

...

  • User signs in
  • We create a new session, session token, reauthToken
  • We store the following Redis mappings:
    • sessionToken ↝ userId
    • userId ↝ (sessionToken) the set of valid session tokens is only this token when user signs in
    • userId ↝ session
  • return the session with the sessionToken and reauthToken in the session

Alternative sign in options that address concurrent sign ins

Concurrent sign ins should be rarer because they involve human intervention (enter credentials, click on a link), but could still theoretically happen. One approach to dealing with this would be to issue a new token with each successful sign in, following this logic:

  • 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

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.

Another alternative would be to try the userIdsession lookup and if a session already exists, return the session token in the session, for some grace period we can record in the session or in the cache (due to network issues though, "concurrency" issues could spread out over time in unexpected ways).

Authenticating a request

  • User makes request with a sessionToken
  • Retrieve the userId with the sessionToken (if this fails return 404)
  • Retrieve the valid sessionToken set with the userId (if this fails return 404)
  • Verify the sessionToken is in the set (if this fails return 404)
  • Update the set to include only this sessionToken (if the set only includes this token do nothing)
  • Retrieve the session with the userId
  • return the session with this session token and whatever reauthentication token is in the session

...

  • 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
  • We create a new session, sessionToken, reauthToken
  • Persist a new record in the secrets table for the new reauthToken
  • We store the following Redis mappings:
    • sessionToken ↝ userId
    • userId ↝ (sessionToken) add this session token to the set, do not recreate it
    • userId ↝ session
  • return the session with the sessionToken and reauthToken in the session

Thus, the tokens are rotated by successful reauthentication attempts, not by an expiration time.

To force rotation of the session token, we regenerate it with each reauthentication attempt. (If we copied it over, you could reauthenticate every day just before the session expired, keeping the token indefinitely, which isn't secure). We can issue multiple session tokens and we need to accept any of them, but once the client uses a session token, that is the only token that we will accept. That's the one the client has definitely received.

Sign Out

  • userId ↝ () empty the set of valid tokens
  • Delete the userId ↝ session mapping
  • Delete the reauth secret records for this user in the secrets table



Reauthentication

When the session token is expired, the client can send a reauth token via the reauth API. We retrieve the N most recent records for that user by their creation date (probably N=2 but could be N=3 if this is more robust), hash the token by the algorithm in each record, and compare to the hashed records looking for a match. (Do this intelligently: cache the hash by algorithm and reuse it since the algorithm is unlikely to change between reauthentications.) If there's a match, we treat this like a sign in: we generate a new session token and persist a new reauth token, and return a new session with these new tokens. If the reauthentication fails, even on return, the previous token continues to work, because we're comparing against older records as well.

...