Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ project-backups/

# Logo and assets - may contain proprietary branding
assets/
.claude/
docs/
frontend/assets/

# Session/context files - AI or IDE session state
Expand Down
19 changes: 12 additions & 7 deletions backend/app/api/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

from flask import Blueprint, request, jsonify, current_app
from app.services.auth_service import AuthService
from app.utils.decorators import token_required, admin_required, validate_json_request
from app.utils.decorators import (
extract_bearer_token,
token_required,
admin_required,
validate_json_request,
)

auth_bp = Blueprint('auth', __name__)

Expand Down Expand Up @@ -47,9 +52,9 @@ def login():
def logout():
"""User logout endpoint."""
try:
auth_header = request.headers.get('Authorization', '')
token = auth_header.replace(
'Bearer ', '') if auth_header.startswith('Bearer ') else ''
token = extract_bearer_token(
request.headers.get('Authorization', '')
) or ''

success, message = AuthService.logout_user(token)

Expand Down Expand Up @@ -82,9 +87,9 @@ def verify_token():
def refresh_token():
"""Refresh authentication token."""
try:
auth_header = request.headers.get('Authorization', '')
token = auth_header.replace(
'Bearer ', '') if auth_header.startswith('Bearer ') else ''
token = extract_bearer_token(
request.headers.get('Authorization', '')
) or ''

success, new_token, message = AuthService.refresh_token(token)

Expand Down
23 changes: 19 additions & 4 deletions backend/app/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,32 @@
from app.services.auth_service import AuthService


def extract_bearer_token(auth_header: str) -> Optional[str]:
"""Extract the token from a ``Bearer <token>`` Authorization header.

Returns the token string, or ``None`` if the header is missing,
empty, or doesn't start with "Bearer ". Centralizes the parsing
that was previously duplicated across decorators and route handlers
with inconsistent approaches (split vs replace).
"""
if not auth_header:
return None
parts = auth_header.split(' ', 1)
if len(parts) != 2 or parts[0] != 'Bearer':
return None
token = parts[1].strip()
return token if token else None


def _extract_and_validate_token() -> Tuple[Optional[Any], Optional[Any]]:
"""
Extract JWT from Authorization header and validate.
Returns (user_data, error_response) - if user_data is set, error_response is None.
"""
token = None
if 'Authorization' in request.headers:
auth_header = request.headers['Authorization']
try:
token = auth_header.split(' ')[1]
except IndexError:
token = extract_bearer_token(request.headers['Authorization'])
if token is None and request.headers['Authorization']:
return None, (jsonify({'error': 'Invalid token format'}), 401)

if not token:
Expand Down
Loading
Loading