flask-advanced-auth 0.1.0__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.
- flask_advanced_auth-0.1.0/.gitignore +71 -0
- flask_advanced_auth-0.1.0/CHANGELOG.md +33 -0
- flask_advanced_auth-0.1.0/LICENSE +21 -0
- flask_advanced_auth-0.1.0/PKG-INFO +263 -0
- flask_advanced_auth-0.1.0/README.md +223 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/TokenProxy.py +69 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/__init__.py +73 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/authentication_classes.py +168 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/blacklist_token.py +101 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/database.py +108 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/decorators.py +126 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/exceptions.py +67 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/factory.py +276 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/helpers.py +84 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/import_handler.py +69 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/initialize.py +60 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/jwt_rules.py +254 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/model_managers.py +85 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/models.py +43 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/py.typed +1 -0
- flask_advanced_auth-0.1.0/flask_advanced_auth/validator.py +125 -0
- flask_advanced_auth-0.1.0/pyproject.toml +113 -0
- flask_advanced_auth-0.1.0/tests/__init__.py +2 -0
- flask_advanced_auth-0.1.0/tests/conftest.py +95 -0
- flask_advanced_auth-0.1.0/tests/integration/__init__.py +2 -0
- flask_advanced_auth-0.1.0/tests/integration/test_full_flow.py +122 -0
- flask_advanced_auth-0.1.0/tests/test_TokenProxy.py +70 -0
- flask_advanced_auth-0.1.0/tests/test_authentication_classes.py +148 -0
- flask_advanced_auth-0.1.0/tests/test_blacklist_token.py +93 -0
- flask_advanced_auth-0.1.0/tests/test_database.py +81 -0
- flask_advanced_auth-0.1.0/tests/test_decorators.py +93 -0
- flask_advanced_auth-0.1.0/tests/test_factory.py +105 -0
- flask_advanced_auth-0.1.0/tests/test_helpers.py +59 -0
- flask_advanced_auth-0.1.0/tests/test_import_handler.py +49 -0
- flask_advanced_auth-0.1.0/tests/test_initialize.py +51 -0
- flask_advanced_auth-0.1.0/tests/test_jwt_rules.py +143 -0
- flask_advanced_auth-0.1.0/tests/test_model_manager.py +56 -0
- flask_advanced_auth-0.1.0/tests/test_models.py +31 -0
- flask_advanced_auth-0.1.0/tests/test_validator.py +102 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
.Python
|
|
8
|
+
build/
|
|
9
|
+
develop-eggs/
|
|
10
|
+
dist/
|
|
11
|
+
downloads/
|
|
12
|
+
eggs/
|
|
13
|
+
.eggs/
|
|
14
|
+
lib/
|
|
15
|
+
lib64/
|
|
16
|
+
parts/
|
|
17
|
+
sdist/
|
|
18
|
+
var/
|
|
19
|
+
wheels/
|
|
20
|
+
share/python-wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
|
|
26
|
+
# Virtual environments
|
|
27
|
+
.env
|
|
28
|
+
.venv
|
|
29
|
+
env/
|
|
30
|
+
venv/
|
|
31
|
+
ENV/
|
|
32
|
+
env.bak/
|
|
33
|
+
venv.bak/
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
.tox/
|
|
37
|
+
.nox/
|
|
38
|
+
.coverage
|
|
39
|
+
.coverage.*
|
|
40
|
+
.cache
|
|
41
|
+
nosetests.xml
|
|
42
|
+
coverage.xml
|
|
43
|
+
*.cover
|
|
44
|
+
*.py,cover
|
|
45
|
+
.hypothesis/
|
|
46
|
+
.pytest_cache/
|
|
47
|
+
cover/
|
|
48
|
+
htmlcov/
|
|
49
|
+
|
|
50
|
+
# Editors
|
|
51
|
+
.vscode/
|
|
52
|
+
.idea/
|
|
53
|
+
*.swp
|
|
54
|
+
*.swo
|
|
55
|
+
*~
|
|
56
|
+
.DS_Store
|
|
57
|
+
|
|
58
|
+
# mypy
|
|
59
|
+
.mypy_cache/
|
|
60
|
+
.dmypy.json
|
|
61
|
+
dmypy.json
|
|
62
|
+
|
|
63
|
+
# SQLite databases (demo/dev)
|
|
64
|
+
instance/
|
|
65
|
+
*.db
|
|
66
|
+
|
|
67
|
+
# Env files with secrets
|
|
68
|
+
backend/.env
|
|
69
|
+
|
|
70
|
+
# Distribution
|
|
71
|
+
dist/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Multi-rule JWT configuration via `JWT_AUTHENTICATION_RULES` dict
|
|
12
|
+
- Singleton `JWTAuthenticationRules` for centralized rule management
|
|
13
|
+
- `TokenFactory` for generating and validating access and refresh tokens
|
|
14
|
+
- `@authenticate` decorator for protecting Flask routes
|
|
15
|
+
- `BlackListed` class for token revocation
|
|
16
|
+
- `Database` singleton for bridging Flask-SQLAlchemy
|
|
17
|
+
- `import_handler` utility for dynamic dotted-path imports
|
|
18
|
+
- `PyJWT` proxy wrapper for encode/decode operations
|
|
19
|
+
- Pydantic-backed `JWTRuleDetails` validator for rule configuration
|
|
20
|
+
- `initialize_jwt_authentication()` bootstrap entry point
|
|
21
|
+
- MIT License
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `parse_token()` was a stub โ now correctly extracts token from `Authorization` header
|
|
25
|
+
- `get_rule(None)` was returning the rule name string instead of the rule dict
|
|
26
|
+
- `@authenticate` decorator had misplaced `return wrapper` causing it to never execute the view
|
|
27
|
+
- `table` / `table_path` field aliases were misspelled as `"tabel"` / `"tabel_path"` in the Pydantic validator
|
|
28
|
+
- `is_blacklisted()` was ignoring `rule_name`, always reading the default rule's blacklist setting
|
|
29
|
+
- Added `_reset()` class methods to `Database` and `JWTAuthenticationRules` singletons for test isolation
|
|
30
|
+
|
|
31
|
+
## [0.1.0] - TBD
|
|
32
|
+
|
|
33
|
+
_Initial public release._
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Waheed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flask-advanced-auth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Advanced JWT authentication for Flask with multi-rule support, token tracking, and blacklisting.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Wehda23/Flask-Advanced-Authentication
|
|
6
|
+
Project-URL: Repository, https://github.com/Wehda23/Flask-Advanced-Authentication
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/Wehda23/Flask-Advanced-Authentication/issues
|
|
8
|
+
Author: Waheed
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: auth,authentication,blacklist,flask,flask-sqlalchemy,jwt,token
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Flask
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: flask-sqlalchemy>=3.0.0
|
|
26
|
+
Requires-Dist: flask>=2.3.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0.0
|
|
28
|
+
Requires-Dist: pyjwt>=2.8.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pre-commit>=3.4.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-flask>=1.3.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# flask-advanced-auth
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/flask-advanced-auth/)
|
|
44
|
+
[](https://pypi.org/project/flask-advanced-auth/)
|
|
45
|
+
[](https://opensource.org/licenses/MIT)
|
|
46
|
+
[](https://github.com/Wehda23/Flask-Advanced-Authentication/actions)
|
|
47
|
+
[](https://github.com/Wehda23/Flask-Advanced-Authentication)
|
|
48
|
+
|
|
49
|
+
**flask-advanced-auth** is a production-grade, highly configurable JWT authentication extension for Flask and Flask-SQLAlchemy. It provides multi-rule support (different token lifetimes, secrets, and models for users vs. admins), built-in database token tracking, automatic token revocation (blacklisting), and clean route decorators.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## โจ Features
|
|
54
|
+
|
|
55
|
+
- ๐ **Multi-Rule Authentication**: Define distinct rules for different user tiers (e.g. `users`, `managers`, `api_clients`) with independent secrets, algorithms, and lifetimes.
|
|
56
|
+
- ๐ก๏ธ **Easy Route Protection**: Clean `@authenticate(auth_classes=[AuthenticateJWT])` decorator with customizable 401 response and model injection into `request.instance`.
|
|
57
|
+
- ๐ **Access & Refresh Tokens**: Native support for short-lived access tokens and long-lived refresh tokens with convenient `refresh_access_token()` helper.
|
|
58
|
+
- ๐ซ **Token Revocation (Blacklisting)**: Built-in revocation mechanism with database persistence and `logout()` helper.
|
|
59
|
+
- ๐ **Token Tracking**: Track issued tokens in the database to detect anomalies, enforce single-session limits, or audit activity.
|
|
60
|
+
- ๐ **Multiple Extraction Strategies**: Extract tokens from `Authorization: Bearer <token>` headers, HTTP cookies, or URL query parameters.
|
|
61
|
+
- โก **Pydantic Validation**: Strict configuration schema validation powered by Pydantic V2 with flexible aliases.
|
|
62
|
+
- ๐งช **Thoroughly Tested**: 100% test pass rate with โฅ 88% test coverage.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ๐ฆ Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install flask-advanced-auth
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or install with development dependencies:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install flask-advanced-auth[dev]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ๐ Quick Start (5 Minutes)
|
|
81
|
+
|
|
82
|
+
### 1. Configure Rules & Bootstrap
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from datetime import timedelta
|
|
86
|
+
from flask import Flask, jsonify, request
|
|
87
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
88
|
+
from flask_advanced_auth import (
|
|
89
|
+
initialize_jwt_authentication,
|
|
90
|
+
authenticate,
|
|
91
|
+
AuthenticateJWT,
|
|
92
|
+
TokenFactory,
|
|
93
|
+
refresh_access_token,
|
|
94
|
+
logout,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
app = Flask(__name__)
|
|
98
|
+
app.config["SECRET_KEY"] = "your-app-secret"
|
|
99
|
+
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
|
|
100
|
+
db = SQLAlchemy(app)
|
|
101
|
+
|
|
102
|
+
# Define your JWT rules
|
|
103
|
+
JWT_RULES = {
|
|
104
|
+
"default": {
|
|
105
|
+
"description": "Standard user access token",
|
|
106
|
+
"secret_key": "your-jwt-secret-key",
|
|
107
|
+
"algorithm": "HS256",
|
|
108
|
+
"access_token_lifetime": timedelta(minutes=15),
|
|
109
|
+
"refresh_token_lifetime": timedelta(days=7),
|
|
110
|
+
"token_header": "Bearer ",
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Initialize flask-advanced-auth
|
|
115
|
+
initialize_jwt_authentication(
|
|
116
|
+
database_path="app.db",
|
|
117
|
+
rules=JWT_RULES,
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. Issue Tokens on Login
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
@app.route("/api/login", methods=["POST"])
|
|
125
|
+
def login():
|
|
126
|
+
username = request.json.get("username")
|
|
127
|
+
|
|
128
|
+
factory = TokenFactory()
|
|
129
|
+
access_token = factory.generate_token(
|
|
130
|
+
rule_name="default",
|
|
131
|
+
extra_payload={"sub": username},
|
|
132
|
+
)
|
|
133
|
+
refresh_token = factory.generate_refresh_token(
|
|
134
|
+
rule_name="default",
|
|
135
|
+
extra_payload={"sub": username},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return jsonify({
|
|
139
|
+
"access_token": access_token,
|
|
140
|
+
"refresh_token": refresh_token,
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 3. Protect Routes
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
@app.route("/api/dashboard")
|
|
148
|
+
@authenticate(auth_classes=[AuthenticateJWT], rule_name="default")
|
|
149
|
+
def dashboard():
|
|
150
|
+
return jsonify({"message": "Welcome to your protected dashboard!"})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 4. Refresh Tokens & Logout
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
@app.route("/api/refresh", methods=["POST"])
|
|
157
|
+
def refresh():
|
|
158
|
+
token = request.json.get("refresh_token")
|
|
159
|
+
new_access_token = refresh_access_token(token, rule_name="default")
|
|
160
|
+
return jsonify({"access_token": new_access_token})
|
|
161
|
+
|
|
162
|
+
@app.route("/api/logout", methods=["POST"])
|
|
163
|
+
def user_logout():
|
|
164
|
+
token = request.headers.get("Authorization", "").replace("Bearer ", "")
|
|
165
|
+
logout(token, rule_name="default")
|
|
166
|
+
return jsonify({"message": "Token revoked successfully."})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## โ๏ธ Configuration Reference
|
|
172
|
+
|
|
173
|
+
Rules are passed as a dictionary where each key represents a rule name (e.g. `"default"`, `"admin"`):
|
|
174
|
+
|
|
175
|
+
| Field | Type | Default | Description |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `description` | `str` | *Required* | Description of what this token rule is used for. |
|
|
178
|
+
| `secret_key` | `str` | *Required* | Secret signing key for encoding and decoding tokens. |
|
|
179
|
+
| `algorithm` | `str` | *Required* | JWT signing algorithm (e.g. `'HS256'`, `'RS256'`). |
|
|
180
|
+
| `access_token_lifetime` | `timedelta` | *Required* | Expiry duration for access tokens. |
|
|
181
|
+
| `refresh_token_lifetime` | `timedelta` | `None` | Expiry duration for refresh tokens. |
|
|
182
|
+
| `token_header` | `str` | `'Bearer '` | Expected header prefix in `Authorization` header. |
|
|
183
|
+
| `table` | `bool` | `False` | Whether to link tokens to a database entity. |
|
|
184
|
+
| `table_path` | `str` | `None` | Dotted import path to model class (e.g. `'myapp.models.User'`). |
|
|
185
|
+
| `track_created` | `bool` | `False` | Persist issued tokens to the database. |
|
|
186
|
+
| `track_created_table_path` | `str` | `None` | Dotted import path to TrackModel class. |
|
|
187
|
+
| `track_created_allow_duplicates` | `bool` | `True` | Allow multiple active tokens for the same user. |
|
|
188
|
+
| `blacklisted` | `bool` | `False` | Enable token blacklist checks on verification. |
|
|
189
|
+
| `blacklisted_table_path` | `str` | `None` | Dotted import path to BlackListModel class. |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## ๐งฉ Authentication Strategies
|
|
194
|
+
|
|
195
|
+
You can choose how tokens are extracted by passing different authentication classes to `@authenticate`:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from flask_advanced_auth import (
|
|
199
|
+
authenticate,
|
|
200
|
+
HeaderAuthentication, # Reads Authorization: Bearer <token>
|
|
201
|
+
CookieAuthentication, # Reads token from HTTP cookies
|
|
202
|
+
QueryParamAuthentication, # Reads ?token=<token>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# 1. From Authorization header
|
|
206
|
+
@app.route("/api/v1/data")
|
|
207
|
+
@authenticate(auth_classes=[HeaderAuthentication])
|
|
208
|
+
def api_data():
|
|
209
|
+
return {"status": "ok"}
|
|
210
|
+
|
|
211
|
+
# 2. From Cookie
|
|
212
|
+
@app.route("/web/profile")
|
|
213
|
+
@authenticate(auth_classes=[CookieAuthentication])
|
|
214
|
+
def web_profile():
|
|
215
|
+
return {"status": "ok"}
|
|
216
|
+
|
|
217
|
+
# 3. From URL parameter (e.g., SSE or WebSocket connection handshake)
|
|
218
|
+
@app.route("/stream")
|
|
219
|
+
@authenticate(auth_classes=[QueryParamAuthentication])
|
|
220
|
+
def stream():
|
|
221
|
+
return {"status": "connected"}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## ๐๏ธ Database Model Mixins
|
|
227
|
+
|
|
228
|
+
When enabling `track_created` or `blacklisted`, use our ready-to-use SQLAlchemy model mixins:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from flask_advanced_auth.models import AbstractTrackModelMixin, AbstractBlackListModelMixin
|
|
232
|
+
from myapp.database import db
|
|
233
|
+
|
|
234
|
+
class TrackedToken(db.Model, AbstractTrackModelMixin):
|
|
235
|
+
__tablename__ = "tracked_tokens"
|
|
236
|
+
|
|
237
|
+
class BlacklistedToken(db.Model, AbstractBlackListModelMixin):
|
|
238
|
+
__tablename__ = "blacklisted_tokens"
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## ๐งช Running Tests
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# Run tests with coverage
|
|
247
|
+
pytest -v
|
|
248
|
+
|
|
249
|
+
# Run single test module
|
|
250
|
+
pytest tests/test_decorators.py -v
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## ๐ค Contributing
|
|
256
|
+
|
|
257
|
+
Contributions are welcome! Please check out [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## ๐ License
|
|
262
|
+
|
|
263
|
+
This project is licensed under the **MIT License** โ see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# flask-advanced-auth
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/flask-advanced-auth/)
|
|
4
|
+
[](https://pypi.org/project/flask-advanced-auth/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://github.com/Wehda23/Flask-Advanced-Authentication/actions)
|
|
7
|
+
[](https://github.com/Wehda23/Flask-Advanced-Authentication)
|
|
8
|
+
|
|
9
|
+
**flask-advanced-auth** is a production-grade, highly configurable JWT authentication extension for Flask and Flask-SQLAlchemy. It provides multi-rule support (different token lifetimes, secrets, and models for users vs. admins), built-in database token tracking, automatic token revocation (blacklisting), and clean route decorators.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## โจ Features
|
|
14
|
+
|
|
15
|
+
- ๐ **Multi-Rule Authentication**: Define distinct rules for different user tiers (e.g. `users`, `managers`, `api_clients`) with independent secrets, algorithms, and lifetimes.
|
|
16
|
+
- ๐ก๏ธ **Easy Route Protection**: Clean `@authenticate(auth_classes=[AuthenticateJWT])` decorator with customizable 401 response and model injection into `request.instance`.
|
|
17
|
+
- ๐ **Access & Refresh Tokens**: Native support for short-lived access tokens and long-lived refresh tokens with convenient `refresh_access_token()` helper.
|
|
18
|
+
- ๐ซ **Token Revocation (Blacklisting)**: Built-in revocation mechanism with database persistence and `logout()` helper.
|
|
19
|
+
- ๐ **Token Tracking**: Track issued tokens in the database to detect anomalies, enforce single-session limits, or audit activity.
|
|
20
|
+
- ๐ **Multiple Extraction Strategies**: Extract tokens from `Authorization: Bearer <token>` headers, HTTP cookies, or URL query parameters.
|
|
21
|
+
- โก **Pydantic Validation**: Strict configuration schema validation powered by Pydantic V2 with flexible aliases.
|
|
22
|
+
- ๐งช **Thoroughly Tested**: 100% test pass rate with โฅ 88% test coverage.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## ๐ฆ Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install flask-advanced-auth
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install with development dependencies:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install flask-advanced-auth[dev]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## ๐ Quick Start (5 Minutes)
|
|
41
|
+
|
|
42
|
+
### 1. Configure Rules & Bootstrap
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from datetime import timedelta
|
|
46
|
+
from flask import Flask, jsonify, request
|
|
47
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
48
|
+
from flask_advanced_auth import (
|
|
49
|
+
initialize_jwt_authentication,
|
|
50
|
+
authenticate,
|
|
51
|
+
AuthenticateJWT,
|
|
52
|
+
TokenFactory,
|
|
53
|
+
refresh_access_token,
|
|
54
|
+
logout,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
app = Flask(__name__)
|
|
58
|
+
app.config["SECRET_KEY"] = "your-app-secret"
|
|
59
|
+
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
|
|
60
|
+
db = SQLAlchemy(app)
|
|
61
|
+
|
|
62
|
+
# Define your JWT rules
|
|
63
|
+
JWT_RULES = {
|
|
64
|
+
"default": {
|
|
65
|
+
"description": "Standard user access token",
|
|
66
|
+
"secret_key": "your-jwt-secret-key",
|
|
67
|
+
"algorithm": "HS256",
|
|
68
|
+
"access_token_lifetime": timedelta(minutes=15),
|
|
69
|
+
"refresh_token_lifetime": timedelta(days=7),
|
|
70
|
+
"token_header": "Bearer ",
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Initialize flask-advanced-auth
|
|
75
|
+
initialize_jwt_authentication(
|
|
76
|
+
database_path="app.db",
|
|
77
|
+
rules=JWT_RULES,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Issue Tokens on Login
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
@app.route("/api/login", methods=["POST"])
|
|
85
|
+
def login():
|
|
86
|
+
username = request.json.get("username")
|
|
87
|
+
|
|
88
|
+
factory = TokenFactory()
|
|
89
|
+
access_token = factory.generate_token(
|
|
90
|
+
rule_name="default",
|
|
91
|
+
extra_payload={"sub": username},
|
|
92
|
+
)
|
|
93
|
+
refresh_token = factory.generate_refresh_token(
|
|
94
|
+
rule_name="default",
|
|
95
|
+
extra_payload={"sub": username},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return jsonify({
|
|
99
|
+
"access_token": access_token,
|
|
100
|
+
"refresh_token": refresh_token,
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3. Protect Routes
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
@app.route("/api/dashboard")
|
|
108
|
+
@authenticate(auth_classes=[AuthenticateJWT], rule_name="default")
|
|
109
|
+
def dashboard():
|
|
110
|
+
return jsonify({"message": "Welcome to your protected dashboard!"})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 4. Refresh Tokens & Logout
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
@app.route("/api/refresh", methods=["POST"])
|
|
117
|
+
def refresh():
|
|
118
|
+
token = request.json.get("refresh_token")
|
|
119
|
+
new_access_token = refresh_access_token(token, rule_name="default")
|
|
120
|
+
return jsonify({"access_token": new_access_token})
|
|
121
|
+
|
|
122
|
+
@app.route("/api/logout", methods=["POST"])
|
|
123
|
+
def user_logout():
|
|
124
|
+
token = request.headers.get("Authorization", "").replace("Bearer ", "")
|
|
125
|
+
logout(token, rule_name="default")
|
|
126
|
+
return jsonify({"message": "Token revoked successfully."})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## โ๏ธ Configuration Reference
|
|
132
|
+
|
|
133
|
+
Rules are passed as a dictionary where each key represents a rule name (e.g. `"default"`, `"admin"`):
|
|
134
|
+
|
|
135
|
+
| Field | Type | Default | Description |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| `description` | `str` | *Required* | Description of what this token rule is used for. |
|
|
138
|
+
| `secret_key` | `str` | *Required* | Secret signing key for encoding and decoding tokens. |
|
|
139
|
+
| `algorithm` | `str` | *Required* | JWT signing algorithm (e.g. `'HS256'`, `'RS256'`). |
|
|
140
|
+
| `access_token_lifetime` | `timedelta` | *Required* | Expiry duration for access tokens. |
|
|
141
|
+
| `refresh_token_lifetime` | `timedelta` | `None` | Expiry duration for refresh tokens. |
|
|
142
|
+
| `token_header` | `str` | `'Bearer '` | Expected header prefix in `Authorization` header. |
|
|
143
|
+
| `table` | `bool` | `False` | Whether to link tokens to a database entity. |
|
|
144
|
+
| `table_path` | `str` | `None` | Dotted import path to model class (e.g. `'myapp.models.User'`). |
|
|
145
|
+
| `track_created` | `bool` | `False` | Persist issued tokens to the database. |
|
|
146
|
+
| `track_created_table_path` | `str` | `None` | Dotted import path to TrackModel class. |
|
|
147
|
+
| `track_created_allow_duplicates` | `bool` | `True` | Allow multiple active tokens for the same user. |
|
|
148
|
+
| `blacklisted` | `bool` | `False` | Enable token blacklist checks on verification. |
|
|
149
|
+
| `blacklisted_table_path` | `str` | `None` | Dotted import path to BlackListModel class. |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## ๐งฉ Authentication Strategies
|
|
154
|
+
|
|
155
|
+
You can choose how tokens are extracted by passing different authentication classes to `@authenticate`:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from flask_advanced_auth import (
|
|
159
|
+
authenticate,
|
|
160
|
+
HeaderAuthentication, # Reads Authorization: Bearer <token>
|
|
161
|
+
CookieAuthentication, # Reads token from HTTP cookies
|
|
162
|
+
QueryParamAuthentication, # Reads ?token=<token>
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# 1. From Authorization header
|
|
166
|
+
@app.route("/api/v1/data")
|
|
167
|
+
@authenticate(auth_classes=[HeaderAuthentication])
|
|
168
|
+
def api_data():
|
|
169
|
+
return {"status": "ok"}
|
|
170
|
+
|
|
171
|
+
# 2. From Cookie
|
|
172
|
+
@app.route("/web/profile")
|
|
173
|
+
@authenticate(auth_classes=[CookieAuthentication])
|
|
174
|
+
def web_profile():
|
|
175
|
+
return {"status": "ok"}
|
|
176
|
+
|
|
177
|
+
# 3. From URL parameter (e.g., SSE or WebSocket connection handshake)
|
|
178
|
+
@app.route("/stream")
|
|
179
|
+
@authenticate(auth_classes=[QueryParamAuthentication])
|
|
180
|
+
def stream():
|
|
181
|
+
return {"status": "connected"}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## ๐๏ธ Database Model Mixins
|
|
187
|
+
|
|
188
|
+
When enabling `track_created` or `blacklisted`, use our ready-to-use SQLAlchemy model mixins:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from flask_advanced_auth.models import AbstractTrackModelMixin, AbstractBlackListModelMixin
|
|
192
|
+
from myapp.database import db
|
|
193
|
+
|
|
194
|
+
class TrackedToken(db.Model, AbstractTrackModelMixin):
|
|
195
|
+
__tablename__ = "tracked_tokens"
|
|
196
|
+
|
|
197
|
+
class BlacklistedToken(db.Model, AbstractBlackListModelMixin):
|
|
198
|
+
__tablename__ = "blacklisted_tokens"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## ๐งช Running Tests
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Run tests with coverage
|
|
207
|
+
pytest -v
|
|
208
|
+
|
|
209
|
+
# Run single test module
|
|
210
|
+
pytest tests/test_decorators.py -v
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## ๐ค Contributing
|
|
216
|
+
|
|
217
|
+
Contributions are welcome! Please check out [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## ๐ License
|
|
222
|
+
|
|
223
|
+
This project is licensed under the **MIT License** โ see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""
|
|
3
|
+
Module contains proxy class for JWT token encoding and decoding.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
import jwt
|
|
9
|
+
from .jwt_rules import JWTAuthenticationRules
|
|
10
|
+
from datetime import datetime, timezone, timedelta
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JsonWebToken(ABC):
|
|
14
|
+
"""Abstract base class for JWT encode/decode operations (Proxy pattern)."""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def encode_token(self, payload: dict, secret_key: str, algorithm: str) -> str:
|
|
18
|
+
"""Encode a payload into a JWT token string."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def decode_token(self, token: str, secret_key: str, algorithm: str) -> Optional[dict]:
|
|
23
|
+
"""Decode a JWT token string into a payload dict."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PyJWT(JsonWebToken):
|
|
28
|
+
"""Concrete JWT proxy using the PyJWT library."""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def rule_handler(self) -> JWTAuthenticationRules:
|
|
32
|
+
return JWTAuthenticationRules()
|
|
33
|
+
|
|
34
|
+
def required_payload(self, rule_name: str, token_type: str) -> dict[str, Any]:
|
|
35
|
+
"""Build the standard required payload fields (type + expiry)."""
|
|
36
|
+
token_lifetime: timedelta = self.rule_handler.get_token_lifetime(
|
|
37
|
+
rule_name, token_type
|
|
38
|
+
)
|
|
39
|
+
return {
|
|
40
|
+
"type": token_type,
|
|
41
|
+
"exp": datetime.now(timezone.utc) + token_lifetime,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def encode_token(
|
|
45
|
+
self,
|
|
46
|
+
payload: dict,
|
|
47
|
+
secret_key: str,
|
|
48
|
+
algorithm: str,
|
|
49
|
+
*args: Any,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Encode a payload dict into a signed JWT string."""
|
|
53
|
+
return jwt.encode(payload, key=secret_key, algorithm=algorithm, *args, **kwargs)
|
|
54
|
+
|
|
55
|
+
def decode_token(
|
|
56
|
+
self,
|
|
57
|
+
token: str,
|
|
58
|
+
secret_key: str,
|
|
59
|
+
algorithm: str,
|
|
60
|
+
*args: Any,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> Optional[dict]:
|
|
63
|
+
"""Decode and verify a JWT token."""
|
|
64
|
+
try:
|
|
65
|
+
return jwt.decode(
|
|
66
|
+
token, key=secret_key, algorithms=[algorithm], *args, **kwargs
|
|
67
|
+
)
|
|
68
|
+
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
|
|
69
|
+
return None
|