-
Notifications
You must be signed in to change notification settings - Fork 541
Session pseudo code
Rishabh Poddar edited this page Oct 8, 2020
·
10 revisions
Below is the pseudo code for implementing rotating refresh tokens (RRTs) in an efficient manner. The challenges of implementing RRTs are described in this video
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.
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'
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)
);
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
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.
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
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.
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