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.
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
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 💫