Back to Blog

Secure File Downloads in React The Handshake Token Pattern

Royan Gagas
January 9, 2026
Share
tips
development
go
Secure File Downloads in React The Handshake Token Pattern

You know that feeling when a project is 99% done, the charts look beautiful, the data is crisp, and then the client asks:

Bro, can we add an 'Export to Excel' button?

It sounds like a five minute task. You just create an endpoint, slap an <a href="..."> tag in your React app, and call it a day, right?

Well, not if you care about security.

I ran into this exact wall while building Jurter. Our API is strictly locked down with Authorization: Bearer headers. The problem is, standard browser links don't support headers.

You cannot add a custom header to an <a> tag or window.location. This leaves us with a dilemma. How do we trigger a file download on a secured API?

The Trap Just use fetch and Blob it!

If you Google this problem, the most common advice is. "Use fetch (or axios) to send the header, get the response as a Blob, and create a temporary object URL."

I tried this. Technically, it works. But for a production app, it introduced two major flaws that I couldn't accept:

1.Memory Hog (The RAM Issue): When you use fetch, the browser must download the entire file into Javascript memory before it can save it to the disk. If a user tries to export a massive yearly report (say, 50MB+), you risk freezing the UI or crashing the browser tab entirely.
2.The Invisible Download: With fetch, the user doesn't see the native browser download bar. They just stare at a spinner on your button. They have no idea if the download is 10% or 99% done. It feels broken.

I wanted the native experience. Click the button, see the browser's download manager fly out, and handle files of any size without touching RAM.

But I also needed the security of my Bearer token.

The Solution is The Temporary Handshake

I solved this using a strategy I call the "Temporary Handshake"

Instead of forcing headers into the download request, I split the process into two steps. Think of it like a coat check at a concert. You show your ID (Authentication) to get a paper ticket. Later, you just hand over the paper ticket to get your coat.

Phase 1 - The Secure Handshake

When a user clicks "Export" in my React app, I don't trigger the download yet. Instead, I fire a standard, authenticated POST request to my backend.

Since this is an AJAX request, it carries the user's Auth headers perfectly.

On the backend (Go/Fiber), I validate the user and generate a short-lived token (the ticket).

This part is crucial: I'm saving the UserId and the Date Filters inside the token store on the server. The token itself is just a random string. it contains no data to decode.

Phase 2 - The Public Execution

Once the frontend receives this token, the hard work is done. We have a valid "ticket". Now we can use the browser's native capabilities.

Because we use window.location, the browser handles the stream. The file goes directly to the hard drive. No RAM spikes, and the user gets their native progress bar.

Why This is Secure (The Parameter Pinning)

You might ask: "Wait, isn't that a public link? Can't someone change the URL parameters?"

A malicious user might try to be clever. They might get a valid token, and then try to change the URL to access someone else's data:

.../export?token=VALID_TOKEN&user_id=ADMIN_ID

But my backend logic simply ignores the URL parameters during the download phase. It look up the token in memory and uses the UserID stored there.

Conclusion

We didn't avoid security, we just moved it. By decoupling the Authorization (getting the token) from the Action (downloading the file), we get the best of both worlds.

User Experience: Smooth, native downloads with progress bars.
Performance: Zero impact on browser memory (RAM).
Security: Full protection against IDOR (Insecure Direct Object References) via parameter pinning.

Sometimes, the best engineering solution is just a simple handshake.