|
21 | 21 | require_unauthenticated_client, |
22 | 22 | ) |
23 | 23 | from utils.core.auth import refresh_token_is_persistent, set_auth_cookies |
| 24 | +from utils.core.csrf import ( |
| 25 | + CSRF_COOKIE_NAME, |
| 26 | + UNSAFE_HTTP_METHODS, |
| 27 | + csrf_enabled, |
| 28 | + extract_submitted_csrf_token, |
| 29 | + generate_csrf_token, |
| 30 | + set_csrf_cookie, |
| 31 | + validate_csrf_token, |
| 32 | +) |
24 | 33 | from utils.core.htmx import ( |
25 | 34 | is_htmx_request, |
26 | 35 | toast_response, |
|
30 | 39 | from exceptions.http_exceptions import ( |
31 | 40 | AlreadyAuthenticatedError, |
32 | 41 | AuthenticationError, |
| 42 | + CsrfError, |
33 | 43 | PasswordValidationError, |
34 | 44 | CredentialsError, |
35 | 45 | RateLimitError, |
@@ -73,6 +83,22 @@ async def flash_cookie_middleware(request: Request, call_next): |
73 | 83 | return response |
74 | 84 |
|
75 | 85 |
|
| 86 | +@app.middleware("http") |
| 87 | +async def csrf_middleware(request: Request, call_next): |
| 88 | + token = request.cookies.get(CSRF_COOKIE_NAME) or generate_csrf_token() |
| 89 | + request.state.csrf_token = token |
| 90 | + |
| 91 | + if csrf_enabled() and request.method in UNSAFE_HTTP_METHODS: |
| 92 | + submitted = await extract_submitted_csrf_token(request) |
| 93 | + if not validate_csrf_token(request, submitted): |
| 94 | + return await csrf_error_handler(request, CsrfError()) |
| 95 | + |
| 96 | + response = await call_next(request) |
| 97 | + if request.cookies.get(CSRF_COOKIE_NAME) != token: |
| 98 | + set_csrf_cookie(response, token) |
| 99 | + return response |
| 100 | + |
| 101 | + |
76 | 102 | # --- Include Routers --- |
77 | 103 |
|
78 | 104 |
|
@@ -137,6 +163,25 @@ async def rate_limit_error_handler(request: Request, exc: RateLimitError): |
137 | 163 | return response |
138 | 164 |
|
139 | 165 |
|
| 166 | +@app.exception_handler(CsrfError) |
| 167 | +async def csrf_error_handler(request: Request, exc: CsrfError): |
| 168 | + if is_htmx_request(request): |
| 169 | + return toast_response( |
| 170 | + request, |
| 171 | + templates, |
| 172 | + exc.detail, |
| 173 | + level="danger", |
| 174 | + status_code=403, |
| 175 | + ) |
| 176 | + user = await get_user_from_request(request) |
| 177 | + return templates.TemplateResponse( |
| 178 | + request, |
| 179 | + "errors/error.html", |
| 180 | + {"status_code": 403, "detail": exc.detail, "errors": None, "user": user}, |
| 181 | + status_code=403, |
| 182 | + ) |
| 183 | + |
| 184 | + |
140 | 185 | # Handle CredentialsError (invalid email/password) with toast for HTMX |
141 | 186 | @app.exception_handler(CredentialsError) |
142 | 187 | async def credentials_exception_handler(request: Request, exc: CredentialsError): |
|
0 commit comments