Protecting API keys used in browser front-end apps: Chat with Gemini

Given below are extracts from a recent chat with Gemini. The context is a Blogger page with Javascript code that makes Blogger v3.0 API calls which need a Google API key. I had introduced restrictions on the key like websites it can be used on and that it is enabled only for Blogger API.

Me: While with above restritions the API key is less exposed to misuse, I am still quite uneasy about having the API key so easily accessible. In this context, I am curious whether if we convert this into a React project and use the API key as an environment variable, will that prevent the API key from getting exposed to the user?

Gem: You've hit upon a critical distinction in web development security! While using environment variables in a React project is a great practice for managing API keys during development and deployment, it generally does not prevent the API key from being exposed to the end-user's browser if the code runs entirely on the client-side.

Let's break this down:

1. Environment Variables in Client-Side React (e.g., Create React App, Vite):

  • How they work: When you use a .env file with REACT_APP_ prefixes (or VITE_ for Vite) and access them via process.env.REACT_APP_YOUR_KEY in your React code, these variables are bundled and embedded into the final JavaScript code that gets sent to the user's browser.

  • Exposure: Because the JavaScript code runs in the user's browser, anyone can use their browser's developer tools (e.g., "Inspect Element," "Network" tab, or even by just viewing the page source) to inspect the bundled JavaScript files. They can then find the API key plain as day within that code.

  • Benefit: The main benefit of using .env files in this scenario is developer convenience and preventing accidental commits of sensitive keys to version control (like Git). You don't hardcode the key in your source files, and your .env file is typically added to .gitignore. This is excellent for keeping your codebase clean and preventing leaks during development, but it's not a solution for hiding the key from the end-user.

2. How to truly hide API keys from the client (and when you need to):

To genuinely prevent an API key from being exposed to the client, you need to ensure the API call that uses the key happens on a server-side environment. This means the key never leaves your server.

Here are the common approaches for this in a React context:

  • a. Backend Proxy Server:

    • How it works: Your React (frontend) application doesn't call the Blogger API directly. Instead, it makes a request to your own custom backend server (e.g., built with Node.js/Express, Python/Flask, etc.). Your backend server then uses its API key (stored securely as a server-side environment variable) to call the Blogger API, processes the response, and sends only the necessary data back to your React frontend.

    • Security: This is the most secure method because the API key never touches the client's browser. It stays on your server.

    • Complexity: Requires setting up and maintaining a separate backend server.

  • b. Serverless Functions (e.g., Netlify Functions, AWS Lambda, Vercel Functions):

    • How it works: This is a popular and often simpler alternative to a full backend server. You write small, isolated server-side functions (usually in Node.js) that get deployed to a serverless platform. Your React app calls these serverless functions, and the functions, in turn, make the API call to Blogger using the securely stored API key (as an environment variable on the serverless platform).

    • Security: Similar to a backend proxy, the API key is never exposed to the client.

    • Complexity: Simpler than a full backend for many use cases, but still requires understanding serverless concepts and deploying functions.

  • c. Server-Side Rendering (SSR) with Frameworks (e.g., Next.js):

    • How it works: Frameworks like Next.js allow you to pre-render React components on the server. If the API call (using the sensitive key) happens during this server-side rendering phase (e.g., in getServerSideProps in Next.js), the data is fetched on the server, and only the rendered HTML (without the API key) is sent to the client.

    • Security: API keys used during server-side data fetching remain on the server.

    • Complexity: Involves adopting a server-side rendering framework, which changes how your React application is structured and deployed.

Conclusion for your Blogger API key:

Given that your Blogger API key is for accessing public data (your published posts), and you've implemented strict domain-based referrer restrictions, the risk of misuse (beyond quota consumption by reading other public blogs if your domains aren't specific enough) is relatively low.

While converting to a React project and using environment variables is a good development practice, it will not hide the API key from the user if the React app is purely client-side. To truly hide it, you would need to introduce a server-side component (a custom backend, serverless functions, or SSR).

For your specific use case, the current solution with client-side JavaScript and strict referrer restrictions is often considered a practical balance between security and implementation complexity, given the nature of the data being accessed. However, if the API key were for sensitive, non-public, or write operations, then a server-side approach would be absolutely mandatory.

Me: Hmm. I have a small React app that is a Gemini API trial. It needs a Gemini API key. I put that key in .env variable thus preventing its exposure from the public GitHub repo associated with the project. But the app is deployed publicy (on Netlify IIRC). So any hacker-type user who wants to extract my Gemini API key and then use it for his/her own purposes, can extract it by examining the source code delivered to his browser when he loads the app URL. Am I right?

Gem: You are absolutely and entirely correct in your assessment!

For a typical client-side React app deployed on Netlify (or any static hosting service like Vercel, GitHub Pages, etc.):

  1. .env for Source Control: Using .env variables is indeed excellent for preventing the API key from being accidentally committed to your public GitHub repository. This is crucial for keeping your development environment clean and preventing a major leak.

  2. Deployment and Exposure: However, when Netlify (or any build process) builds your React app for deployment, it takes those REACT_APP_ (or VITE_) prefixed environment variables and "bakes" their values directly into the static JavaScript bundles. These bundles are then served to the user's browser.

  3. Client-Side Visibility: Once that JavaScript code is in the browser, any "hacker-type user" (or simply a curious developer) can:

    • View Page Source: Look at the raw HTML and linked JavaScript files.

    • Browser Developer Tools:

      • Go to the "Sources" tab and navigate through the JavaScript files.

      • Go to the "Network" tab, inspect the API calls being made, and see the request parameters (including the API key if it's sent directly in the URL or headers).

      • Search through the bundled JavaScript files for the API key string or the variable name you used.

Therefore, your Gemini API key, when used in a purely client-side React app deployed publicly, is indeed exposed to anyone who inspects the browser's network requests or bundled JavaScript.

What does this mean for a Gemini API key?

  • Quota Consumption: The primary risk is that someone could take your key and start making their own Gemini API calls, potentially consuming your free quota and possibly incurring costs if you have billing enabled and exceed the free tier.

  • Malicious Use (less common for Gemini): While less common for generative AI APIs than, say, payment processing APIs, a malicious actor could try to use your key to generate spam, harmful content, or just waste resources.

How to truly secure it (as discussed for Blogger, applies even more here):

To prevent the Gemini API key from being exposed to the client, you must route your API calls through a server-side component. For Netlify, the most common and convenient way to do this is using Netlify Functions (which are powered by AWS Lambda under the hood).

General flow with Netlify Functions:

  1. Your React app (frontend) makes an API call to a Netlify Function endpoint (e.g., /api/gemini-proxy).

  2. The Netlify Function (backend code) is deployed separately on Netlify's serverless infrastructure.

  3. Inside the Netlify Function, you securely store your Gemini API key as an environment variable within Netlify's build settings for that function's scope. This key is never sent to the browser.

  4. The Netlify Function uses this secure API key to make the actual call to the Gemini API.

  5. The Netlify Function receives the response from Gemini and sends only the relevant data back to your React app.

This way, the API key remains server-side, completely invisible to the client.

For a trial or small personal app where the potential for extreme misuse or cost is low, you might choose to accept the client-side exposure. But for anything more serious, a serverless function or dedicated backend is the way to go.

...

Me: I guess since I am on Google free tier as of now, the worst case scenario is somebody extracting my Google API keys and using them to exhaust my quota after which my apps API calls will fail. If and when I run into such issues that's when I should consider making them secret (with you having given some of the ways to accomplish that). For now since I am on free tier and these apps I am discussing with you are NOT production web apps with low tolerance of failure (due to API quota exhaustion), I think I can avoid the extra hassles of making the keys truly secret. What do u say?

Gem: Yes, your assessment is absolutely sound and pragmatic for your current situation!

You've perfectly articulated the trade-offs involved:

  • Understanding the Risk: You correctly identify that the primary risk on the free tier is indeed quota exhaustion, leading to your applications temporarily failing.

  • Cost vs. Benefit: You're wisely weighing the "hassle" (complexity and time investment) of implementing server-side solutions against the current, low-impact consequences of key exposure on your non-production, free-tier applications.

  • Just-in-Time Security: Your strategy to address the "secrets" problem when and if it actually becomes a problem (e.g., hitting quotas, or moving to a production context) is a very reasonable approach. Over-engineering security for non-critical, free-tier projects can often consume valuable development time without providing immediate, tangible benefits.

In short: I completely agree with your approach.

For personal projects and applications on a free tier, where the worst-case scenario is temporary service interruption due to quota exhaustion (rather than, say, financial fraud or data breaches), prioritizing ease of development and deployment is a very common and sensible choice.

Just keep in mind the potential solutions (serverless functions, custom backends) in your toolkit for when these projects evolve, or when you encounter those quota issues. You've got a clear understanding of the implications, which is the most important thing!

Me: Just for knowledge sake ... If I store the API key on local storage in my browser (using a separate small private web app) then I could read the API key from local storage instead of hardcoding it in front end code. If I do not get the key from local storage I could prompt the user to specify the (his/her own Google Blogger API) key (which I can save to local storage for future use and prevent such prompts). Such an approach is NOT user friendly and will work only for developers familiar with Google Blogger API keys. But in this case, the key is not exposed to hackers/developers. Am I right?

Gem: You're thinking creatively about how to manage keys, and it's a good thought process to explore different approaches!

However, storing the API key in local storage in the browser, even if entered by the user, generally does NOT prevent it from being exposed to hackers/developers.

Let's break down why:

  1. Local Storage is Client-Side: Just like variables embedded in your bundled JavaScript code, data in local storage resides directly in the user's browser. It's designed for client-side persistence.

  2. Accessibility:

    • Browser Developer Tools: Anyone can easily open their browser's developer tools (usually F12 or right-click -> Inspect) and go to the "Application" tab (or "Storage" depending on the browser). They can then inspect, read, and even modify anything stored in local storage. It's stored in plain text by default.

    • Cross-Site Scripting (XSS) Attacks: This is the most significant vulnerability. If your web app ever has an XSS vulnerability (e.g., if you display user-generated content without proper sanitization), a malicious script injected by an attacker could:

      • Read everything in local storage.

      • Send that data (including your API key) to the attacker's server.

      • Execute API calls using your key.

    • Browser Extensions: Malicious browser extensions could potentially read data from local storage, depending on their permissions.

    • Malware: If a user's computer is compromised by malware, the local storage files (which are usually just simple files on the user's hard drive) could be directly accessed.

So, while your approach of having the user input their own key is thoughtful and bypasses the "hardcoding in public repo" problem, it doesn't solve the "key exposed in the browser" problem. The moment that key hits the browser's local storage, it becomes vulnerable to client-side attacks.

Your assessment regarding user-friendliness is also correct:

  • Not User-Friendly: Requiring users to obtain and input their own Google Blogger API key is highly inconvenient and impractical for a general audience. It would indeed only appeal to developers or very technical users.

The "true secrecy" for API keys remains:

Any API key that grants access to sensitive operations, private data, or has significant financial implications (like a Gemini key which can incur costs) must reside on a server you control. This means:

  • Backend Proxy / Serverless Function: As discussed, your frontend app calls your backend (serverless function), which then securely uses the API key to call the external API (Blogger, Gemini, etc.). The key never leaves your server.

This scenario underscores that while a user providing their own key seems to solve the "your key" problem, it just shifts the exposure to "their key" if that key is then stored insecurely on the client.

For the specific Blogger API key scenario you have, the existing solution with domain restrictions is a good balance for public data. For Gemini, if you want full secrecy, a serverless function is the way to go.

Me: Yes, XSS vulnerability in program and virus on client PC will expose it. But the point is that it will expose only the user's own key. If my PC does not have a virus and my app does not have an XSS vulnerability then ***my Google API key*** kept in local storage is not visible to some external person who has no direct access to my PC. If the external person uses the app and specifies his/her own Google API key then only that key will be in his/her local storage and so exposed to him/her. That's no problem. The external person should not be able to access somebody else's Google API key.

Gem: You've articulated the specific concern and distinction very clearly!

You are absolutely right:

  • Your Key vs. User's Key: If you modify your app to prompt the user for their own Google Blogger API key and store that key in their local storage, then yes, the only key at risk on that specific user's machine is their own key. Your original key would not be embedded in the code or their local storage.

  • Reduced Risk for You (as the developer): This indeed means your personal Google API key is no longer directly exposed to anyone who simply loads your application. This is a significant improvement from your perspective as the app developer.

  • Client-Side Risks Remain: However, it's crucial to understand that while your key is now protected, the user's key (the one they input) is still subject to all the client-side vulnerabilities we discussed:

    • XSS: If your app has an XSS vulnerability, a malicious script injected by an attacker could still steal the user's own API key from their local storage.

    • Malware: If the user's PC is infected with malware, that malware could still access the user's local storage and extract their key.

    • Browser Dev Tools: Any technically savvy user could still inspect their own browser's local storage and see their own key.

In essence:

Your proposed method shifts the direct exposure of your API key from the public code to the user's responsibility to provide and manage their own key. This is a valid security improvement for your assets.

However, the fundamental vulnerability of local storage as a secure place for any sensitive credential (whether it's your key or the user's key) remains. It's not a secret store; it's a client-side data store.

So, for a niche application targeted at developers who are expected to manage their own API keys, your proposed approach is perfectly acceptable and enhances your security posture. For a general public application, the user-friendliness issue (requiring users to get their own keys) would be a major barrier, and the underlying client-side insecurity of local storage would still apply to their keys.

Me: Well, I will put it in another way. I am shifting the responsibility of protecting local storage including the user's Google API key, stored on user's PC on to the user. For a simple app or a blog page for that matter, where I don't want to overload it with serverless function complexity or similar complexity (development as well as management complexity/cost), I think that's a fair deal. The user can always chose to cancel the prompt asking for his/her own Google key (and which prompt can have textual content explaining that the key he/she provides will be stored in their browser's local storage and has to be protected by them) and not avail of the app functionality or blog page functionality (like list of all posts on the blog).

Gem: You've articulated a very common and perfectly reasonable approach, especially for personal projects and "hobby" applications!

You are correct: you are effectively shifting the responsibility of key protection onto the user for their own key.

Here's why your reasoning is sound in this context:

  1. Reduced Developer Burden: You avoid the complexity, development time, and potential ongoing costs (even if small) of setting up and managing a server-side component (like a Lambda function or a custom backend) for a simple, non-critical application. This is a huge win for developer velocity and simplicity.

  2. Explicit Consent and Transparency: By clearly stating that the user's key will be stored locally and that they are responsible for its protection, you are being transparent. This empowers the user to make an informed decision. If they're a developer or technically savvy, they'll understand the implications.

  3. User Choice: The user has the explicit choice to:

    • Provide their key: If they want to use the functionality and are comfortable with the risks and responsibility.

    • Decline: If they are not comfortable, they simply won't use that specific feature of your app/page.

  4. Nature of the Data/App: For a blog "list all posts" functionality, the API access is inherently read-only for public data. The impact of a compromised user's Blogger API key is primarily limited to their own quota being consumed for public reads, which is a low-risk scenario compared to, say, a payment processing API.

In summary, this is a perfectly fair deal for a simple, non-production application, especially when the target audience is technical or when you clearly communicate the implications.

It's a classic example of balancing security rigor with practicality and development efficiency. For apps where the stakes are low, the user-provided, client-side storage model (with clear warnings) can be a perfectly acceptable compromise.

Me: One benefit of this approach is that the "list all posts" code can be made generic such that it can work for variety of blogger blogs. User's Google API key is controlled by them and they can specify which websites it should work with. Specifically if the "list all posts" code is copied into a page in xyz.blogspot.com and some user tries to use that page to list all posts of the xyz.blogspot.com blog, so long as the key they provide supports that website (or supports all websites), it will work. Whereas if the code is hardcoded with my key which is restricted only to my blogs, the code will not work for xyz blog.

Gem: You've identified a very powerful and often overlooked benefit of the "user-provided key" approach for generic applications!

You are absolutely correct. Here's why that's a significant advantage:

  • Generality and Reusability:

    • If you embed your specific API key, restricted to your blogs (e.g., raviswdev.blogspot.com/*), that code will only work on your blogs. If someone copies that code and tries to use it on xyz.blogspot.com, it will fail due to the referrer restriction.

    • By prompting the user for their own key, the code itself becomes entirely generic. It doesn't care whose key it is, only that it's a valid Blogger API key.

  • User Control over Restrictions:

    • The user providing the key can then set their own restrictions on that key.

    • If they want it to work only for xyz.blogspot.com/*, they can set that restriction in their Google Cloud Console.

    • If they have multiple blogs (like you do) and want to use the same key for them, they can add all their specific blog domains to their key's referrer restrictions.

    • If they want it to work for any blogspot.com blog (or even no restrictions at all, though less secure), they have that choice and responsibility.

  • Empowerment for Other Developers/Users: This approach empowers other developers or technically savvy users to leverage your code on their own blogs without needing any intervention from you to modify key restrictions. It democratizes the use of the script.

This is a fantastic point, and it genuinely highlights why, for generic, open-source, or utility scripts where security isn't paramount for your assets, prompting for the user's own API key can be a superior design choice. It shifts both the responsibility and the flexibility to the user, making your code more broadly applicable.

Me: Now we can have a generic app - an HTML and JS app - hosted say on GitHub Pages - which can:

* take any Blogger blog ID as an input (it will be nice if it provides a way to extract blog ID from blog domain but that is not necessary),

* take Google API key as an input (unless it had been already provided in the past and is in local storage) with the API key having permission to access the blog ID blog

* And then output list of all posts on that blog.

That will be a nice app and Blogger blogs can link to it if needed. You see the archive widget has some restrictions and does not offer a list of all posts at one time kind of functionality. So this list all posts can be very helpful for searching through titles of a post or simply browsing through them. The user need not be an owner of a blog and have access to its dashboard. All the user will need is a Google API key and know the blog ID of the blog whose posts he wants to list.

Gem: That's a fantastic vision for a generic, publicly hosted Blogger post listing application! You've perfectly encapsulated the value proposition, especially compared to the limitations of the native Blogger archive widget. Such an app would indeed be very useful for users who want a comprehensive, browsable list of posts from any public Blogger blog, without needing to own or access the blog's dashboard.

Your proposed features cover the core needs:

  1. Blogger Blog ID Input: This is essential. As we've discussed, extracting it programmatically from a domain is possible using the Blogger API's blogs.getByUrl method (which takes a blog's URL and returns its details, including the ID). While it adds an extra API call and some complexity, it would significantly enhance user-friendliness by removing the manual "find blog ID" step.

  2. Google API Key Input (with Local Storage): This strikes a good balance for a generic, publicly hosted app. It places the responsibility and flexibility with the user, while making the app reusable across different blogs and users. Your plan to save it to local storage to avoid repeated prompts is also a good UX improvement.

  3. Output: List of All Posts: This is the core functionality, leveraging the robust Blogger API v3.0 pagination we just perfected.

This would indeed be a very helpful tool for the Blogger community!

Let's generate some queries to ensure we have all the latest details on implementing such a generic app. Okay, the search results confirm that the blogs.getByUrl endpoint is indeed available in Blogger API v3.0, and storing/retrieving data from localStorage is standard JavaScript practice.

Here's a conceptual outline for building such a generic Blogger post listing app, which you could host on GitHub Pages:

Generic Blogger Post Lister App Concept

HTML Structure (index.html):

...

JavaScript Logic (app.js):

======================

Gem provided the code for the first version of such an app. I tried that out and we went through a series of interactions which is still ongoing as of late evening 30 Jun 2025. I have put up the code as a public repo: https://github.com/ravisiyer/BloggerAllPostsLister .


Comments