As a full-stack developer, I often revisit and refine core features like user authentication to ensure they’re secure, efficient, and scalable. Recently, I dedicated time to overhauling the auth system in an application. The app was functional but redundant and needed to evolve into a cleaner, more robust version by using Bearer tokens.
In this blog post, I’ll walk you through the before & after, the reasoning behind the changes, and the trade-offs considered. This not only improved the app’s security posture but also simplified maintenance.
The Original Setup: Functional but Flawed
In the original setup, after a successful login, the backend would generate and return a JSON Web Token (JWT) that included the user’s ID. However, the backend also sent along the user slug. On the frontend, both were stored using React’s Context API. For every protected API request, the frontend would include the slug in the request body (not as a URL parameter) alongside the token.
On the backend, the process involved:
- Fetching the user record based on the provided slug.
- Verifying the JWT and decoding it to extract the user ID.
- Comparing the decoded ID with the one from the fetched user record to ensure they matched.
This approach effectively prevented unauthorized access and avoided common vulnerabilities like Insecure Direct Object References (IDOR). However, it introduced unnecessary redundancy. The JWT already contained a trusted user ID, so we didn’t need the user slug.
Room for Improvement
While the system worked, it had several drawbacks.
1. Redundancy and Payload Overhead
Sending the slug with every request increased the payload size, albeit slightly, and required extra code on the frontend to manage and include it.
2. Increased Complexity
The frontend’s fetch logic and Context API became more convoluted, handling both the token and slug. This added maintenance burden and the potential for bugs.
3. Potential Risks Over Time
Even with strong backend checks, relying on user controlled data (the slug) could lead to issues in the future, such as:
- A new endpoint forgetting the ID comparison.
- Refactors altering slug handling, introducing inconsistencies.
4. Unnecessary Client-Side Logic
The backend could derive all necessary information from the signed JWT alone, making client-side trust in the slug irrelevant.
By eliminating the slug, I aimed to reduce the attack surface, streamline code, and establish the backend as the single source of truth for authorization.
The Refactor
On the frontend, I now only store and send the Bearer token in the Authorization header for protected requests. (No more slugs) Requests are now easier to manage. On the backend, when a request is received, the JWT is verified and decoded to extract the trusted user ID directly from the token. This ID then serves as the basis for identifying the requesting user.
For operations that involve specific resources (e.g., fetching or updating user data, posts, profiles, etc.), the backend still explicitly checks that the resource’s owner matches this decoded user ID. This prevents the possibility of one authenticated user from editing or accessing another user’s data.
The results
The app has cleaner code on both the front and backend. I was able to reduce the complexity of the logic in the Context API and API calls. There is now a smaller attack surface, the app no longer trusts (handles) additional user provided data. Lastly the app is now more scalable since there is less logic to maintain.
Trade-Offs and Considerations
Here are the trade-offs I considered before proceeding.
1. Header Size
Including the full token in headers adds a negligible increase in size for this app, far outweighed by the benefits.
2. Loss of Quick References
Previously, slugs allowed for easy client-side referencing, but since these are authenticated routes, it’s not essential. The token handles everything securely.
3. Token Management
The frontend must handle token expiration and refreshes properly. Fortunately, this was already in place, so no additional work was required.
Why This Matters
Refactoring the authentication is about proactively improving the app making it more scalable and maintainable. This helped me reinforce the importance of questioning redundant patterns and prioritizing backend driven security.
