Background
Hello all, this will be a slightly in-depth post that I hope helps some developers out there.
We are using OAuth and one of the ways we are using session management on the Front-end is by providing cookies of each of the Access Token and Refresh Token expiration dates.
Assumptions
I assume that the reader is knowledgable on using .split, .map, and .reduce array methods.
The Problem
Our Backend is built using Ruby on Rails and the problem I was facing when the response cookies were set were:
- Multiple cookies were stored for the Frontend
- Both the access token expiration and refresh token expiration are stored as a Ruby hash in an encoded string.
// document.cookie
'FLIPPER_ID=flipper_on; token_info=%7B%3Aaccess_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A11%3A58.265440745+UTC+%2B00%3A00%2C+%3Arefresh_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A36%3A58.147084176+UTC+%2B00%3A00%7D'
Requirements
From here I had a few requirements of my own to ensure the session management work as I expected
- An object with both the access token & refresh token expirations
- Having the expirations be usable JavaScript dates
Part 1 of the Creating Our Cookie Object
const oAuthCookieObject = document.cookie
// Creates an array of each cookie
.split(';')
// Maps the cookies to <key>=<value> pairs
.map(cookie => cookie.split('='))
/*
Reduces it down to a single object of our access token and
refresh tokens by checking if our cookieKey includes the
'info_token' value we are looking for
*/
.reduce((_, [cookieKey, cookieValue]) => ({
...(cookieKey.includes('info_token') && {
...formatOurCookie(decodeURIComponent(cookieValue))
})
}), {});
Part 1 Breakdown
1) We create a variable from document.cookie
2) We split each cookie string
// original string
'FLIPPER_ID=flipper_on; info_token='
// after .split
['FLIPPER_ID=flipper_on', 'info_token=%7B%3Aaccess_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A11%3A58.265440745+UTC+%2B00%3A00%2C+%3Arefresh_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A36%3A58.147084176+UTC+%2B00%3A00%7D']
3) We map each cookie to a new array of arrays by splitting on the '='
// original array after .split
[
'FLIPPER_ID=flipper_on',
'info_token=%7B%3Aaccess_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A11%3A58.265440745+UTC+%2B00%3A00%2C+%3Arefresh_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A36%3A58.147084176+UTC+%2B00%3A00%7D'
]
// after we use .map
[
['FLIPPER_ID', 'flipper_on']
['info_token', '%7B%3Aaccess_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A11%3A58.265440745+UTC+%2B00%3A00%2C+%3Arefresh_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A36%3A58.147084176+UTC+%2B00%3A00%7D']
]
4) We reduce to a single usable object by destructing the cookie's key|value pair if it matches our 'info_token' and call another function with the value being interpreted as a decodedURIComponent string.
// String before decodeURIComponent is called
const nonDecoded = '%7B%3Aaccess_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A11%3A58.265440745+UTC+%2B00%3A00%2C+%3Arefresh_token_expiration%3D%3EFri%2C+17+Jun+2022+16%3A36%3A58.147084176+UTC+%2B00%3A00%7D';
// String after decodedURIComponent is called
const decoded = '{:access_token_expiration=>Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00,+:refresh_token_expiration=>Fri,+17+Jun+2022+16:36:58.147084176+UTC++00:00}'
Part 2 of Creating our Cookie Object formatOurCookie
function
function formatOurCookie(unformattedCookieString) {
return unformattedCookieString
// Creates an array by splitting on ',+:' to get the access token and refresh token
.split(',+:')
.reduce((obj, cookieVal) => {
// Destructure the key|value pair of the token's name and its expiration date and uses Regex to remove {: and }
const [key, val] = cookieVal
.replace(/{:|}/g, '').split('=>')
// Update the value by replacing the '+' with spaces and removing the UTC timezone ending
const formattedValue = val
.replaceAll('++00:00', '')
.replaceAll('+', ' ')
// Return's the accumulator and the key|value pair with a usable JavaScript Date object
return {
...obj,
[key]: new Date(formattedValue)
}
}, {})
}
Part 2 of Breakdown of formatOurCookie
function
1) Take the unformattedCookieString
parameter which will be a decodeURIComponent
string and use the split method on ',+:'
to get the access_token_expiration and the refresh_token_expiration into an array
// original string
'{:access_token_expiration=>Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00,+:refresh_token_expiration=>Fri,+17+Jun+2022+16:36:58.147084176+UTC++00:00}'
// array split on the `',+:'`
[
'{:access_token_expiration=>Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00',
'refresh_token_expiration=>Fri,+17+Jun+2022+16:36:58.147084176+UTC++00:00}'
]
2) Use the .reduce method to loop through the split array with the goal being to reduce it into a single object.
3) We want to destructure the key|value pairs by
a. First removing all instances of :{
and }
from the string.
// original (removes `:{`)
'{:access_token_expiration=>Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00'
// after removes `:{`
'access_token_expiration=>Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00'
// after removes `}`
'refresh_token_expiration=>Fri,+17+Jun+2022+16:36:58.147084176+UTC++00:00'
b. Then by splitting the string on the =>
using the .split method
// original
'access_token_expiration=>Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00'
// transformed
[
'access_token_expiration',
'Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00'
]
c. Format the key's value into a usable format by replacing the +
with a single space and removing the ++00:00
// original
'Fri,+17+Jun+2022+16:11:58.265440745+UTC++00:00'
// formatted
'Fri, 17 Jun 2022 16:11:58.265440745 UTC'
4) Return the accumulator and coerce the above string into a usable JavaScript Date
TL/DR
const oAuthCookieObject = document.cookie
.split(';')
.map(cookie => cookie.split('='))
.reduce((_, [cookieKey, cookieValue]) => ({
...(cookieKey.includes('info_token') && {
...formatOAuthCookie(decodeURIComponent(cookieValue))
})
}), {});
function formatOurCookie(unformattedCookieString) {
return unformattedCookieString
.split(',+:')
.reduce((obj, cookieVal) => {
const [key, val] = cookieVal
.replace(/{:|}/g, '').split('=>')
const formattedValue = val
.replaceAll('++00:00', '')
.replaceAll('+', ' ')
return {
...obj,
[key]: new Date(formattedValue)
}
}, {})
}
Hopefully some of you found that useful. Cheers! 🎉
Top comments (2)
thanks for sharing the code! interesting use case with multiple cookies..any specific reason why if you dont mind me asking?
Sure. On our project we have multiple cookies being set for OAuth with most of them being httpOnly cookies (meaning they aren't accessible to JavaScript). We decided to store the token's expiration in a cookie available in JavaScript so that we could reduce the number of network requests and only call a silent refresh request when needed.