How to store your users' API keys securely in Django 🔑

When building your bespoke AI-powered cat joke generator (CatGPT 🐈), you'll want to store your users' external API keys, e.g., their OpenAI keys.

How should you do this in a secure way?

Here are 5 short steps to build an app that encrypts your users' API keys in Django, including encrypting them in the database.

Your furst employee at CatGPT

And here's a video walkthrough (featuring me):

1. Setup

  • Install our packages and create a new app called sim
pip install django django-environ cryptography

django-admin startproject core .
python manage.py startapp sim
  • Add our new app to the INSTALLED_APPS in core/settings.py:
# settings.py
INSTALLED_APPS = [
    # ...
    'sim',
    # ...
]
  • Add this to the top of core/settings.py to load our environment variables:
import environ

env = environ.Env()
environ.Env.read_env()
  • Add your secret encryption key:

a) create the file core/.env

b) In your python console python manage.py shell, run: from cryptography.fernet import Fernet; print(Fernet.generate_key()) to generate a key

c) Add the below to your .env file (no speech marks needed):

ENCRYPTION_KEY=<your_value>

Note: if you are uploading your code to GitHub (or anywhere else), don't share your .env file containing your encryption key. If using Git, the simplest way to avoid sharing this file is to include a reference to .env in your .gitignore file. In your production server, you would then add your environment variables directly into your server.

2. Create a model to encrypt each user's API Keys

  • Add this into your sim/models.py:
from django.db import models
from django.contrib.auth.models import User
from cryptography.fernet import Fernet
import os


class ApiKey(models.Model):
    user = models.ForeignKey(User, related_name='api_keys', on_delete=models.CASCADE)
    name = models.CharField(unique=True, max_length=255, null=True, blank=True)
    encrypted_api_key = models.BinaryField(null=True, blank=True)

    @property
    def key(self) -> str:
        cipher_suite = Fernet(os.environ['ENCRYPTION_KEY'])
        return cipher_suite.decrypt(self.encrypted_api_key).decode() if self.encrypted_api_key else ""

    @key.setter
    def key(self, value) -> None:
        cipher_suite = Fernet(os.environ['ENCRYPTION_KEY'])
        self.encrypted_api_key = cipher_suite.encrypt(value.encode())
  • Run your migrations:
python manage.py makemigrations
python manage.py migrate
  • Register your model by adding this to sim/admin.py:
from django.contrib import admin
from .models import ApiKey 


class ApiKeyAdmin(admin.ModelAdmin):
    list_display = ('user', 'encrypted_api_key')


admin.site.register(ApiKey, ApiKeyAdmin)

3. Create a frontend

We'll create a simple frontend to allow the user to create, read, and delete his API keys:

  • Create a form (sim/forms.py):
from django import forms
from .models import ApiKey
import os
from cryptography.fernet import Fernet


class ApiKeyForm(forms.ModelForm):
    name = forms.CharField(max_length=255, required=True)
    value = forms.CharField(max_length=255, required=True)

    class Meta:
        model = ApiKey
        fields = []

    def save(self, commit=True):
        instance = super(ApiKeyForm, self).save(commit=False)
        instance.name = self.cleaned_data.get('name')
        key = self.cleaned_data.get('value')
        cipher_suite = Fernet(os.environ['ENCRYPTION_KEY'])
        instance.encrypted_api_key = cipher_suite.encrypt(key.encode())


        if commit:
            instance.save()
        return instance
  • Create your views (sim/views.py):
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect
from .models import ApiKey
from .forms import ApiKeyForm


def manage_api_keys(request: HttpRequest) -> HttpResponse:
    """
    Handle the display, creation, and deletion of API keys for the logged-in user.
    """
    if request.method == 'POST':
        form = ApiKeyForm(request.POST)
        if form.is_valid():
            new_api_key = form.save(commit=False)
            new_api_key.user = request.user
            new_api_key.save()
        return redirect('manage_api_keys')

    api_keys = ApiKey.objects.filter(user=request.user)
    form = ApiKeyForm()
    return render(request, 'manage_api_keys.html', {'api_keys': api_keys, 'form': form})


def api_key_delete(request: HttpRequest, api_key_id: str) -> HttpResponse:
    """
    Delete an API key by its ID.
    """
    api_key = ApiKey.objects.get(id=api_key_id, user=request.user)
    api_key.delete()
    return HttpResponse(status=204)
  • Create and add the below to sim/urls.py:
from django.urls import path
from . import views

urlpatterns = [
    path('manage-api-keys/', views.manage_api_keys, name='manage_api_keys'),
    path('api_keys/delete/<int:api_key_id>/', views.api_key_delete, name='api_key_delete'),
]
  • Update core/urls.py:
from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sim.urls')),
]

4. Add your templates

  • Create the folder sim/templates
  • Add the below to a new file at sim/templates/manage_api_keys.html
<!DOCTYPE html>
<html>
<head>
    <title>Manage API Keys</title>
    <script src="https://unpkg.com/htmx.org@1.6.1"></script>
</head>
<body>
<h1>Your API Keys</h1>

<form hx-post="{% url 'manage_api_keys' %}" hx-swap="outerHTML" hx-target="body">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Create</button>
</form>

<div>
    {% for api_key in api_keys %}
    <div style="display: flex; align-items: center;">
        <p>Name:</p><input disabled value="{{ api_key.name }}"/>
        <p>Key:</p><input disabled value="{{ api_key.key }}"/>
        <a href="{% url 'api_key_delete' api_key.id %}" hx-swap="outerHTML">Delete</a>
    </div>
    {% endfor %}
</div>
</body>
</html>

5. Save your API keys securely

  • Create a superuser
python manage.py createsuperuser
  • Run your server and visit the url to see your API keys
python manage.py runserver
  • Login in using the admin console at /admin

  • Visit your page at /manage-api-keys

The simple frontend that allows your user to add their external API keys

Finished 🎉

Congrats - you've now added a basic level of security when storing your users' keys.
This security is based on keeping your ENCRYPTION_KEY secure. It would be good practice to change this key regularly.

Alternatively, you could update your models to offload encryption to a third party, such as AWS KMS (Key Management System). We'd need just a few mores lines to add KMS to our existing models and forms.

P.S - Photon Designer

I'm building Photon Designer - an entirely visual editor for building Django frontend at the speed that light hits your eyes 💫

Let's get visual.

Do you want to create beautiful django frontends effortlessly?
Click below to book your spot on our early access mailing list (as well as early adopter prices).