This "Simple" App is Harder Than I Thought: Navigating SPA Security

By Manny V | July 15, 2025 | #WebSecurity #SPADevelopment

As I dive deeper into building the Queue Management System (QMS), I've had a humbling realization: what I initially perceived as a "simple" Single Page Application (SPA) is proving to be far more complex than anticipated, particularly when it comes to web security. My focus has been heavily on the internals, getting the core logic and real-time communication working, but the moment you connect a client-side application to your server's endpoints, a whole new world of concerns opens up.

This post chronicles my unexpected journey down the rabbit hole of modern web authentication and authorization, a critical but often misunderstood aspect of SPA development.

The Security Paradigm Shift: SPA vs. Traditional Web Dev

In traditional, server-rendered web applications, much of the security logic and sensitive data handling happens on the server, away from the client's direct view. The browser primarily receives fully formed HTML pages. With a Single Page Application (SPA), the paradigm shifts dramatically. The client-side application (our React app in this case) is downloaded and executed directly in the user's browser. It then makes direct calls to our server's internal API endpoints.

This architecture introduces unique security challenges. Suddenly, you have to worry intensely about protecting your client-side security, ensuring that your internal API is not exposed to unauthorized access, and preventing common security vulnerabilities like Cross-Site Scripting (XSS) or Cross-Origin Resource Sharing (CORS) issues.

The Misinformation Minefield: .env Files and Sensitive Information

One of the most dangerous pieces of misinformation I encountered early on, particularly in blogs and YouTube tutorials aimed at beginners, was the casual advice to put sensitive information (like API keys or secrets) into .env files for client-side applications. Let me be crystal clear:

You cannot put ANY sensitive information in a publicly inspectable Single Page Web Application.

Anything bundled into your client-side JavaScript code, even if it comes from an .env file during the build process, will ultimately be visible to anyone who inspects your browser's developer tools. This is a fundamental front-end security principle and ignoring it is a recipe for disaster, leaving your API security completely compromised.

The Rabbit Hole: OAuth, JWTs, and PKCE

Realizing the gravity of proper API security for my QMS web app, I went down the rabbit hole. This involved extensive research into OAuth 2.0, the industry-standard protocol for authorization. I delved into the intricacies of JWTs (JSON Web Tokens), which are commonly used to transmit information securely between parties. And then there was PKCE (Proof Key for Code Exchange), a crucial extension for securing public clients like SPAs.

The challenge was that a lot of the readily available posts and tutorials focused heavily on the Authorization Code Flow, primarily for user login scenarios where a user explicitly grants access to an application. While important, this wasn't quite what I needed for certain parts of my QMS.

The Specific Problem: Client Credentials Flow for SPAs

What I specifically needed was a way for my client application (the React SPA) to securely authenticate itself to my server-side endpoints, often referred to as Client Credentials Flow or Machine-to-Machine (M2M) authentication. This is typically used when an application needs to access its own resources, not on behalf of a specific user.

The core problem here is that traditional Client Credentials Flow relies on a client secret, which is meant to be kept confidential. As established, you cannot safely store a client secret in a publicly accessible Single Page Application. Any secret embedded in the client-side code is no longer a secret. This is a common dilemma in modern web development.

The Breakthrough: Scott Brady's Insights

After much searching and head-scratching, I stumbled upon the invaluable resources provided by Scott Brady. His posts cut through the noise and directly addressed the nuances of OAuth for browser-based applications. Specifically, these two articles were game-changers:

  • OAuth 2.0 for Browser-Based Applications Cheat Sheet: This provided a clear overview of the recommended flows and security considerations for SPAs.
  • Client Authentication vs. PKCE: This article directly addressed the challenge of client secrets in public clients and highlighted why PKCE is the appropriate solution for securing the Authorization Code Flow for SPAs, even when you're not dealing with a traditional user login. It helped clarify that for machine-to-machine type interactions originating from a browser, a backend proxy is often the most secure approach, where the client secret is kept server-side.

These resources clarified that for scenarios where the SPA itself needs to authenticate to a backend API without a user context (like fetching public configuration or initial data), the SPA typically acts as an intermediary, and the actual authentication (including any client secrets) should happen securely on the backend server, which then proxies the request to the target API. This keeps the client secret truly secret. For specific user actions, the Authorization Code Flow with PKCE is the way to go.

Current Status: Primitive but Functional Security

Now that I have a clearer understanding of security best practices for SPAs and how to handle client-server communication securely (albeit in a very primitive form for this proof of concept), I can finally breathe a sigh of relief. The foundational security layer is in place, protecting our QMS API endpoints.

This detour into web security was unexpected and consumed a significant amount of time, but it was absolutely essential. You cannot build a reliable and trustworthy system, especially one handling sensitive customer data or operational flows, without a solid understanding of its security posture.

The Path Forward: Back to the Core Problem

With the security groundwork laid, I can now continue and focus on the actual problems: implementing the UI workflow I had in mind for the QMS, building out the core queue management features, and enhancing the customer experience. This journey has reinforced a critical lesson: sometimes, the "simple" parts of development hide the deepest complexities. Prioritizing foundational elements like security, even if they seem like a detour, is paramount for building a robust and sustainable product.

Stay tuned for updates on building out the user interface and the core queueing logic!