From Cookie strings into usable JavaScript

Parsing Cookie strings and transforming them into usable JavaScript because backend engineers were busy

Read time is about 11 minutes

Alexander Garcia is an effective JavaScript Engineer who crafts stunning web experiences.

Alexander Garcia is a meticulous Web Architect who creates scalable, maintainable web solutions.

Alexander Garcia is a passionate Software Consultant who develops extendable, fault-tolerant code.

Alexander Garcia is a detail-oriented Web Developer who builds user-friendly websites.

Alexander Garcia is a passionate Lead Software Engineer who builds user-friendly experiences.

Alexander Garcia is a trailblazing UI Engineer who develops pixel-perfect code and design.

Background

If you've ever worked on a frontend that consumes cookies set by a backend you don't control, you know the pain. The backend team serializes data in whatever format is natural for their language, and you're left parsing it on the client side with JavaScript. This is especially common in OAuth session management where token metadata may be stored in cookies.

At VA.gov, our authentication system set cookies containing access token and refresh token expiration dates. The frontend needed those dates to manage session lifecycle — things like knowing when to trigger a token refresh, displaying session timeout warnings, and determining if the user's session had expired. The problem? The expiration dates were stored as a Ruby hash, URI-encoded, and crammed into a single cookie string.

We were on a tight deadline and the backend didn't have the resources — as a result the token service wasn't set up to serialize the cookie values as JSON, and refactoring it would have meant blocking the entire OAuth launch. So rather than wait, I wrote a parser on the frontend to transform the raw cookie string into usable JavaScript Date objects. Sometimes you don't get to pick the ideal solution — you pick the one that ships. This is a pattern you'll encounter anytime your frontend and backend speak different languages — literally.

The Problem

Our backend was built using Ruby, and the cookies it set had two issues:

  1. Multiple cookies were stored for the frontend, and only one of them (info_token) contained what we needed
  2. The access token expiration and refresh token expiration were serialized as a Ruby hash inside a URI-encoded string — not JSON, not a timestamp, but a raw Ruby object coerced to a 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

Before writing any code I defined what the end result needed to look like:

  • A single JavaScript object with both the access token and refresh token expirations
  • Each expiration value as a native JavaScript Date object (not a string) so I could do comparisons like if (Date.now() > accessTokenExpiration)
  • No external dependencies — just vanilla JavaScript that could run in any browser

The goal was to go from that unreadable encoded string to something like:

{ access_token_expiration: Fri Jun 17 2022 16:11:58 GMT+0000, refresh_token_expiration: Fri Jun 17 2022 16:36:58 GMT+0000 }
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

  • We create a variable from document.cookie
  • 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") ];
  • 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'] ]
  • 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: The 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: Breakdown

  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}") ];
  1. Use the .reduce method to loop through the split array with the goal being to reduce it into a single object.

  2. 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";
  1. Return the accumulator and coerce the above string into a usable JavaScript Date

The complete solution

const oAuthCookieObject = document.cookie .split(";") .map((cookie) => cookie.split("=")) .reduce( (_, [cookieKey, cookieValue]) => ({ ...(cookieKey.includes("info_token") && { ...formatOurCookie(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), }; }, {}); }

Fair question. Libraries like js-cookie are great for reading and writing simple key-value cookies. But they don't solve this problem. The challenge here wasn't getting the cookie — document.cookie handles that. The challenge was parsing the value which was a URI-encoded Ruby hash, not JSON. No cookie library is going to know how to deserialize {:access_token_expiration=>Fri,+17+Jun+2022...} into a JavaScript Date. That's custom parsing logic no matter what.

Lessons learned

A few things I'd do differently if I were writing this today:

1

Ask the backend to change the format

The cleanest solution is always to have the backend serialize cookie values as JSON. A Ruby hash stringified as :key=>value is a Ruby implementation detail that shouldn't leak to the client. If the backend had set the cookie as JSON.generate, the frontend would have just called JSON.parse() and been done.

2

Use timestamps instead of date strings

Unix timestamps (1655482318) are language-agnostic and timezone-safe. Parsing human-readable date strings like "Fri, 17 Jun 2022 16:11:58" with new Date() works but depends on the browser's date parser accepting that exact format. Timestamps eliminate that ambiguity entirely.

3

Add error handling

The code above assumes the cookie exists and is formatted correctly. In production at VA.gov, we wrapped this in a try/catch and fell back to a logged-out state if parsing failed. Cookie formats can change without warning when the backend deploys — defensive parsing saves you from a blank screen.

That said, sometimes you don't control the backend and you can't wait for changes. In those situations, frontend string parsing like this is the right tool. The broader skill here — chaining split, map, reduce, and regex to transform messy data into clean structures — applies far beyond cookies. I've used the same pattern to parse CSV exports, log files, and URL query strings that didn't follow standard conventions.

Wrapping up

The next time your backend hands you a cookie that looks like a serialized object from another language, don't panic. Break the problem down step by step: split the string, decode it, extract the pieces you need, and coerce them into native JavaScript types. It's not glamorous, but it's the kind of practical problem-solving that keeps production applications running.