From 6f68f71ce37b666e1b1c2b278a9557ec4709fb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madelen=20Trygstad=20Lyngv=C3=A6r?= Date: Thu, 24 Sep 2026 09:07:03 +0200 Subject: [PATCH] Wire JWT lifetimes to settings, read secret key from env --- core/api.py | 27 +++++++++++++-------------- core/settings.py | 36 +++++++++++++++++++++++++++++------- docker-compose.yml | 4 ++++ 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/core/api.py b/core/api.py index 88855a6..d37d377 100644 --- a/core/api.py +++ b/core/api.py @@ -1,5 +1,5 @@ from decimal import Decimal -from datetime import datetime, timedelta, date +from datetime import datetime, timedelta, timezone, date from typing import List import jwt from django.conf import settings @@ -22,31 +22,30 @@ class JWTAuth(HttpBearer): return None user = User.objects.get(id=payload["user_id"]) return user - except (jwt.PyJWTError, User.DoesNotExist): + except (jwt.PyJWTError, User.DoesNotExist, KeyError): return None 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 = { "user_id": user.id, - "type": "access", - "exp": datetime.utcnow() + timedelta(minutes=30), - "iat": datetime.utcnow(), + "type": token_type, + "exp": now + lifetime, + "iat": now, } 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: - payload = { - "user_id": user.id, - "type": "refresh", - "exp": datetime.utcnow() + timedelta(days=14), - "iat": datetime.utcnow(), - } - return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + return _generate_token(user, "refresh", settings.JWT_REFRESH_TOKEN_LIFETIME) # Schemas @@ -190,7 +189,7 @@ def refresh_token_view(request, payload: RefreshIn): user = User.objects.get(id=data["user_id"]) new_access_token = generate_access_token(user) 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") diff --git a/core/settings.py b/core/settings.py index bf5929a..56a9317 100644 --- a/core/settings.py +++ b/core/settings.py @@ -17,17 +17,43 @@ import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = BASE_DIR / 'data' + # Quick-start development settings - unsuitable for production # 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! -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! 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", @@ -107,7 +133,7 @@ WSGI_APPLICATION = 'core.wsgi.application' DATABASES = { 'default': { '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_SAVE_EVERY_REQUEST = True SESSION_COOKIE_HTTPONLY = True -NINJA_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=14), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), -} diff --git a/docker-compose.yml b/docker-compose.yml index b93a5ea..41ecaf7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,8 @@ services: - "8000:8000" volumes: - ./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