/stripe-guides

How to restrict Stripe API keys on frontend?

Learn how to securely restrict Stripe API keys on the frontend by keeping secret keys server-side, using restricted publishable keys, and implementing best security practices.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to restrict Stripe API keys on frontend?

How to Restrict Stripe API Keys on Frontend

 

Introduction

 

Exposing Stripe API keys on the frontend is a significant security risk that can lead to unauthorized access, fraudulent transactions, and potential financial loss. This tutorial provides comprehensive steps to properly secure your Stripe implementation by keeping sensitive API keys off the client-side and implementing a secure server-side approach.

 

Step 1: Understanding the Security Risk

 

First, let's understand why exposing Stripe API keys in frontend code is dangerous:

  • Publishable keys can be safely used in the frontend, but secret keys must never be exposed
  • Even publishable keys should have proper restrictions
  • Anyone with access to your secret key can make charges, refunds, and access customer data
  • Browser code is visible to all users through browser developer tools

 

Step 2: Set Up a Backend API Server

 

The most secure approach is to handle all Stripe operations on your server. Create a backend service using Node.js, Python, Ruby, or any other server-side language.

Example using Node.js and Express:


// server.js
const express = require('express');
const stripe = require('stripe')('sk_test_YourSecretKeyHere');
const cors = require('cors');
const app = express();

// Middleware
app.use(express.json());
app.use(cors({
  origin: 'https://your-trusted-frontend-domain.com' // Restrict to your frontend domain
}));

// Secure payment endpoint
app.post('/api/create-payment-intent', async (req, res) => {
  try {
    const { amount, currency, customer } = req.body;
    
    // Validate inputs here
    if (!amount || amount <= 0) {
      return res.status(400).json({ error: 'Invalid amount' });
    }

    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency,
      customer,
      // Additional options as needed
    });

    res.json({ clientSecret: paymentIntent.client\_secret });
  } catch (error) {
    console.error('Payment intent error:', error);
    res.status(500).json({ error: error.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

 

Step 3: Secure Your Stripe API Keys

 

Never hardcode your Stripe secret key in your code. Use environment variables instead:


// Using environment variables
require('dotenv').config(); // Make sure to install dotenv package
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

Create a .env file (not checked into version control):


# .env file
STRIPE_SECRET_KEY=sk_test_YourSecretKeyHere
STRIPE_PUBLISHABLE_KEY=pk_test_YourPublishableKeyHere

Add .env to your .gitignore file:


# .gitignore
.env
node\_modules/

 

Step 4: Implement Frontend Code

 

In your frontend code, only use the Stripe publishable key and communicate with your backend:


// Frontend JavaScript (React example)
import React, { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { CardElement, Elements, useStripe, useElements } from '@stripe/react-stripe-js';

// Initialize Stripe with publishable key only
const stripePromise = loadStripe('pk_test_YourPublishableKeyHere');

const CheckoutForm = () => {
  const stripe = useStripe();
  const elements = useElements();
  const [clientSecret, setClientSecret] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Call your backend to create a PaymentIntent and get the clientSecret
    fetch('https://your-backend-api.com/api/create-payment-intent', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        amount: 1000, // $10.00
        currency: 'usd',
      }),
    })
      .then((res) => res.json())
      .then((data) => {
        setClientSecret(data.clientSecret);
      })
      .catch((err) => {
        setError('Error preparing payment. Please try again.');
        console.error(err);
      });
  }, []);

  const handleSubmit = async (event) => {
    event.preventDefault();
    setLoading(true);

    if (!stripe || !elements) {
      return;
    }

    const cardElement = elements.getElement(CardElement);

    const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
      payment\_method: {
        card: cardElement,
        billing\_details: {
          name: 'Customer Name',
        },
      },
    });

    setLoading(false);

    if (error) {
      setError(error.message);
    } else if (paymentIntent.status === 'succeeded') {
      // Payment successful - update UI or redirect
      console.log('Payment succeeded!');
    }
  };

  return (
    
{error &&
{error}
} ); }; const StripeCheckout = () => ( ); export default StripeCheckout;

 

Step 5: Implement Proper CORS Settings

 

Configure Cross-Origin Resource Sharing (CORS) on your backend to only allow requests from your trusted domains:


// Detailed CORS configuration for Express
const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://your-production-domain.com', 
      'https://www.your-production-domain.com'
    ];
    
    // During development, you might want to allow localhost
    if (process.env.NODE\_ENV === 'development') {
      allowedOrigins.push('http://localhost:3000');
    }
    
    if (!origin || allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // 24 hours
};

app.use(cors(corsOptions));

 

Step 6: Restrict Stripe Publishable Key Usage

 

Even though the publishable key is designed to be public, you can restrict its capabilities in the Stripe Dashboard:

  • Log in to your Stripe Dashboard
  • Go to Developers → API keys
  • Next to your publishable key, click "Manage restricted keys"
  • Create a new restricted key with only the permissions needed for your frontend
  • Typically, you only need to enable "Create payment methods" and "Read payment methods"
  • You can also restrict the key to specific URLs

 

Step 7: Use Stripe Elements for Additional Security

 

Stripe Elements handles sensitive card data on Stripe's servers rather than yours:


// Include Stripe.js before using Stripe Elements


// Initialize Elements
const stripe = Stripe('pk_test_YourRestrictedPublishableKey');
const elements = stripe.elements();

// Create card element
const cardElement = elements.create('card', {
  style: {
    base: {
      fontSize: '16px',
      color: '#32325d',
    }
  }
});

// Mount the card element to the DOM
cardElement.mount('#card-element');

// Handle form submission
document.getElementById('payment-form').addEventListener('submit', async (event) => {
  event.preventDefault();
  const { paymentMethod, error } = await stripe.createPaymentMethod({
    type: 'card',
    card: cardElement,
  });

  if (error) {
    // Handle error
    console.error(error);
  } else {
    // Send paymentMethod.id to your server
    const response = await fetch('/api/process-payment', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        payment_method_id: paymentMethod.id,
        amount: 1000, // $10.00
      }),
    });
    
    const result = await response.json();
    
    // Handle server response
    if (result.success) {
      // Payment succeeded
    } else {
      // Payment failed
    }
  }
});

 

Step 8: Implement Strong Authentication

 

Secure your API endpoints with proper authentication to ensure only authorized users can initiate payment operations:


// Authentication middleware (example using JWT)
const jwt = require('jsonwebtoken');

const authenticateUser = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT\_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

// Apply middleware to payment routes
app.post('/api/create-payment-intent', authenticateUser, async (req, res) => {
  // Now you have access to the authenticated user via req.user
  // You can use this to ensure the payment is for the correct user
  
  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: req.body.amount,
      currency: req.body.currency,
      customer: req.user.stripeCustomerId, // Use the authenticated user's ID
      // Additional options
    });
    
    res.json({ clientSecret: paymentIntent.client\_secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

 

Step 9: Set Up Webhook Handling

 

Implement Stripe webhooks to handle asynchronous payment events securely:


// Webhook handling endpoint
app.post('/stripe-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.error(`Webhook error: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle the event
  switch (event.type) {
    case 'payment\_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log(`PaymentIntent for ${paymentIntent.amount} was successful!`);
      // Update your database, fulfill orders, etc.
      break;
    case 'payment_intent.payment_failed':
      const failedPayment = event.data.object;
      console.log(`Payment failed: ${failedPayment.last_payment_error?.message}`);
      // Notify customer of failed payment
      break;
    default:
      // Unexpected event type
      console.log(`Unhandled event type ${event.type}`);
  }
  
  // Return a 200 response to acknowledge receipt of the event
  res.json({ received: true });
});

 

Step 10: Implement Additional Security Measures

 

  • Use HTTPS for all communications between frontend and backend
  • Implement rate limiting to prevent abuse
  • Add input validation on both client and server side
  • Use Stripe's built-in fraud prevention tools
  • Monitor your Stripe dashboard for suspicious activities
  • Keep all libraries and dependencies up to date

Example rate limiting implementation:


const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 _ 60 _ 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
  message: 'Too many requests from this IP, please try again after 15 minutes'
});

// Apply rate limiting to all payment-related routes
app.use('/api/', apiLimiter);

 

Conclusion

 

By following these steps, you've created a secure implementation for Stripe payments that keeps sensitive API keys off the frontend. Remember that security is an ongoing process, and you should regularly review and update your implementation to address new security challenges and comply with Stripe's best practices.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022