-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_csrf.py
More file actions
82 lines (64 loc) · 2.74 KB
/
Copy path_csrf.py
File metadata and controls
82 lines (64 loc) · 2.74 KB
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
"""CSRF token subsystem for admin form POSTs.
Per-session double-submit cookie pattern:
* On every render of a page that contains a form, ensure a cookie
``hawkapi_admin_csrf`` is set (or reused) with a 32-byte URL-safe token.
* Templates render the same token as a hidden ``<input name="_csrf">``.
* POST handlers compare cookie and form value via ``hmac.compare_digest``.
The cookie is ``HttpOnly``: templates echo the token from the request scope
(server-side), not from the cookie via JavaScript, so there is no reason to
expose it to client scripts. The cookie is also short-lived (``Max-Age``) so
tokens rotate rather than living indefinitely.
"""
from __future__ import annotations
import hmac
import secrets
from typing import Any
COOKIE_NAME = "hawkapi_admin_csrf"
FORM_FIELD = "_csrf"
SCOPE_KEY = "admin_csrf_token"
SCOPE_NEW_KEY = "admin_csrf_token_is_new"
def _generate_token() -> str:
return secrets.token_urlsafe(32)
def ensure_csrf_token(request: Any) -> str:
"""Return the per-session CSRF token, reading the cookie or generating a new one.
The token is cached on ``request.scope`` so multiple template renders within
the same request return the same value, and so the response builder can
decide whether to emit a ``Set-Cookie`` header.
"""
cached = request.scope.get(SCOPE_KEY)
if isinstance(cached, str) and cached:
return cached
token = request.cookies.get(COOKIE_NAME)
is_new = False
if not isinstance(token, str) or not token:
token = _generate_token()
is_new = True
request.scope[SCOPE_KEY] = token
request.scope[SCOPE_NEW_KEY] = is_new
return token
def build_set_cookie_header(token: str, *, path: str) -> str:
"""Build a Set-Cookie header value for the CSRF cookie."""
return f"{COOKIE_NAME}={token}; Path={path}; Max-Age=86400; Secure; HttpOnly; SameSite=Lax"
def validate_csrf(request: Any, form: Any) -> None:
"""Validate that the form-supplied token matches the cookie token.
Raises ``HTTPException(403)`` on mismatch. Imported here lazily to avoid
a hard module-level dependency on hawkapi when this file is exercised
in isolation.
"""
from hawkapi import HTTPException
cookie_token = request.cookies.get(COOKIE_NAME) or ""
form_token_raw = form.get(FORM_FIELD)
form_token = form_token_raw if isinstance(form_token_raw, str) else ""
if not cookie_token or not form_token:
raise HTTPException(403, detail="CSRF token mismatch")
if not hmac.compare_digest(cookie_token, form_token):
raise HTTPException(403, detail="CSRF token mismatch")
__all__ = [
"COOKIE_NAME",
"FORM_FIELD",
"SCOPE_KEY",
"SCOPE_NEW_KEY",
"build_set_cookie_header",
"ensure_csrf_token",
"validate_csrf",
]