fastapi-oauth2-cookie 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ from .oauth2_password_cookie import OAuth2PasswordCookie, Tokens
2
+ from .auth_cookie_manager import AuthCookieManager
3
+
4
+ __all__ = ["OAuth2PasswordCookie", "AuthCookieManager", "Tokens"]
@@ -0,0 +1,66 @@
1
+ from typing import Literal, Optional
2
+ from fastapi import Response
3
+
4
+
5
+ class AuthCookieManager:
6
+ """
7
+ Basic auth cookie management
8
+ """
9
+
10
+ def __init__(
11
+ self,
12
+ access_cookie_name: str = "ACCESS-TOKEN",
13
+ refresh_cookie_name: str = "REFRESH-TOKEN",
14
+ refresh_path: str = "/refresh-token",
15
+ secure: bool = True,
16
+ samesite: Literal["lax", "strict", "none"] = "lax",
17
+ ):
18
+ self.access_cookie_name: str = access_cookie_name
19
+ self.refresh_cookie_name: str = refresh_cookie_name
20
+ self.refresh_path = refresh_path
21
+ self.secure: bool = secure
22
+ self.samesite: Literal["lax", "strict", "none"] = samesite
23
+
24
+ def set_auth_cookies(
25
+ self,
26
+ response: Response,
27
+ access_token: str,
28
+ refresh_token: Optional[str] = None,
29
+ max_age_access: int = 900,
30
+ max_age_refresh: int = 604800,
31
+ ) -> None:
32
+ """Sets auth cookies using the manager's global configuration."""
33
+ response.set_cookie(
34
+ key=self.access_cookie_name,
35
+ value=access_token,
36
+ httponly=True,
37
+ secure=self.secure,
38
+ samesite=self.samesite,
39
+ max_age=max_age_access,
40
+ )
41
+ if refresh_token:
42
+ response.set_cookie(
43
+ key=self.refresh_cookie_name,
44
+ value=refresh_token,
45
+ path=self.refresh_path,
46
+ httponly=True,
47
+ secure=self.secure,
48
+ samesite=self.samesite,
49
+ max_age=max_age_refresh,
50
+ )
51
+
52
+ def clear_auth_cookies(self, response: Response) -> None:
53
+ """Clears auth cookies using the manager's global configuration."""
54
+ response.delete_cookie(
55
+ key=self.access_cookie_name,
56
+ secure=self.secure,
57
+ samesite=self.samesite,
58
+ httponly=True,
59
+ )
60
+ response.delete_cookie(
61
+ key=self.refresh_cookie_name,
62
+ secure=self.secure,
63
+ samesite=self.samesite,
64
+ httponly=True,
65
+ path=self.refresh_path,
66
+ )
@@ -0,0 +1,59 @@
1
+ from typing import Dict, NamedTuple, Optional
2
+ from fastapi import HTTPException, Request, status
3
+ from fastapi.openapi.models import OAuthFlowPassword
4
+ from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
5
+ from fastapi.security import OAuth2
6
+
7
+
8
+ class Tokens(NamedTuple):
9
+ access_token: str
10
+ csrf_token: Optional[str]
11
+
12
+
13
+ class OAuth2PasswordCookie(OAuth2):
14
+ """
15
+ OAuth2 security scheme that extracts the access token from an HttpOnly cookie
16
+ and the CSRF token from a custom header.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ tokenUrl: str,
22
+ scheme_name: Optional[str] = None,
23
+ scopes: Optional[Dict[str, str]] = None,
24
+ auto_error: bool = True,
25
+ access_cookie_name: str = "ACCESS-TOKEN",
26
+ csrf_header_name: str = "X-CSRF-TOKEN",
27
+ require_csrf: bool = True,
28
+ ):
29
+ if not scopes:
30
+ scopes = {}
31
+
32
+ flows = OAuthFlowsModel(
33
+ password=OAuthFlowPassword(tokenUrl=tokenUrl, scopes=scopes)
34
+ )
35
+ super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
36
+
37
+ self.access_cookie_name = access_cookie_name
38
+ self.csrf_header_name = csrf_header_name
39
+ self.require_csrf = require_csrf
40
+
41
+ async def __call__( # type: ignore[override]
42
+ self, request: Request
43
+ ) -> Optional[Tokens]:
44
+ access_token = request.cookies.get(self.access_cookie_name)
45
+ csrf_token = (
46
+ request.headers.get(self.csrf_header_name) if self.require_csrf else None
47
+ )
48
+
49
+ if not access_token or (self.require_csrf and not csrf_token):
50
+ if self.auto_error:
51
+ raise HTTPException(
52
+ status_code=status.HTTP_401_UNAUTHORIZED,
53
+ detail="Not authenticated",
54
+ headers={"WWW-Authenticate": "Bearer"},
55
+ )
56
+ else:
57
+ return None
58
+
59
+ return Tokens(access_token=access_token, csrf_token=csrf_token)
File without changes
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.5
2
+ Name: fastapi-oauth2-cookie
3
+ Version: 0.0.1
4
+ Summary: A lightweight FastAPI library for OAuth2 via HttpOnly Cookies and CSRF headers.
5
+ Project-URL: Homepage, https://github.com/D1918/fastapi-oauth2-cookie.git
6
+ Author-email: Denis Lyovin <DOS1918@proton.me>
7
+ License: MIT
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: fastapi>=0.95.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # FastAPI OAuth2 Cookie Authentication
13
+
14
+ A lightweight, production-ready FastAPI library for handling OAuth2 authentication using **HttpOnly Cookies** and **CSRF headers**.
15
+
16
+ ## Features
17
+
18
+ * 🔒 **HttpOnly Cookies**: Keeps JWT access and refresh tokens out of `localStorage` to prevent XSS attacks.
19
+ * ⚡️ **Swagger UI Compatible**: Extends FastAPI's native `OAuth2` class to keep your OpenAPI docs working.
20
+ * 🛡️ **CSRF Defense**: Native support for extracting and requiring CSRF tokens via headers (`X-CSRF-TOKEN`).
21
+ * 🪶 **Zero Overhead**: No third-party dependencies outside of FastAPI.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install fastapi-oauth2-cookie
27
+ # or using uv:
28
+ uv add fastapi-oauth2-cookie
29
+
30
+ ```
31
+
32
+ ## Quickstart
33
+
34
+ Here is everything you need to set up login, logout, and a protected route.
35
+
36
+ ```python
37
+ from typing import Annotated
38
+ from fastapi import Depends, FastAPI, HTTPException, Response, status
39
+ from fastapi.security import OAuth2PasswordRequestForm
40
+ from fastapi_oauth2_cookie import AuthCookieManager, OAuth2PasswordCookie, Tokens
41
+
42
+ app = FastAPI()
43
+
44
+ cookie_manager = AuthCookieManager(secure=False) # set True in production
45
+
46
+ oauth2_scheme = OAuth2PasswordCookie(
47
+ tokenUrl="token",
48
+ access_cookie_name=cookie_manager.access_cookie_name,
49
+ require_csrf=False # set to True in production to avoid CSRF attacks
50
+ )
51
+
52
+ FAKE_USERS_DB = {
53
+ "johndoe": {
54
+ "username": "johndoe",
55
+ "hashed_password": "fakehashedsecretpassword",
56
+ }
57
+ }
58
+
59
+ def fake_hash_password(password: str):
60
+ return "fakehashed" + password
61
+
62
+ def fake_decode_token(token: str):
63
+ return FAKE_USERS_DB.get(token)
64
+
65
+ async def get_current_user(tokens: Tokens = Depends(oauth2_scheme)):
66
+ user = fake_decode_token(tokens.access_token)
67
+ if not user:
68
+ raise HTTPException(
69
+ status_code=status.HTTP_401_UNAUTHORIZED,
70
+ detail="Invalid authentication credentials",
71
+ )
72
+ return user
73
+
74
+ @app.post("/token")
75
+ async def login(
76
+ response: Response,
77
+ form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
78
+ ):
79
+ user = FAKE_USERS_DB.get(form_data.username)
80
+ if not user or fake_hash_password(form_data.password) != user["hashed_password"]:
81
+ raise HTTPException(
82
+ status_code=status.HTTP_400_BAD_REQUEST,
83
+ detail="Incorrect username or password"
84
+ )
85
+
86
+ # 1. Set the HttpOnly cookie for the browser
87
+ cookie_manager.set_auth_cookies(response, access_token=user["username"])
88
+
89
+ # 2. Return standard OAuth2 JSON so Swagger UI works correctly
90
+ return {"access_token": user["username"], "token_type": "bearer"}
91
+
92
+ @app.get("/users/me")
93
+ async def read_users_me(current_user: dict = Depends(get_current_user)):
94
+ return {"username": current_user["username"]}
95
+
96
+ @app.post("/logout")
97
+ def logout(response: Response):
98
+ cookie_manager.clear_auth_cookies(response)
99
+ return {"message": "Successfully logged out"}
100
+
101
+ ```
102
+
103
+ ## Core API
104
+
105
+ ### `AuthCookieManager`
106
+
107
+ Handles the configuration and application of cookies
108
+
109
+ * `set_auth_cookies(response, access_token, refresh_token=None, ...)`: Attaches `HttpOnly` cookies to the response.
110
+ * `clear_auth_cookies(response)`: Clears the authentication cookies.
111
+
112
+ ### `OAuth2PasswordCookie`
113
+
114
+ The FastAPI dependency you inject into your routes.
115
+
116
+ * Extracts the token from the configured `access_cookie_name`.
117
+ * Optionally enforces and extracts a CSRF token from the `csrf_header_name` (defaults to `X-CSRF-TOKEN`).
118
+ * Returns a named tuple `Tokens`: `(access_token, csrf_token)`. Raises `401 Unauthorized` if tokens are missing or invalid.
119
+
120
+ ## Development
121
+
122
+ ### Run dev server
123
+ ```bash
124
+ uv run fastapi dev dev/main.py
125
+ ```
126
+
127
+ ### Run tests
128
+ ```bash
129
+ uv run pytest
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,7 @@
1
+ fastapi_oauth2_cookie/__init__.py,sha256=mGFvGrJEWHo0ss3G4fUdZmcqsxgQqOmtZ1cQmwt_q5k,183
2
+ fastapi_oauth2_cookie/auth_cookie_manager.py,sha256=PFj6w_dEmxxbNQwNc8Plr_ZpM6G8t3amQuR4xBJxkt0,2103
3
+ fastapi_oauth2_cookie/oauth2_password_cookie.py,sha256=1xa2K_b-APK2IsW6g-sQN6IPqw_tZYtO5FgcmCx90Do,1971
4
+ fastapi_oauth2_cookie/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ fastapi_oauth2_cookie-0.0.1.dist-info/METADATA,sha256=7XVkQPRWDHlGw9IBWmDuLxREQ7svxm4rtH0p7OifduU,4066
6
+ fastapi_oauth2_cookie-0.0.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
7
+ fastapi_oauth2_cookie-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any