Wire JWT lifetimes to settings, read secret key from env

This commit is contained in:
Madelen Trygstad Lyngvær committed 2026-09-24 09:07:03 +02:00
1 parent b95303caa9
commit 6f68f71ce3
3 files changed
+46 -21

No files matched your search

+13 -14
View File
@@ -1,5 +1,5 @@
from decimal import Decimal from decimal import Decimal
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, timezone, date
from typing import List from typing import List
import jwt import jwt
from django.conf import settings from django.conf import settings
@@ -22,31 +22,30 @@ class JWTAuth(HttpBearer):
return None return None
user = User.objects.get(id=payload["user_id"]) user = User.objects.get(id=payload["user_id"])
return user return user
except (jwt.PyJWTError, User.DoesNotExist): except (jwt.PyJWTError, User.DoesNotExist, KeyError):
return None return None
auth = JWTAuth() auth = JWTAuth()
def generate_access_token(user: User) -> str: def _generate_token(user: User, token_type: str, lifetime: timedelta) -> str:
now = datetime.now(timezone.utc)
payload = { payload = {
"user_id": user.id, "user_id": user.id,
"type": "access", "type": token_type,
"exp": datetime.utcnow() + timedelta(minutes=30), "exp": now + lifetime,
"iat": datetime.utcnow(), "iat": now,
} }
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
def generate_access_token(user: User) -> str:
return _generate_token(user, "access", settings.JWT_ACCESS_TOKEN_LIFETIME)
def generate_refresh_token(user: User) -> str: def generate_refresh_token(user: User) -> str:
payload = { return _generate_token(user, "refresh", settings.JWT_REFRESH_TOKEN_LIFETIME)
"user_id": user.id,
"type": "refresh",
"exp": datetime.utcnow() + timedelta(days=14),
"iat": datetime.utcnow(),
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
# Schemas # Schemas
@@ -190,7 +189,7 @@ def refresh_token_view(request, payload: RefreshIn):
user = User.objects.get(id=data["user_id"]) user = User.objects.get(id=data["user_id"])
new_access_token = generate_access_token(user) new_access_token = generate_access_token(user)
return {"access_token": new_access_token} return {"access_token": new_access_token}
except (jwt.PyJWTError, User.DoesNotExist): except (jwt.PyJWTError, User.DoesNotExist, KeyError):
raise HttpError(401, "Invalid or expired refresh token") raise HttpError(401, "Invalid or expired refresh token")
+29 -7
View File
@@ -17,17 +17,43 @@ import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = BASE_DIR / 'data'
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
def _read_secret_key() -> str:
"""Prefer DJANGO_SECRET_KEY, else a key persisted in the (gitignored) data dir."""
env_key = os.getenv('DJANGO_SECRET_KEY', '').strip()
if env_key:
return env_key
key_file = DATA_DIR / 'secret_key.txt'
if key_file.exists():
stored = key_file.read_text().strip()
if stored:
return stored
from django.core.management.utils import get_random_secret_key
generated = get_random_secret_key()
key_file.parent.mkdir(parents=True, exist_ok=True)
key_file.write_text(generated)
key_file.chmod(0o600)
return generated
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-m#==%hz74d192o)4!#_!dpz%$yox@!hkabc9)pggkkiuvc7((q' # This also signs every JWT, so leaking it means anyone can forge a token.
SECRET_KEY = _read_secret_key()
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
ALLOWED_HOSTS = ['*'] # Lifetimes for the hand-rolled JWTs issued in core/api.py.
JWT_ACCESS_TOKEN_LIFETIME = timedelta(days=int(os.getenv('JWT_ACCESS_DAYS', '14')))
JWT_REFRESH_TOKEN_LIFETIME = timedelta(days=int(os.getenv('JWT_REFRESH_DAYS', '30')))
ALLOWED_HOSTS = os.getenv( ALLOWED_HOSTS = os.getenv(
"ALLOWED_HOSTS", "ALLOWED_HOSTS",
@@ -107,7 +133,7 @@ WSGI_APPLICATION = 'core.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'data' / 'db.sqlite3', 'NAME': DATA_DIR / 'db.sqlite3',
} }
} }
@@ -168,7 +194,3 @@ SESSION_ENGINE = "django.contrib.sessions.backends.db"
SESSION_COOKIE_AGE = 1209600 # Persist session for 2 weeks (in seconds) SESSION_COOKIE_AGE = 1209600 # Persist session for 2 weeks (in seconds)
SESSION_SAVE_EVERY_REQUEST = True SESSION_SAVE_EVERY_REQUEST = True
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
NINJA_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=14),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}
+4
View File
@@ -8,4 +8,8 @@ services:
- "8000:8000" - "8000:8000"
volumes: volumes:
- ./data:/app/data - ./data:/app/data
environment:
JWT_ACCESS_DAYS: "14"
JWT_REFRESH_DAYS: "30"
# DJANGO_SECRET_KEY: "..." # else auto-generated into ./data/secret_key.txt
restart: unless-stopped restart: unless-stopped