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):
pip install django django-environ cryptography
django-admin startproject core .
python manage.py startapp sim
# settings.py
INSTALLED_APPS = [
# ...
'sim',
# ...
]
import environ
env = environ.Env()
environ.Env.read_env()
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.
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())
python manage.py makemigrations
python manage.py migrate
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)
We'll create a simple frontend to allow the user to create, read, and delete his API keys:
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
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)
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'),
]
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
sim/templates
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>
python manage.py createsuperuser
python manage.py runserver
Login in using the admin console at /admin
Visit your page at /manage-api-keys
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.
I'm building Photon Designer - an entirely visual editor for building Django frontend at the speed that light hits your eyes 💫