Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,6 @@ temp/

# Heroku-specific files (Backend)
/backend/.heroku/
/backend/Procfile

# ===============================
# Migrations (Backend)
# ===============================
# Django migrations (optional)
/backend/**/migrations/

# ===============================
# Miscellaneous
Expand All @@ -202,9 +195,6 @@ temp/
/frontend/build/
/frontend/public/static/

# Ignore static files generated during build (Backend)
/backend/hoagiemeal/static/

# Ignore mock files (if not to be committed)
/frontend/mock/

Expand Down
15 changes: 13 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
SECRET_KEY="insecure-key-for-dev-only"
DEBUG="True"
ALLOWED_HOSTS="localhost,"

# Set to True to get debug logs (Django, SQL queries, HTTP requests, service logs, etc.) in console
LOGS=

# Set to True to use test database and use Django's debug mode
DEBUG="True"

# Database
DATABASE_URL=""
TEST_DATABASE_URL=""
TEST_DATABASE_URL=""

# Auth0
AUTH0_DOMAIN=
AUTH0_AUDIENCE=
132 changes: 97 additions & 35 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,81 +37,143 @@
# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'hoagiehelp',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"hoagiehelp",
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = 'config.urls'
ROOT_URLCONF = "config.urls"

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]

WSGI_APPLICATION = 'config.wsgi.application'
WSGI_APPLICATION = "config.wsgi.application"


# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases

# Select the appropriate database URL based on DEBUG setting
os.environ["DATABASE_URL"] = os.getenv("TEST_DATABASE_URL") if DEBUG else os.getenv("DATABASE_URL")
DATABASES = {"default": dj_database_url.config(default=os.getenv("DATABASE_URL"), ssl_require=False)}
DATABASES["default"]["CONN_HEALTH_CHECKS"] = True

AUTH_USER_MODEL = "hoagiehelp.CustomUser"

# Auth0 Configuration
AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN")
AUTH0_AUDIENCE = os.getenv("AUTH0_AUDIENCE")
AUTH0_ALGORITHMS = ["RS256"]

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"hoagiehelp.auth.auth.Auth0JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}

# Logging
LOGS = os.getenv("LOGS", "False").lower() == "true"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "{asctime} [{levelname}] {name}: {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
"loggers": {
# Your application
"hoagiehelp": {
"level": "DEBUG" if LOGS else "INFO",
"handlers": ["console"],
"propagate": False,
},
# Django internals
"django": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
# SQL queries
"django.db.backends": {
"level": "DEBUG" if LOGS else "WARNING",
"handlers": ["console"],
"propagate": False,
},
# HTTP requests
"django.request": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
},
}

# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]


# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/

LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"

TIME_ZONE = 'EST'
TIME_ZONE = "EST"

USE_I18N = True

Expand All @@ -121,10 +183,10 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/

STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
Empty file.
103 changes: 103 additions & 0 deletions backend/hoagiehelp/auth/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import logging
from datetime import datetime

import jwt
import requests
from django.conf import settings
from django.core.cache import cache
from django.http import HttpRequest
from jwt.algorithms import RSAAlgorithm
from rest_framework import authentication, exceptions

from hoagiehelp.models import CustomUser

logger = logging.getLogger(__name__)


class Auth0JWTAuthentication(authentication.BaseAuthentication):
def authenticate(self, request: HttpRequest):
auth_header = request.headers.get("Authorization")

if not auth_header:
return None

if not auth_header.startswith("Bearer "):
raise exceptions.AuthenticationFailed("Invalid token header")

token = auth_header.split(" ")[1]
try:
# Verify and decode the token
payload = self.verify_token(token)

# Get or create user based on Auth0 subject
auth0_id = payload["sub"]
net_id = auth0_id.split("|")[2].split("@")[0]
name = payload.get("https://hoagie.io/name", "")
email = payload.get("https://hoagie.io/email", "")

first_name = name.split(" ")[0]
# Handle missing last name
last_name = name.split(" ")[-1] if " " in name else ""

user, _ = CustomUser.objects.get_or_create(
net_id=net_id,
defaults={
"email": email,
"first_name": first_name,
"last_name": last_name,
"net_id": net_id,
"username": net_id,
"class_year": datetime.now().year + 1,
},
)

return (user, payload)

except jwt.ExpiredSignatureError as e:
raise exceptions.AuthenticationFailed("Token has expired") from e
except jwt.InvalidTokenError as e:
raise exceptions.AuthenticationFailed("Invalid token") from e
except Exception as e:
logger.error(f"Authentication error: {str(e)}")
raise exceptions.AuthenticationFailed("Authentication failed") from e

def _fetch_jwks(self):
jwks_url = f"https://{settings.AUTH0_DOMAIN}/.well-known/jwks.json"
jwks = cache.get("auth0_jwks")
if jwks is None:
jwks = requests.get(jwks_url, timeout=5).json()
cache.set("auth0_jwks", jwks, timeout=3600)
return jwks

def _find_rsa_key(self, jwks, kid):
for key in jwks["keys"]:
if key["kid"] == kid:
return RSAAlgorithm.from_jwk(key)
return None

def verify_token(self, token: str):
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header["kid"]

jwks = self._fetch_jwks()
rsa_key = self._find_rsa_key(jwks, kid)

# Key not found so refetch in case Auth0 rotated keys
if rsa_key is None:
cache.delete("auth0_jwks")
jwks = self._fetch_jwks()
rsa_key = self._find_rsa_key(jwks, kid)

if rsa_key is None:
raise exceptions.AuthenticationFailed("Unable to find appropriate key")

# Verify and decode token
payload = jwt.decode(
token,
rsa_key,
algorithms=settings.AUTH0_ALGORITHMS,
audience=settings.AUTH0_AUDIENCE,
issuer=f"https://{settings.AUTH0_DOMAIN}/",
)

return payload
2 changes: 2 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ dependencies = [
"django>=5.1",
"djangorestframework>=3.16.1",
"postgres>=4.0",
"pyjwt[crypto]>=2.11.0",
"python-dotenv>=1.2.1",
"requests>=2.32.5",
]

# ===============================
Expand Down
Loading
Loading