Skip to content

Session pseudo code

Rishabh Poddar edited this page Oct 4, 2020 · 10 revisions

The logic below is not a one-to-one mapping with the actual code. For example, the pseudo-code below does not optimise on number of db lookups, and it does not express the use of transactions for thread safety.

Notation

encrypt(x, y) = encrypt 'x' using the key 'y'
createJWTWithPayload(x, y) = create a JWT containing 'x' as the payload, signed with 'y'
hash(x) = hash of 'x'
base64(x) = base64 representation of 'x'

Tables

CREATE TABLE refresh_tokens (
    session_handle VARCHAR(255) NOT NULL,
    user_id VARCHAR(128) NOT NULL,
    refresh_token_hash_2 VARCHAR(128) NOT NULL,
    session_data TEXT,
    expires_at BIGINT UNSIGNED NOT NULL,
    created_at_time BIGINT UNSIGNED NOT NULL,
    jwt_user_payload TEXT,
    PRIMARY KEY(session_handle)
);

Creating a session

Input from user:
userId: string
jwtPayload: JSON object, array or any primitive type. Is serialised everywhere.
sessionData: JSON object, array or any primitive type. Is serialised everywhere.
res: response object

Logic:
CODE: sessionHandle = a random string that will stay constant during this session
CODE: jwtKey = fetched from memory / db
CODE: refreshTokenKey = fetched from memory / db
CODE: refreshNonce = a random string
CODE: antiCsrfToken = a random string
CODE: timeNow = time in milli now.

// the refreshNonce is unique per refresh tokens. It's what makes one refresh token belonging to a session different to another one.
CODE: refreshToken = encrypt({sessionHandle, userId, refreshNonce, antiCsrfToken}, refreshTokenKey) + "." + refreshNonce

// the access token contains a pointer to the refresh token via rt below.
// It may not make sense now as to why this link is needed but will become clear later.
// The antiCsrfToken is stored in the access token so that we can do CSRF check without a db lookup.
CODE: accessToken = createJWTWithPayload({sessionHandle, userId, rt: hash(refreshToken), expiryTime, userPayload: jwtPayload, antiCsrfToken}, jwtKey)

// this token is accessible to the frontend and is used to read session info there.
CODE: frontToken = base64({uid: userId, ate: expiryTime, up: jwtPayload})

// This token acts as a proxy to the refresh token for the frontend. Whenever the refresh token changes, this token changes.
// The frontend needs to know when the refresh token changes to prevent race conditions while calling the refresh API.
// Since it cannot read the refresh token directly (since it's httpOnly cookie), it uses this token to keep track of changes.
CODE: idRefreshToken = a random string

// We store double hashed version of refresh token in the db since the plain text goes as cookie, 
// the hashed is stored in the access token, so we store the hash of that. This way, if the db is compromised,
// then there is literally nothing that can be with this double hashed refresh token.
CODE: insert into db: sessionHandle => {userId, rtHash2: hash(hash(refreshToken)), sessionData, refreshTokenExpiryTime, timeNow, jwtPayload}


CODE: set refreshToken (HttpOnly, secure), idRefreshToken (HttpOnly, secure) and accessToken (HttpOnly, secure) in cookies using res
CODE: set antiCsrfToken in the res header with the key anti-csrf
CODE: set idRefreshToken in res header with the key id-refresh-token
CODE: set frontToken in res header with key front-token

Getting session from cookie

Input from user:
req: request object. Should contain the accessToken & idRefreshToken cookies, and antiCsrf header if enabled
res: response object 
enableAntiCsrf: boolean

Logic:
CODE: jwtKey = fetched from memory / db

CODE: if idRefreshToken cookie is missing:
CODE:     clear cookies and throw UNAUTHORISED

CODE: if accessToken cookie is missing:
CODE:     throw TRY_REFRSH_TOKEN 
    
CODE: accessToken = fetch from cookies using req object.
CODE: accessTokenInfo = verifyJWTAndGetPayload(accessToken, jwtKey) // if this fails or token has expired, we throw TRY_REFRESH_TOKEN error

CODE: if enableAntiCsrf
CODE:     tokenFromHeader = get anti-csrf header value from res
CODE:     if tokenFromHeader == undefined || tokenFromHeader !== accessTokenInfo.antiCsrfToken
CODE:         throw TRY_REFRSH_TOKEN


// If blacklisting is enabled, we take the opportunity to check if the JWT payload for this session has changed.
// This way, other sessions can modify the JWT payload of this session, and that will be reflected on this session immediately.
CODE: needsToUpdateJWTPayload = false
CODE: if blacklisting is enabled
CODE:     if accessTokenInfo.sessionHandle is not in database
CODE:         clear cookies and throw UNAUTHORISED
CODE:     if accessTokenInfo.userPayload != payload from the database:
CODE:         needsToUpdateJWTPayload = true


// Please see Refresh a session section below first. Otherwise, the logic below will not make much sense.
// If this condition is true, it means that the refresh token associated 
// with this access token is already the parent, so we do not need to promote this refresh token.
CODE: if accessTokenInfo.prt === undefined && !needsToUpdateJWTPayload
CODE:     return session object to user using accessTokenInfo.


CODE: sessionInfo = read session row from db using accessTokenInfo.sessionHandle
CODE: if sessionInfo === undefined
CODE:     clear cookies and throw UNAUTHORISED


// If the below condition is true, it means that the refresh token of this access token needs to become the 
// parent refresh token of this session. This happens when this access token is used immediately after 
// a refresh call.
CODE: promote = sessionInfo.rtHash2 === hash(accessTokenInfo.prt)

// "sessionInfo.rtHash2 == hash(accessTokenInfo.rt)" will be true if the access token's refresh token has already become the parent,
// which is possible if an access token is used multiple times in parallel after a refresh call.
CODE: if promote || needsToUpdateJWTPayload || sessionInfo.rtHash2 == hash(accessTokenInfo.rt)
CODE:     if promote // we make the refresh token of this access token the parent one.
CODE:         update db row: sessionInfo.sessionHandle => {rtHash2: hash(accessTokenInfo.rt), now + refreshTokenExpiryTime}

          // We set the prt of this access token to null so that next time we know that we can verify it without hitting the core.
CODE:     newAccessToken = create new JWT using input accessTokenInfo. Get the JWTPayload from sessionInfo, and set prt to null.
CODE:     newFrontToken = create like we did in create new session
CODE:     update cookies with new access token using res object.
CODE:     update header with new-front-token
CODE:     return session object to user.


// The execution can come here if an access token that still has a prt of a grandfather refresh token is being used.
// and if it's still alive and valid.
CODE: return session object to user using accessTokenInfo.

Refreshing a session

Input from user:
req: request object. Should contain the refreshToken cookie and antiCsrf header if enabled
res: response object 

Logic:
CODE: refreshTokenKey = fetched from memory / db
CODE: jwtKey = fetched from memory / db

CODE: if auth cookies are missing:
CODE:     clear cookies and throw UNAUTHORISED
    
CODE: refreshToken = get token from req object


// The function below decrypts the token and verifies that the nonce matches.
// If this fails, clear cookies and throw UNAUTHORISED error
CODE: refreshTokenInfo = verifyAndGetInfo(refreshToken, refreshTokenKey)

CODE: if antiCsrf from request != antiCsrf in token
CODE:     throw UNAUTHORISED

CODE: <LABEL>
CODE: sessionHandle = refreshTokenInfo.sessionHandle
CODE: sessionInfo = read session row from db using sessionHandle

CODE: if sessionInfo === undefined || sessionInfo.refreshTokenExpiryTime < now
CODE:     clear cookies and throw UNAUTHORISED


// if the condition below is true, it means the current token is already the parent of the session,
// so we can issue children. Otherwise, we must make this one the parent first (see if statement below this one)
CODE: if sessionInfo.rtHash2 === hash(hash(refreshToken))
CODE:     refreshNonce = a random string
CODE:     antiCsrfToken = a random string

          // Notice that the prt in the newRefreshToken (which is a child token) points to the input refresh token,
          // and likewise for the new child access token. Also notice that the new access token points to the new 
          // refresh token "rt" key
CODE:     newRefreshToken = encrypt({sessionHandle, userId, refreshNonce, prt: hash(refreshToken), antiCsrfToken}, refreshTokenKey) + "." + refreshNonce
CODE:     newAccessToken = createJWTWithPayload({sessionHandle, userId, rt: hash(newRefreshToken), prt: hash(refreshToken), expiryTime, sessionInfo.jwtPayload, antiCsrfToken, lmrt: sessionInfo.lmrt}, jwtKey)
CODE:     newFrontToken = similar to how it's created in create new session
   
CODE:     set cookies to these new tokens using res object.
CODE:     set anti-csrf header to the new antiCsrfToken.
CODE:     set frontToken
CODE:     return session object to user.


      // If the condition below is true, it means that the input refresh token is an immediate
      // child of the current parent refresh token. In this case, we must promote this child to become the parent,
      // and try again
CODE: if refreshTokenInfo.prt !== undefined && sessionInfo.rtHash2 === hash(refreshTokenInfo.prt)
CODE:     update db row: sessionInfo.sessionHandle => {rtHash2: hash(hash(refreshToken)), now + refreshTokenExpiryTime}
CODE:     GOTO <LABEL>


// If the code execution comes here, it means that this refresh token was issued to this user at some point,
// so it's valid. However, it's being used after it's no longer the parent, nor is a child. This means,
// it must be a grandparent in which case, there has been a token theft.
CODE: call token theft function
CODE: clear cookies and throw UNAUTHORISED error

Destroying a session

Input from user:
req: request object. Should contain the accessToken and the idRefreshToken
res: response object
    
Logic:
CODE: sessionObject = get session from cookie (above logic)
CODE: sessionHandle = get it from the sessionObject
CODE: delete db row where sessionHandle = sessionHandle
CODE: clear cookies if a row was deleted.

Regeneration of a session

Input from user:
accessToken: from request
userDataInJWT: JSON Object for new JWT payload. Can be null

Logic:
// The below function extracts the payload from the access token without verifying.
// This is because it this function is only called after an access token has been verified already, 
// so we do not want this to fail in case the current access token has expired during the 
// course of API execution.
CODE: accessTokenInfo = getInfoFromAccessTokenWithoutVerifying(accessToken)
CODE: newJWTPayload = userDataInJWT == null ? getJWTPayloadFromDb() : userDataInJWT


// lmrt stands for last manual regeneration time. It will be used to know when regeneration of a session
// was called last, so that we can determine if a login screen needs to be shown (while the session is alive)
CODE: lmrt =  now
CODE: updateJWTPayload in db with newJWTPayload & lmrt


// if the above succeeds but the below fails, it's OK since the client will get server error and will try
// again. In this case, the JWT data will be updated again since the API will get the old JWT. In case there
// is a refresh call, the new JWT will get the new data.
CODE: if the access token has expired
CODE:     return session info with the new JWT payload, but without updating the access token


CODE: newAccessToken = createNewAccessToken with the old access token's info, except for new lmrt and jwtpayload.
CODE: return newAccessToken