Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2614df4
Enhance Docker and JWT handling: update Dockerfile to install OpenSSL…
Apr 7, 2025
c8bc60e
Implement JWT key rotation: add endpoint to rotate keys, update key m…
Apr 8, 2025
968da27
Refactor JWT key handling: remove hardcoded key loading, utilize dyna…
Apr 8, 2025
01eff90
Add timestamp logging for key creation in JWT rotation
Apr 8, 2025
702725a
Change route to be more RESTful
Apr 8, 2025
f2834ee
Remove JWT_SECRET_KEY from config and add script for cleaning up expi…
Apr 8, 2025
6bc3104
Refactor JWT tests to use public key for encoding/decoding and enhanc…
Apr 8, 2025
ffb491a
Remove redundant sample_person_id fixture from test_refresh.py
Apr 8, 2025
ab6d222
Merge branch 'main' into security/use_rsa_for_jwt
Apr 10, 2025
ae6c76a
Refactor key cleanup script to use dynamic expiry days from JWT confi…
Apr 10, 2025
5c0e461
Refactor JWT handling by moving related functions and classes to a ne…
Apr 11, 2025
7b392a5
Refactor JWT key handling by consolidating functions into common modu…
Apr 11, 2025
21602d3
Refactor JWT test suite by removing obsolete test files and consolida…
Apr 11, 2025
b1f94b3
Clarify entrypoint script comment to specify starting the API
Apr 11, 2025
e4078fd
Optimize payload generation
Vianpyro Apr 13, 2025
bc7d9d0
Merge branch 'main' into security/use_rsa_for_jwt
Apr 13, 2025
68a2081
Add key management functionality and refactor key paths
Apr 13, 2025
8b05ed4
Refactor Dockerfile to remove entrypoint script and directly run key …
Apr 13, 2025
9d4a087
Add OpenSSL installation steps for Linux, macOS, and Windows in Pytes…
Apr 13, 2025
3ad2c9c
Fix variable name inconsistency for key directory in jwtoken and keys…
Apr 13, 2025
2eb3e51
Refactor Dockerfile to use entrypoint script for key rotation and app…
Apr 13, 2025
926a4cc
Reorganize Dockerfile to copy entrypoint script before exposing Flask…
Apr 13, 2025
8c15694
Remove entrypoint script copy command from Dockerfile; streamline app…
Apr 13, 2025
7ebd0e5
Update ENTRYPOINT in Dockerfile to use entrypoint.sh for improved pro…
Apr 13, 2025
89ba7cd
Update ENTRYPOINT in Dockerfile to use entrypoint.sh for improved pro…
Apr 13, 2025
7e9fc5f
Refactor app.py to use ACTIVE_KID_FILE constant for key existence che…
Apr 13, 2025
cd26564
Remove OpenSSL installation step for macOS in Pytest CI workflow; cla…
Apr 13, 2025
5c18cc5
Refactor datetime usage in token generation and logging setup; improv…
Apr 13, 2025
c28cf21
Remove entrypoint.sh and update ENTRYPOINT in Dockerfile to directly …
Apr 13, 2025
b9f9624
Add curl to fix healthcheck and remove cached libraries to save up space
Apr 13, 2025
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
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM alpine:latest

# Install common tools
RUN apk add --no-cache bash git \
python3 py3-pip
python3 py3-pip openssl

# Setup default user
ARG USERNAME=vscode
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: "pip"

- name: Install OpenSSL (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libssl-dev

# MacOS does not require OpenSSL installation as it is pre-installed

- name: Install OpenSSL (Windows)
if: runner.os == 'Windows'
run: choco install openssl.light --no-progress

- name: Install dependencies
run: |
pip install -r requirements.txt
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
logs/
*.log

# Ignore all pem files
# Ignore JWT keys
keys/
*.pem

# Upload folder
Expand Down
15 changes: 9 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ FROM python:3.13-slim
# Set a specific working directory in the container
WORKDIR /app

# Install dependencies separately for better caching
# Install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends curl openssl \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

# Install required Python packages
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application files
COPY . .

# Expose the Flask port
EXPOSE 5000

# Create a non-root user and set permissions for the /app directory
RUN adduser --disabled-password --gecos '' apiuser && chown -R apiuser /app
USER apiuser

# Expose the Flask port
EXPOSE 5000

# Add health check for the container
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl --fail http://localhost:5000/ || exit 1

# Command to run the app
CMD ["python", "app.py"]
ENTRYPOINT ["python3", "app.py"]
7 changes: 7 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import argparse
import os
import traceback

from flask import Flask, jsonify, request
from flask_cors import CORS
from werkzeug.exceptions import HTTPException

from config.jwtoken import ACTIVE_KID_FILE
from config.logging import setup_logging
from config.ratelimit import limiter
from config.settings import Config
from routes import register_routes
from utility.database import extract_error_message
from utility.jwtoken.keys_rotation import rotate_keys

app = Flask(__name__)
app.config.from_object(Config)
Expand All @@ -26,6 +29,10 @@
if app.config["TESTING"]:
limiter.enabled = False

# Ensure the keys directory and active_kid.txt file exist
if not os.path.exists(ACTIVE_KID_FILE):
rotate_keys()


@app.route("/")
def home():
Expand Down
8 changes: 8 additions & 0 deletions config/jwtoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from pathlib import Path

KEYS_DIR = Path("keys")

ACTIVE_KID_FILE = KEYS_DIR / "active_kid.txt"
CREATED_AT_FILE = "created_at.txt"
PRIVATE_KEY_FILE = "private.pem"
PUBLIC_KEY_FILE = "public.pem"
4 changes: 2 additions & 2 deletions config/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ def setup_logging():
logger.setLevel(logging.DEBUG)

# Create a file handler that rotates logs daily
timestamp = datetime.datetime.now().strftime("%Y-%m-%d")
current_time = datetime.datetime.now().strftime("%Y-%m-%d")

# Create a directory for logs if it doesn't exist
if not os.path.exists(LOGS_DIRECTORY):
os.makedirs(LOGS_DIRECTORY)
log_filename = f"{LOGS_DIRECTORY}/{timestamp}.log"
log_filename = f"{LOGS_DIRECTORY}/{current_time}.log"

# Set up timed rotating file handler
file_handler = TimedRotatingFileHandler(
Expand Down
1 change: 0 additions & 1 deletion config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class Config:
MYSQL_CURSORCLASS = "DictCursor"

# JWT configuration
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1)
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30)

Expand Down
78 changes: 0 additions & 78 deletions jwt_helper.py

This file was deleted.

File renamed without changes.
24 changes: 24 additions & 0 deletions jwtoken/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from functools import wraps

from flask import jsonify, request

from utility.jwtoken.common import extract_token_from_header

from .exceptions import TokenError
from .tokens import verify_token


def token_required(f):
"""Decorator to protect routes by requiring a valid token."""

@wraps(f)
def decorated(*args, **kwargs):
try:
token = extract_token_from_header()
decoded = verify_token(token, required_type="access")
request.person_id = decoded["person_id"]
return f(*args, **kwargs)
except TokenError as e:
return jsonify(message=e.message), e.status_code

return decorated
7 changes: 7 additions & 0 deletions jwtoken/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class TokenError(Exception):
"""Custom exception for token-related errors."""

def __init__(self, message, status_code):
super().__init__(message)
self.status_code = status_code
self.message = message
63 changes: 63 additions & 0 deletions jwtoken/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from datetime import datetime, timedelta, timezone

import jwt

from utility.jwtoken.common import get_active_kid, load_private_key, load_public_key

from .exceptions import TokenError

JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1)
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30)


def generate_access_token(person_id: int) -> str:
kid = get_active_kid()
private_key = load_private_key(kid)

current_time = datetime.now(timezone.utc)
payload = {
"person_id": person_id,
"exp": current_time + JWT_ACCESS_TOKEN_EXPIRY, # Expiration
"iat": current_time, # Issued at
"token_type": "access",
}
headers = {"kid": kid}
return jwt.encode(payload, private_key, algorithm="RS256", headers=headers)


def generate_refresh_token(person_id: int) -> str:
"""Generate a long-lived refresh token for a user."""
kid = get_active_kid()
private_key = load_private_key(kid)

current_time = datetime.now(timezone.utc)
payload = {
"person_id": person_id,
"exp": current_time + JWT_REFRESH_TOKEN_EXPIRY, # Expiration
"iat": current_time, # Issued at
"token_type": "refresh",
}
headers = {"kid": kid}

return jwt.encode(payload, private_key, algorithm="RS256", headers=headers)


def verify_token(token: str, required_type: str) -> dict:
"""Verify and decode a JWT token."""
try:
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header.get("kid")
if not kid:
raise TokenError("KID missing in token header", 401)

public_key = load_public_key(kid)

decoded = jwt.decode(token, public_key, algorithms=["RS256"])
if decoded.get("token_type") != required_type:
raise jwt.InvalidTokenError("Invalid token type")
return decoded

except jwt.ExpiredSignatureError:
raise TokenError("Token has expired", 401)
except jwt.InvalidTokenError:
raise TokenError("Invalid token", 401)
10 changes: 3 additions & 7 deletions routes/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
from pymysql import MySQLError

from config.ratelimit import limiter
from jwt_helper import (
TokenError,
extract_token_from_header,
generate_access_token,
generate_refresh_token,
verify_token,
)
from jwtoken.exceptions import TokenError
from jwtoken.tokens import generate_access_token, generate_refresh_token, verify_token
from utility.database import database_cursor
from utility.encryption import encrypt_email, hash_email, hash_password, verify_password
from utility.jwtoken.common import extract_token_from_header
from utility.validation import validate_email, validate_password

authentication_blueprint = Blueprint("authentication", __name__)
Expand Down
2 changes: 1 addition & 1 deletion routes/picture.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from flask import Blueprint, jsonify, request, send_from_directory

from config.settings import Config
from jwt_helper import token_required
from jwtoken.decorators import token_required
from utility.database import database_cursor

picture_blueprint = Blueprint("picture", __name__)
Expand Down
54 changes: 0 additions & 54 deletions tests/test_jwt/test_verify_token.py

This file was deleted.

Empty file added tests/test_jwtoken/__init__.py
Empty file.
Loading