Skip to content

Commit 699db82

Browse files
authored
Revert "Change JWT encryption from HMAC to RSA with rotating keys (#37)"
This reverts commit 5079f28.
1 parent 5079f28 commit 699db82

30 files changed

+181
-499
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM alpine:latest
22

33
# Install common tools
44
RUN apk add --no-cache bash git \
5-
python3 py3-pip openssl
5+
python3 py3-pip
66

77
# Setup default user
88
ARG USERNAME=vscode

.github/workflows/pytest.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,6 @@ jobs:
3333
python-version: ${{ matrix.python-version }}
3434
cache: "pip"
3535

36-
- name: Install OpenSSL (Linux)
37-
if: runner.os == 'Linux'
38-
run: sudo apt-get update && sudo apt-get install -y libssl-dev
39-
40-
# MacOS does not require OpenSSL installation as it is pre-installed
41-
42-
- name: Install OpenSSL (Windows)
43-
if: runner.os == 'Windows'
44-
run: choco install openssl.light --no-progress
45-
4636
- name: Install dependencies
4737
run: |
4838
pip install -r requirements.txt

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
logs/
33
*.log
44

5-
# Ignore JWT keys
6-
keys/
5+
# Ignore all pem files
76
*.pem
87

98
# Upload folder

Dockerfile

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,23 @@ FROM python:3.13-slim
44
# Set a specific working directory in the container
55
WORKDIR /app
66

7-
# Install dependencies
8-
RUN apt-get update && apt-get install -y --no-install-recommends curl openssl \
9-
&& apt-get clean && rm -rf /var/lib/apt/lists/*
10-
11-
# Install required Python packages
7+
# Install dependencies separately for better caching
128
COPY requirements.txt .
139
RUN pip install --no-cache-dir -r requirements.txt
1410

1511
# Copy the rest of the application files
1612
COPY . .
1713

14+
# Expose the Flask port
15+
EXPOSE 5000
16+
1817
# Create a non-root user and set permissions for the /app directory
1918
RUN adduser --disabled-password --gecos '' apiuser && chown -R apiuser /app
2019
USER apiuser
2120

22-
# Expose the Flask port
23-
EXPOSE 5000
24-
2521
# Add health check for the container
2622
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
2723
CMD curl --fail http://localhost:5000/ || exit 1
2824

29-
ENTRYPOINT ["python3", "app.py"]
25+
# Command to run the app
26+
CMD ["python", "app.py"]

app.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import argparse
2-
import os
32
import traceback
43

54
from flask import Flask, jsonify, request
65
from flask_cors import CORS
76
from werkzeug.exceptions import HTTPException
87

9-
from config.jwtoken import ACTIVE_KID_FILE
108
from config.logging import setup_logging
119
from config.ratelimit import limiter
1210
from config.settings import Config
1311
from routes import register_routes
1412
from utility.database import extract_error_message
15-
from utility.jwtoken.keys_rotation import rotate_keys
1613

1714
app = Flask(__name__)
1815
app.config.from_object(Config)
@@ -29,10 +26,6 @@
2926
if app.config["TESTING"]:
3027
limiter.enabled = False
3128

32-
# Ensure the keys directory and active_kid.txt file exist
33-
if not os.path.exists(ACTIVE_KID_FILE):
34-
rotate_keys()
35-
3629

3730
@app.route("/")
3831
def home():

config/jwtoken.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

config/logging.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ def setup_logging():
1212
logger.setLevel(logging.DEBUG)
1313

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

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

2222
# Set up timed rotating file handler
2323
file_handler = TimedRotatingFileHandler(

config/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Config:
2020
MYSQL_CURSORCLASS = "DictCursor"
2121

2222
# JWT configuration
23+
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
2324
JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1)
2425
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30)
2526

jwt_helper.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
from datetime import datetime, timedelta, timezone
3+
from functools import wraps
4+
5+
import jwt
6+
from flask import jsonify, request
7+
8+
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "SuperSecretKey")
9+
JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1)
10+
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30)
11+
12+
13+
class TokenError(Exception):
14+
"""Custom exception for token-related errors."""
15+
16+
def __init__(self, message, status_code):
17+
super().__init__(message)
18+
self.status_code = status_code
19+
self.message = message
20+
21+
22+
def generate_access_token(person_id: int) -> str:
23+
"""Generate a short-lived JWT access token for a user."""
24+
payload = {
25+
"person_id": person_id,
26+
"exp": datetime.now(timezone.utc) + JWT_ACCESS_TOKEN_EXPIRY, # Expiration
27+
"iat": datetime.now(timezone.utc), # Issued at
28+
"token_type": "access",
29+
}
30+
return jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256")
31+
32+
33+
def generate_refresh_token(person_id: int) -> str:
34+
"""Generate a long-lived refresh token for a user."""
35+
payload = {
36+
"person_id": person_id,
37+
"exp": datetime.now(timezone.utc) + JWT_REFRESH_TOKEN_EXPIRY,
38+
"iat": datetime.now(timezone.utc),
39+
"token_type": "refresh",
40+
}
41+
return jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256")
42+
43+
44+
def extract_token_from_header() -> str:
45+
"""Extract the Bearer token from the Authorization header."""
46+
auth_header = request.headers.get("Authorization")
47+
if not auth_header or not auth_header.startswith("Bearer "):
48+
raise TokenError("Token is missing or improperly formatted", 401)
49+
return auth_header.split("Bearer ")[1]
50+
51+
52+
def verify_token(token: str, required_type: str) -> dict:
53+
"""Verify and decode a JWT token."""
54+
try:
55+
decoded = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
56+
if decoded.get("token_type") != required_type:
57+
raise jwt.InvalidTokenError("Invalid token type")
58+
return decoded
59+
except jwt.ExpiredSignatureError:
60+
raise TokenError("Token has expired", 401)
61+
except jwt.InvalidTokenError:
62+
raise TokenError("Invalid token", 401)
63+
64+
65+
def token_required(f):
66+
"""Decorator to protect routes by requiring a valid token."""
67+
68+
@wraps(f)
69+
def decorated(*args, **kwargs):
70+
try:
71+
token = extract_token_from_header()
72+
decoded = verify_token(token, required_type="access")
73+
request.person_id = decoded["person_id"]
74+
return f(*args, **kwargs)
75+
except TokenError as e:
76+
return jsonify(message=e.message), e.status_code
77+
78+
return decorated

jwtoken/decorators.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)