mohd-faraz

_blogs

// blogs / 20260813.md

Dev Log: August 13 Wrap-up

2026-08-13
#security#auth#debugging#typescript#backend

Overview

Today was a bit of a deep dive into the plumbing of our chat widget's auth flow. Most of the day was spent tightening up how we handle session tokens and making sure our security constraints (like CORS and session binding) are actually doing their jobs instead of just looking good on paper.

What I Worked On

Squashing the "Session Access Denied" Loop

I spent a good chunk of the morning debugging a frustrating bug where refreshing the chat page would occasionally lock users out. It turns out that every time the component initialized, it was unconditionally fetching a new flow-level token. If a user already had an active session, that new token would clobber the session-scoped one, leading to a spurious "Session access denied" error.

I updated the logic to check for a valid, unexpired token in local storage first. It sounds simple, but it prevents the widget from nuking its own session state on every page reload.

// Only fetch a new token if the current one is missing or stale
let token = this.hasValidToken() ? storage.get('JWT_Token') : '';

if (!token) {
  const response = await this.fetchToken(this.config);
  token = response?.access_token || '';
}

if (token) {
  this.initializeChat(token);
}

Hardening Sessions Against Hijacking

On the backend, I realized we were relying a bit too much on self-reported headers for session tracking. That’s a classic vulnerability. To fix this, I modified the token generation to cryptographically bind the session ID to the JWT.

Now, once a chat session is established, the token itself carries the session_id. If someone tries to spoof a session header but their token doesn't match, the backend just shuts it down. It’s a much more robust way to handle ownership than just trusting whatever the client sends in a header.

Cleaning up CORS and Origins

I also took a pass at our CORS middleware. We had some wildcard settings that were a bit too permissive for an app that handles credentials. I switched it to a proper allowlist with regex support.

While I was in there, I noticed our workflow utility was defaulting to a localhost origin for guest sessions because it couldn't find a domain ID in the context. I added a helper to resolve the actual caller's origin from the request headers so that our allowlist checks actually mean something in production.

Passing reCAPTCHA to the Iframe

Finally, a small but necessary fix for our builder interface. Since it runs in an iframe, it’s isolated and can't see global configuration variables on the parent window. I updated the loader to pass the reCAPTCHA site key as a query parameter. Since it's a public key anyway, there's no security risk, and it finally allows the embedded app to handle validation properly.

Wrapping Up

It feels good to have the auth flow feeling a bit more solid. Security work can be tedious because when it works, nobody notices, but it's much better than the alternative. Tomorrow, I'm hoping to get back into some more feature-focused work now that the foundation is stable.