fastapi-oauth2-cookie 0.0.1__tar.gz

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,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ deploy:
9
+ runs-on: ubuntu-latest
10
+ environment:
11
+ name: pypi
12
+ url: https://pypi.org/p/fastapi-oauth2-cookie
13
+ permissions:
14
+ id-token: write
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v5
26
+
27
+ - name: Build package
28
+ run: uv build
29
+
30
+ - name: Publish package to PyPI
31
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,70 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+
6
+ # Virtual environments
7
+ env/
8
+ venv/
9
+ .venv/
10
+ env3.*
11
+ *venv*
12
+
13
+ # Packaging / builds
14
+ build/
15
+ dist/
16
+ *.egg-info/
17
+ .eggs/
18
+
19
+ # Testing / coverage
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+ .pyre/
23
+ .pytype/
24
+ .ruff_cache/
25
+ .hypothesis/
26
+ htmlcov/
27
+ .coverage*
28
+ coverage.xml
29
+
30
+ # Documentation builds
31
+ docs/_build/
32
+ site/
33
+ site_build/
34
+ docs_build/
35
+
36
+ # Jupyter
37
+ .ipynb_checkpoints/
38
+
39
+ # IDEs
40
+ .idea/
41
+ .vscode/
42
+
43
+ # Environment / secrets
44
+ .env
45
+ .env.*
46
+ !.env.example
47
+
48
+ # Local files
49
+ test.db
50
+ log.txt
51
+ .cache/
52
+
53
+ # Deployment / tooling
54
+ .netlify/
55
+ .codspeed/
56
+
57
+ # Archives / generated files
58
+ docs.zip
59
+ archive.zip
60
+
61
+ # Vim / editor temporary files
62
+ *~
63
+ *.swp
64
+ *.swo
65
+
66
+ # macOS
67
+ .DS_Store
68
+
69
+ # Emacs
70
+ .projectile
@@ -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,123 @@
1
+ # FastAPI OAuth2 Cookie Authentication
2
+
3
+ A lightweight, production-ready FastAPI library for handling OAuth2 authentication using **HttpOnly Cookies** and **CSRF headers**.
4
+
5
+ ## Features
6
+
7
+ * 🔒 **HttpOnly Cookies**: Keeps JWT access and refresh tokens out of `localStorage` to prevent XSS attacks.
8
+ * ⚡️ **Swagger UI Compatible**: Extends FastAPI's native `OAuth2` class to keep your OpenAPI docs working.
9
+ * 🛡️ **CSRF Defense**: Native support for extracting and requiring CSRF tokens via headers (`X-CSRF-TOKEN`).
10
+ * 🪶 **Zero Overhead**: No third-party dependencies outside of FastAPI.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install fastapi-oauth2-cookie
16
+ # or using uv:
17
+ uv add fastapi-oauth2-cookie
18
+
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ Here is everything you need to set up login, logout, and a protected route.
24
+
25
+ ```python
26
+ from typing import Annotated
27
+ from fastapi import Depends, FastAPI, HTTPException, Response, status
28
+ from fastapi.security import OAuth2PasswordRequestForm
29
+ from fastapi_oauth2_cookie import AuthCookieManager, OAuth2PasswordCookie, Tokens
30
+
31
+ app = FastAPI()
32
+
33
+ cookie_manager = AuthCookieManager(secure=False) # set True in production
34
+
35
+ oauth2_scheme = OAuth2PasswordCookie(
36
+ tokenUrl="token",
37
+ access_cookie_name=cookie_manager.access_cookie_name,
38
+ require_csrf=False # set to True in production to avoid CSRF attacks
39
+ )
40
+
41
+ FAKE_USERS_DB = {
42
+ "johndoe": {
43
+ "username": "johndoe",
44
+ "hashed_password": "fakehashedsecretpassword",
45
+ }
46
+ }
47
+
48
+ def fake_hash_password(password: str):
49
+ return "fakehashed" + password
50
+
51
+ def fake_decode_token(token: str):
52
+ return FAKE_USERS_DB.get(token)
53
+
54
+ async def get_current_user(tokens: Tokens = Depends(oauth2_scheme)):
55
+ user = fake_decode_token(tokens.access_token)
56
+ if not user:
57
+ raise HTTPException(
58
+ status_code=status.HTTP_401_UNAUTHORIZED,
59
+ detail="Invalid authentication credentials",
60
+ )
61
+ return user
62
+
63
+ @app.post("/token")
64
+ async def login(
65
+ response: Response,
66
+ form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
67
+ ):
68
+ user = FAKE_USERS_DB.get(form_data.username)
69
+ if not user or fake_hash_password(form_data.password) != user["hashed_password"]:
70
+ raise HTTPException(
71
+ status_code=status.HTTP_400_BAD_REQUEST,
72
+ detail="Incorrect username or password"
73
+ )
74
+
75
+ # 1. Set the HttpOnly cookie for the browser
76
+ cookie_manager.set_auth_cookies(response, access_token=user["username"])
77
+
78
+ # 2. Return standard OAuth2 JSON so Swagger UI works correctly
79
+ return {"access_token": user["username"], "token_type": "bearer"}
80
+
81
+ @app.get("/users/me")
82
+ async def read_users_me(current_user: dict = Depends(get_current_user)):
83
+ return {"username": current_user["username"]}
84
+
85
+ @app.post("/logout")
86
+ def logout(response: Response):
87
+ cookie_manager.clear_auth_cookies(response)
88
+ return {"message": "Successfully logged out"}
89
+
90
+ ```
91
+
92
+ ## Core API
93
+
94
+ ### `AuthCookieManager`
95
+
96
+ Handles the configuration and application of cookies
97
+
98
+ * `set_auth_cookies(response, access_token, refresh_token=None, ...)`: Attaches `HttpOnly` cookies to the response.
99
+ * `clear_auth_cookies(response)`: Clears the authentication cookies.
100
+
101
+ ### `OAuth2PasswordCookie`
102
+
103
+ The FastAPI dependency you inject into your routes.
104
+
105
+ * Extracts the token from the configured `access_cookie_name`.
106
+ * Optionally enforces and extracts a CSRF token from the `csrf_header_name` (defaults to `X-CSRF-TOKEN`).
107
+ * Returns a named tuple `Tokens`: `(access_token, csrf_token)`. Raises `401 Unauthorized` if tokens are missing or invalid.
108
+
109
+ ## Development
110
+
111
+ ### Run dev server
112
+ ```bash
113
+ uv run fastapi dev dev/main.py
114
+ ```
115
+
116
+ ### Run tests
117
+ ```bash
118
+ uv run pytest
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT
@@ -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)
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "fastapi-oauth2-cookie"
7
+ version = "0.0.1"
8
+ description = "A lightweight FastAPI library for OAuth2 via HttpOnly Cookies and CSRF headers."
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Denis Lyovin", email = "DOS1918@proton.me" },
14
+ ]
15
+ dependencies = [
16
+ "fastapi>=0.95.0",
17
+ ]
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=9.1.1",
22
+ "httpx2>=2.12.0",
23
+ "fastapi[standard]>=0.141.1",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/D1918/fastapi-oauth2-cookie.git"
28
+
29
+ [tool.hatch.build.targets.sdist]
30
+ exclude = [
31
+ "1.md",
32
+ "uv.lock",
33
+ ".gitignore",
34
+ "tests/",
35
+ "dev/"
36
+ ]
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["fastapi_oauth2_cookie"]