How to Safely Store API Keys in a Frontend Project

Team 7 min read

#frontend

#security

#best-practices

#api-keys

#architecture

Introduction

Frontend applications frequently need to interact with backend services or third-party APIs. It’s common to see developers embed API keys directly in the frontend or in environment files that are bundled into the client. However, anything sent to the browser is potentially accessible by users, attackers, or automated tooling. This guide outlines safe patterns for managing API keys in frontend projects, explains why secrets should live on the server, and provides concrete deployment steps and code examples to minimize risk.

The reality: secrets in the browser aren’t secret

  • The browser is a public surface. Anyone who can load your app can view the JavaScript bundle and extract keys from source, build artifacts, or network traffic.
  • Even attempts to “hide” keys through obfuscation or minification are security-through-obscurity at best.
  • If a key is compromised, the attacker can abuse it until you rotate or revoke it, potentially hitting quotas, incurring costs, or exposing users.

Best practice: assume any key present in the frontend is compromised. Design your system so the frontend never needs to hold or use a secret key directly.

Public keys vs secret keys: what can and cannot be hidden

  • Some services require keys that must be present in the client (for example, map services, analytics, or client-side-only identifiers). These keys are not truly secret; you should protect them by configuring the provider (referrers, HTTP origins, IP restrictions) and limiting scope and permissions.
  • Secret keys or credentials (those capable of performing privileged operations or accessing sensitive data) must never be shipped to the client. These belong on a trusted server that the frontend can call through a controlled surface.

When in doubt, treat all keys in the client as public-facing and implement server-side controls to perform sensitive operations.

Architectures that help store keys safely

  • Backend-for-Frontend (BFF)
    • The frontend talks to a dedicated backend layer you control.
    • The backend holds secrets (in environment variables or a secret store) and performs privileged operations, returning only the results or short-lived tokens to the client.
  • Serverless signing/token endpoint
    • A serverless or small backend function uses secrets to generate ephemeral credentials (short-lived tokens, signed URLs) that the frontend can use for a limited time.
    • No long-term secrets leave the server; the frontend only stores the temporary token.
  • OAuth 2.0 with PKCE for public clients
    • For SPAs that need user-based access, use the PKCE extension to obtain access tokens without exposing a client secret.
    • The frontend never holds sensitive credentials; tokens are scoped and time-limited.
  • API gateways with controlled proxies
    • An API gateway or edge proxy can enforce policies, rotate keys, and apply rate limiting. Secrets remain in the gateway’s secure store while the client uses a lightweight, restricted surface.

In all these patterns, the key idea is to move the secret from the browser into a trusted backend component and to minimize what the frontend can do directly with the secret.

Practical steps to implement safely

  • Inventory and scan for secrets
    • Search your codebase for API keys, secrets, or credentials and remove them from source code and config files that end up in the client bundle.
    • Use secret-scanning tools and enforce checks in CI to prevent secrets from leaking into commits.
  • Separate secrets from the frontend
    • Store all secrets in a backend environment (server, function, or managed secret store like AWS Secrets Manager or GCP Secret Manager).
    • Create a backend API that the frontend can call for privileged operations, instead of calling third-party services directly with secrets.
  • Implement a token-based workflow
    • Use short-lived tokens (e.g., JWTs or opaque tokens) generated by a backend, with strict scope and expiry.
    • The frontend stores only the temporary token and uses it to access the backend-protected API or resource.
  • Use a secure, auditable surface
    • Prefer server-side signing of requests or issuance of signed URLs rather than exposing keys to the client.
    • Log and monitor who accessed what and when; rotate credentials regularly.
  • Protect what you expose to the client
    • If you must expose a public key for a service, configure the provider with strict restrictions (referrers, HTTP origins, app restrictions) and minimize allowed actions.
  • Handle token storage securely on the client
    • If you must store tokens in the client, prefer in-memory storage for the shortest viable time and reduce exposure; if feasible, use httpOnly cookies served by the backend to mitigate XSS risks.
  • Plan for rotation and revocation
    • Implement automated rotation of credentials and a clear revocation path. Ensure clients can gracefully handle key rotation without downtime.

Example architectures in practice

  • BFF example
    • Frontend calls /api/data on the BFF.
    • BFF authenticates the user, uses its own secret to fetch data from a protected service, and returns a safe, role-limited response to the frontend.
  • Ephemeral token signer
    • Frontend calls /api/get-token with user context.
    • Backend uses a secret to generate a time-limited token (or a signed URL) and returns it.
    • Frontend calls the target API with the ephemeral token or signed URL.
  • Public-key usage with restrictions
    • A frontend-only API key is loaded for a provider like a maps service.
    • The key is restricted by origin and usage scope. No privileged actions are allowed directly via the key.

Example: a small BFF signer in Node (conceptual)

This example demonstrates a minimal backend endpoint that signs a short-lived token using a secret. The frontend asks for a token and then uses it to call a protected API.

Code (server side)

// node example using express and jsonwebtoken
const express = require('express');
const jwt = require('jsonwebtoken');
require('dotenv').config();

const app = express();
app.use(express.json());

const SECRET = process.env.SIGNING_SECRET; // stored securely in env or secret manager
const TOKEN_TTL = '15m';

app.post('/api/get-token', (req, res) => {
  // In a real app, authenticate the user/session here
  const { userId, scope } = req.body;

  if (!userId) {
    return res.status(400).json({ error: 'Missing userId' });
  }

  const payload = { sub: userId, scope };
  const token = jwt.sign(payload, SECRET, { expiresIn: TOKEN_TTL });
  res.json({ token });
});

// Protected endpoint that consumes the ephemeral token
app.get('/api/protected-data', (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) return res.status(401).json({ error: 'Missing token' });

  const token = authHeader.split(' ')[1];
  try {
    const payload = jwt.verify(token, SECRET);
    // Validate scope, permissions, etc.
    res.json({ data: 'Secret data for ' + payload.sub });
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
});

app.listen(3000, () => console.log('BFF signer running on port 3000'));

Code (client side)

// Frontend flow to obtain and use the ephemeral token
async function fetchEphemeralToken(userId) {
  const resp = await fetch('/api/get-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId, scope: 'read:data' }),
  });
  if (!resp.ok) throw new Error('Failed to obtain token');
  const { token } = await resp.json();
  return token;
}

async function fetchProtectedData(userId) {
  const token = await fetchEphemeralToken(userId);
  const resp = await fetch('/api/protected-data', {
    headers: { Authorization: `Bearer ${token}` },
  });
  const data = await resp.json();
  return data;
}

// Example usage
fetchProtectedData('user-123')
  .then(console.log)
  .catch(console.error);

Note: This is a simplified illustration. In production, you would authenticate the user securely, handle token storage safely, and implement proper error handling, TLS enforcement, and session management.

Testing and maintenance

  • Regularly audit code for secrets
    • Use automated secret scanners in CI/CD to prevent accidental leakage.
  • Validate access patterns
    • Monitor which operations are performed with ephemeral tokens and enforce least privilege.
  • Rotate and revoke
    • Implement a rotation policy for signing secrets and ensure clients can handle rotation with minimal disruption.
  • Update provider configurations
    • Review API provider restrictions (referrers, IPs, scopes) and adjust as your deployment evolves.
  • Educate the team
    • Share best practices for not embedding secrets in frontend code and for designing backend-first access patterns.

Conclusion

Secrets belong where you can control access and monitor usage: on the server or in a trusted backend layer. By shifting privileged operations off the client, using ephemeral tokens, and leveraging architecture patterns like BFF or serverless signers, you can significantly reduce the risk of API key leakage in frontend projects. Treat any key visible to the user as potentially compromised, apply strict access controls, and design your frontend to rely on secure backend surfaces for privileged actions.