File size: 9,430 Bytes
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887aa67
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887aa67
 
 
 
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887aa67
 
 
 
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887aa67
 
 
 
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
887aa67
 
 
 
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887aa67
 
 
 
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887aa67
 
 
 
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
"""
Auth Middleware - Request authentication layer

Intercepts requests to validate JWT tokens and attach authenticated
user to request.state for use in route handlers.
"""

import logging
from fastapi import Request, HTTPException, status
from fastapi.responses import JSONResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.middleware.base import BaseHTTPMiddleware

from core.database import async_session_maker
from core.models import User
from core.api_response import error_response, ErrorCode
from services.auth_service.config import AuthServiceConfig
from services.auth_service.jwt_provider import (
    verify_access_token,
    TokenExpiredError,
    InvalidTokenError,
    JWTError,
)
from services.base_service.middleware_chain import (
    BaseServiceMiddleware,
    get_request_context,
)

logger = logging.getLogger(__name__)


class AuthMiddleware(BaseServiceMiddleware):
    """
    Authentication middleware for request validation.
    
    Flow:
    1. Check if route requires/allows auth based on URL
    2. Extract Authorization header
    3. Verify JWT token
    4. Load user from database
    5. Attach user to request.state.user
    6. Continue to next middleware/route
    
    Public routes skip all auth checks.
    Required routes must have valid auth or return 401.
    Optional routes attach user if auth is provided, but don't fail if missing.
    """
    
    SERVICE_NAME = "auth"
    
    async def dispatch(self, request: Request, call_next):
        """Process request through auth middleware."""
        # Skip OPTIONS requests (CORS preflight)
        if request.method == "OPTIONS":
            return await call_next(request)
        
        # Initialize request context
        ctx = get_request_context(request)
        
        # Get path and method from request
        path = request.url.path
        
        # Check if route is public (skip all auth)
        if AuthServiceConfig.is_public(path):
            self.log_request(request, "Public route, skipping auth")
            request.state.user = None
            ctx.user = None
            ctx.is_authenticated = False
            response = await call_next(request)
            return response
        
        # Check if route requires auth or allows optional auth
        requires_auth = AuthServiceConfig.requires_auth(path)
        allows_optional = AuthServiceConfig.allows_optional_auth(path)
        
        # If route doesn't require auth and doesn't allow optional, skip
        if not requires_auth and not allows_optional:
            self.log_request(request, "Route not configured for auth, skipping")
            request.state.user = None
            ctx.user = None
            ctx.is_authenticated = False
            response = await call_next(request)
            return response
        
        # Extract Authorization header
        auth_header = request.headers.get("Authorization")
        
        # If no auth header
        if not auth_header:
            if requires_auth:
                self.log_request(request, "Missing Authorization header (required)")
                return JSONResponse(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    content=error_response(
                        ErrorCode.UNAUTHORIZED,
                        "Missing Authorization header"
                    ),
                    headers={"WWW-Authenticate": "Bearer"},
                )
            else:
                # Optional auth, no header provided
                self.log_request(request, "No auth header (optional route)")
                request.state.user = None
                ctx.user = None
                ctx.is_authenticated = False
                response = await call_next(request)
                return response
        
        # Validate Authorization header format
        if not auth_header.startswith("Bearer "):
            if requires_auth:
                self.log_request(request, "Invalid Authorization header format")
                return JSONResponse(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    content=error_response(
                        ErrorCode.TOKEN_INVALID,
                        "Invalid Authorization header format. Use: Bearer <token>"
                    ),
                    headers={"WWW-Authenticate": "Bearer"},
                )
            else:
                # Optional auth, invalid format
                request.state.user = None
                ctx.user = None
                ctx.is_authenticated = False
                response = await call_next(request)
                return response
        
        # Extract token
        token = auth_header.split(" ", 1)[1]
        
        # Verify token
        try:
            payload = verify_access_token(token)
        except TokenExpiredError:
            if requires_auth:
                self.log_request(request, "Token expired")
                return JSONResponse(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    content=error_response(
                        ErrorCode.TOKEN_EXPIRED,
                        "Token has expired. Please sign in again."
                    ),
                    headers={"WWW-Authenticate": "Bearer"},
                )
            else:
                # Optional auth, expired token
                request.state.user = None
                ctx.user = None
                ctx.is_authenticated = False
                response = await call_next(request)
                return response
        except (InvalidTokenError, JWTError) as e:
            if requires_auth:
                self.log_error(request, f"Token verification failed: {e}")
                return JSONResponse(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    content=error_response(
                        ErrorCode.TOKEN_INVALID,
                        f"Invalid token: {str(e)}"
                    ),
                    headers={"WWW-Authenticate": "Bearer"},
                )
            else:
                # Optional auth, invalid token
                request.state.user = None
                ctx.user = None
                ctx.is_authenticated = False
                response = await call_next(request)
                return response
        
        # Get database session
        async with async_session_maker() as db:
            try:
                # Load user from database
                query = select(User).where(
                    User.user_id == payload.user_id,
                    User.is_active == True
                )
                result = await db.execute(query)
                user = result.scalar_one_or_none()
                
                if not user:
                    if requires_auth:
                        self.log_request(request, "User not found or inactive")
                        return JSONResponse(
                            status_code=status.HTTP_401_UNAUTHORIZED,
                            content=error_response(
                                ErrorCode.USER_NOT_FOUND,
                                "User not found or inactive"
                            ),
                        )
                    else:
                        # Optional auth, user not found
                        request.state.user = None
                        ctx.user = None
                        ctx.is_authenticated = False
                        response = await call_next(request)
                        return response
                
                if payload.token_version < user.token_version:
                    if requires_auth:
                        self.log_request(
                            request,
                            f"Token invalidated (version {payload.token_version} < {user.token_version})"
                        )
                        return JSONResponse(
                            status_code=status.HTTP_401_UNAUTHORIZED,
                            content=error_response(
                                ErrorCode.TOKEN_INVALID,
                                "Token has been invalidated. Please sign in again."
                            ),
                            headers={"WWW-Authenticate": "Bearer"},
                        )
                    else:
                        # Optional auth, invalidated token
                        request.state.user = None
                        ctx.user = None
                        ctx.is_authenticated = False
                        response = await call_next(request)
                        return response
                
                # Attach user to request state
                request.state.user = user
                ctx.set_user(user)
                
                # Check if user is admin
                is_admin = AuthServiceConfig.is_admin(user.email)
                request.state.is_admin = is_admin
                ctx.set_flag('is_admin', is_admin)
                
                self.log_request(request, f"Authenticated user: {user.user_id}")
                
                # Continue to next middleware/route
                response = await call_next(request)
                return response
            
            finally:
                await db.close()


__all__ = ['AuthMiddleware']