-
Notifications
You must be signed in to change notification settings - Fork 541
Session pseudo code
Rishabh Poddar edited this page Oct 3, 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.
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}, 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: express request object. Should only contain the refreshToken and idRefresh cookies
res: express response object
Logic:
get session from cookie (above logic)
sessionHandle = get it from the session object.
delete db row where sessionHandle = sessionHandle
clear cookies if a row was deleted.