the37lab-authlib 0.1.1750840398__tar.gz → 0.1.1750844514__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.
Potentially problematic release.
This version of the37lab-authlib might be problematic. Click here for more details.
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/PKG-INFO +17 -2
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/README.md +16 -1
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/pyproject.toml +1 -1
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/auth.py +51 -43
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/PKG-INFO +17 -2
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/setup.cfg +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/__init__.py +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/db.py +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/decorators.py +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/exceptions.py +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/models.py +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/requires.txt +0 -0
- {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: the37lab_authlib
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1750844514
|
|
4
4
|
Summary: Python SDK for the Authlib
|
|
5
5
|
Author-email: the37lab <info@the37lab.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -70,8 +70,21 @@ auth = AuthManager(
|
|
|
70
70
|
@auth.require_auth(roles=["admin"])
|
|
71
71
|
def protected_route():
|
|
72
72
|
return "Protected content"
|
|
73
|
+
|
|
74
|
+
@app.route("/public")
|
|
75
|
+
@auth.public_endpoint
|
|
76
|
+
def custom_public_route():
|
|
77
|
+
return "Public content"
|
|
73
78
|
```
|
|
74
79
|
|
|
80
|
+
`AuthManager`'s blueprint now registers a global error handler for
|
|
81
|
+
`AuthError` and authenticates requests for all of its routes by default.
|
|
82
|
+
Authenticated users are made available as `flask.g.requesting_user`.
|
|
83
|
+
Only the login, OAuth, token refresh, registration and role listing
|
|
84
|
+
endpoints are exempt from this check. Additional routes can be marked as
|
|
85
|
+
public using the `@auth.public_endpoint` decorator or
|
|
86
|
+
`auth.add_public_endpoint("auth.some_endpoint")`.
|
|
87
|
+
|
|
75
88
|
## Configuration
|
|
76
89
|
|
|
77
90
|
### Required Parameters
|
|
@@ -144,7 +157,9 @@ def protected_route():
|
|
|
144
157
|
- Get redirect URL from `/api/v1/users/login/oauth`.
|
|
145
158
|
- Complete OAuth flow via `/api/v1/users/login/oauth2callback`.
|
|
146
159
|
4. **Protected Routes:**
|
|
147
|
-
-
|
|
160
|
+
- All routes inside the provided blueprint are authenticated by default.
|
|
161
|
+
The authenticated user can be accessed via `g.requesting_user`.
|
|
162
|
+
Use `@auth.require_auth()` to protect custom routes in your application.
|
|
148
163
|
|
|
149
164
|
## User Object
|
|
150
165
|
|
|
@@ -53,8 +53,21 @@ auth = AuthManager(
|
|
|
53
53
|
@auth.require_auth(roles=["admin"])
|
|
54
54
|
def protected_route():
|
|
55
55
|
return "Protected content"
|
|
56
|
+
|
|
57
|
+
@app.route("/public")
|
|
58
|
+
@auth.public_endpoint
|
|
59
|
+
def custom_public_route():
|
|
60
|
+
return "Public content"
|
|
56
61
|
```
|
|
57
62
|
|
|
63
|
+
`AuthManager`'s blueprint now registers a global error handler for
|
|
64
|
+
`AuthError` and authenticates requests for all of its routes by default.
|
|
65
|
+
Authenticated users are made available as `flask.g.requesting_user`.
|
|
66
|
+
Only the login, OAuth, token refresh, registration and role listing
|
|
67
|
+
endpoints are exempt from this check. Additional routes can be marked as
|
|
68
|
+
public using the `@auth.public_endpoint` decorator or
|
|
69
|
+
`auth.add_public_endpoint("auth.some_endpoint")`.
|
|
70
|
+
|
|
58
71
|
## Configuration
|
|
59
72
|
|
|
60
73
|
### Required Parameters
|
|
@@ -127,7 +140,9 @@ def protected_route():
|
|
|
127
140
|
- Get redirect URL from `/api/v1/users/login/oauth`.
|
|
128
141
|
- Complete OAuth flow via `/api/v1/users/login/oauth2callback`.
|
|
129
142
|
4. **Protected Routes:**
|
|
130
|
-
-
|
|
143
|
+
- All routes inside the provided blueprint are authenticated by default.
|
|
144
|
+
The authenticated user can be accessed via `g.requesting_user`.
|
|
145
|
+
Use `@auth.require_auth()` to protect custom routes in your application.
|
|
131
146
|
|
|
132
147
|
## User Object
|
|
133
148
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "the37lab_authlib"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1750844514"
|
|
8
8
|
description = "Python SDK for the Authlib"
|
|
9
9
|
authors = [{name = "the37lab", email = "info@the37lab.com"}]
|
|
10
10
|
dependencies = ["flask", "psycopg2-binary", "pyjwt", "python-dotenv", "requests", "authlib", "bcrypt"]
|
{the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/auth.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from flask import Blueprint, request, jsonify, current_app, url_for, redirect
|
|
2
|
+
from flask import Blueprint, request, jsonify, current_app, url_for, redirect, g
|
|
3
3
|
import jwt
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
5
|
from .db import Database
|
|
@@ -15,24 +15,20 @@ from functools import wraps
|
|
|
15
15
|
logging.basicConfig(level=logging.DEBUG)
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
|
-
def handle_auth_errors(f):
|
|
19
|
-
@wraps(f)
|
|
20
|
-
def decorated(*args, **kwargs):
|
|
21
|
-
try:
|
|
22
|
-
return f(*args, **kwargs)
|
|
23
|
-
except AuthError as e:
|
|
24
|
-
response = jsonify(e.to_dict())
|
|
25
|
-
response.status_code = e.status_code
|
|
26
|
-
return response
|
|
27
|
-
return decorated
|
|
28
|
-
|
|
29
18
|
class AuthManager:
|
|
30
19
|
def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer'):
|
|
31
|
-
logger.info("INITIALIZING AUTHMANAGER {} - {} - {}".format(db_dsn, jwt_secret, not app))
|
|
32
20
|
self.db = Database(db_dsn, id_type=id_type) if db_dsn else None
|
|
33
21
|
self.jwt_secret = jwt_secret
|
|
34
|
-
logger.debug(f"Initializing AuthManager with JWT secret: {jwt_secret[:5]}..." if jwt_secret else "No JWT secret provided")
|
|
35
22
|
self.oauth_config = oauth_config or {}
|
|
23
|
+
self.public_endpoints = {
|
|
24
|
+
'auth.login',
|
|
25
|
+
'auth.oauth_login',
|
|
26
|
+
'auth.oauth_callback',
|
|
27
|
+
'auth.refresh_token',
|
|
28
|
+
'auth.register',
|
|
29
|
+
'auth.get_roles'
|
|
30
|
+
}
|
|
31
|
+
self.bp = None
|
|
36
32
|
|
|
37
33
|
if app:
|
|
38
34
|
self.init_app(app)
|
|
@@ -124,6 +120,23 @@ class AuthManager:
|
|
|
124
120
|
|
|
125
121
|
return f(*args, **kwargs)
|
|
126
122
|
return decorated
|
|
123
|
+
|
|
124
|
+
def add_public_endpoint(self, endpoint):
|
|
125
|
+
"""Mark an endpoint as public so it bypasses authentication."""
|
|
126
|
+
self.public_endpoints.add(endpoint)
|
|
127
|
+
|
|
128
|
+
def public_endpoint(self, f):
|
|
129
|
+
"""Decorator to mark a view function as public."""
|
|
130
|
+
# Always register the bare function name so application level routes
|
|
131
|
+
# are exempt from authentication checks.
|
|
132
|
+
self.add_public_endpoint(f.__name__)
|
|
133
|
+
|
|
134
|
+
# If a blueprint is active, also register the blueprint-prefixed name
|
|
135
|
+
# used by Flask for endpoint identification.
|
|
136
|
+
if self.bp:
|
|
137
|
+
endpoint = f"{self.bp.name}.{f.__name__}"
|
|
138
|
+
self.add_public_endpoint(endpoint)
|
|
139
|
+
return f
|
|
127
140
|
|
|
128
141
|
def init_app(self, app):
|
|
129
142
|
app.auth_manager = self
|
|
@@ -136,9 +149,21 @@ class AuthManager:
|
|
|
136
149
|
|
|
137
150
|
def create_blueprint(self):
|
|
138
151
|
bp = Blueprint('auth', __name__, url_prefix='/api/v1/users')
|
|
152
|
+
self.bp = bp
|
|
153
|
+
bp.public_endpoint = self.public_endpoint
|
|
154
|
+
|
|
155
|
+
@bp.errorhandler(AuthError)
|
|
156
|
+
def handle_auth_error(err):
|
|
157
|
+
response = jsonify(err.to_dict())
|
|
158
|
+
response.status_code = err.status_code
|
|
159
|
+
return response
|
|
160
|
+
|
|
161
|
+
@bp.before_request
|
|
162
|
+
def load_user():
|
|
163
|
+
if request.endpoint not in self.public_endpoints:
|
|
164
|
+
g.requesting_user = self._authenticate_request()
|
|
139
165
|
|
|
140
166
|
@bp.route('/login', methods=['POST'])
|
|
141
|
-
@handle_auth_errors
|
|
142
167
|
def login():
|
|
143
168
|
data = request.get_json()
|
|
144
169
|
username = data.get('username')
|
|
@@ -173,7 +198,6 @@ class AuthManager:
|
|
|
173
198
|
})
|
|
174
199
|
|
|
175
200
|
@bp.route('/login/oauth', methods=['POST'])
|
|
176
|
-
@handle_auth_errors
|
|
177
201
|
def oauth_login():
|
|
178
202
|
provider = request.json.get('provider')
|
|
179
203
|
if provider not in self.oauth_config:
|
|
@@ -185,7 +209,6 @@ class AuthManager:
|
|
|
185
209
|
})
|
|
186
210
|
|
|
187
211
|
@bp.route('/login/oauth2callback')
|
|
188
|
-
@handle_auth_errors
|
|
189
212
|
def oauth_callback():
|
|
190
213
|
code = request.args.get('code')
|
|
191
214
|
provider = request.args.get('state')
|
|
@@ -202,28 +225,22 @@ class AuthManager:
|
|
|
202
225
|
return redirect(f"{frontend_url}/oauth-callback?token={token}&refresh_token={refresh_token}")
|
|
203
226
|
|
|
204
227
|
@bp.route('/login/profile')
|
|
205
|
-
@handle_auth_errors
|
|
206
228
|
def profile():
|
|
207
|
-
|
|
208
|
-
user = self.validate_token(token)
|
|
229
|
+
user = g.requesting_user
|
|
209
230
|
return jsonify(user)
|
|
210
231
|
|
|
211
232
|
@bp.route('/api-tokens', methods=['GET'])
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def get_tokens(requesting_user):
|
|
215
|
-
tokens = self.get_user_api_tokens(requesting_user['id'])
|
|
233
|
+
def get_tokens():
|
|
234
|
+
tokens = self.get_user_api_tokens(g.requesting_user['id'])
|
|
216
235
|
return jsonify(tokens)
|
|
217
236
|
|
|
218
237
|
@bp.route('/api-tokens', methods=['POST'])
|
|
219
|
-
|
|
220
|
-
@self.require_auth
|
|
221
|
-
def create_token(requesting_user):
|
|
238
|
+
def create_token():
|
|
222
239
|
name = request.json.get('name')
|
|
223
240
|
expires_in_days = request.json.get('expires_in_days')
|
|
224
241
|
if not name:
|
|
225
242
|
raise AuthError('Token name is required', 400)
|
|
226
|
-
api_token = self.create_api_token(requesting_user['id'], name, expires_in_days)
|
|
243
|
+
api_token = self.create_api_token(g.requesting_user['id'], name, expires_in_days)
|
|
227
244
|
return jsonify({
|
|
228
245
|
'id': api_token.id,
|
|
229
246
|
'name': api_token.name,
|
|
@@ -233,7 +250,6 @@ class AuthManager:
|
|
|
233
250
|
})
|
|
234
251
|
|
|
235
252
|
@bp.route('/token-refresh', methods=['POST'])
|
|
236
|
-
@handle_auth_errors
|
|
237
253
|
def refresh_token():
|
|
238
254
|
refresh_token = request.json.get('refresh_token')
|
|
239
255
|
if not refresh_token:
|
|
@@ -258,20 +274,16 @@ class AuthManager:
|
|
|
258
274
|
raise AuthError('Invalid refresh token', 401)
|
|
259
275
|
|
|
260
276
|
@bp.route('/api-tokens', methods=['POST'])
|
|
261
|
-
|
|
262
|
-
@self.require_auth
|
|
263
|
-
def create_api_token(requesting_user):
|
|
277
|
+
def create_api_token():
|
|
264
278
|
name = request.json.get('name')
|
|
265
279
|
if not name:
|
|
266
280
|
raise AuthError('Token name required', 400)
|
|
267
281
|
|
|
268
|
-
token = self.create_api_token(requesting_user['id'], name)
|
|
282
|
+
token = self.create_api_token(g.requesting_user['id'], name)
|
|
269
283
|
return jsonify({'token': token.token})
|
|
270
284
|
|
|
271
285
|
@bp.route('/api-tokens/validate', methods=['GET'])
|
|
272
|
-
|
|
273
|
-
@self.require_auth
|
|
274
|
-
def validate_api_token(requesting_user):
|
|
286
|
+
def validate_api_token():
|
|
275
287
|
token = request.json.get('token')
|
|
276
288
|
if not token:
|
|
277
289
|
raise AuthError('No API token provided', 401)
|
|
@@ -281,7 +293,7 @@ class AuthManager:
|
|
|
281
293
|
cur.execute("""
|
|
282
294
|
SELECT * FROM api_tokens
|
|
283
295
|
WHERE user_id = %s AND id = %s
|
|
284
|
-
""", (requesting_user['id'], token))
|
|
296
|
+
""", (g.requesting_user['id'], token))
|
|
285
297
|
api_token = cur.fetchone()
|
|
286
298
|
|
|
287
299
|
if not api_token:
|
|
@@ -302,9 +314,7 @@ class AuthManager:
|
|
|
302
314
|
return jsonify({'valid': True})
|
|
303
315
|
|
|
304
316
|
@bp.route('/api-tokens', methods=['DELETE'])
|
|
305
|
-
|
|
306
|
-
@self.require_auth
|
|
307
|
-
def delete_api_token(requesting_user):
|
|
317
|
+
def delete_api_token():
|
|
308
318
|
token = request.json.get('token')
|
|
309
319
|
if not token:
|
|
310
320
|
raise AuthError('Token required', 400)
|
|
@@ -315,7 +325,7 @@ class AuthManager:
|
|
|
315
325
|
DELETE FROM api_tokens
|
|
316
326
|
WHERE user_id = %s AND id = %s
|
|
317
327
|
RETURNING id
|
|
318
|
-
""", (requesting_user['id'], token))
|
|
328
|
+
""", (g.requesting_user['id'], token))
|
|
319
329
|
deleted_id = cur.fetchone()
|
|
320
330
|
if not deleted_id:
|
|
321
331
|
raise ValueError('Token not found or already deleted')
|
|
@@ -323,7 +333,6 @@ class AuthManager:
|
|
|
323
333
|
return jsonify({'deleted': True})
|
|
324
334
|
|
|
325
335
|
@bp.route('/register', methods=['POST'])
|
|
326
|
-
@handle_auth_errors
|
|
327
336
|
def register():
|
|
328
337
|
data = request.get_json()
|
|
329
338
|
|
|
@@ -362,7 +371,6 @@ class AuthManager:
|
|
|
362
371
|
return jsonify({'id': user.id}), 201
|
|
363
372
|
|
|
364
373
|
@bp.route('/roles', methods=['GET'])
|
|
365
|
-
@handle_auth_errors
|
|
366
374
|
def get_roles():
|
|
367
375
|
with self.db.get_cursor() as cur:
|
|
368
376
|
cur.execute("SELECT * FROM roles")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: the37lab_authlib
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1750844514
|
|
4
4
|
Summary: Python SDK for the Authlib
|
|
5
5
|
Author-email: the37lab <info@the37lab.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -70,8 +70,21 @@ auth = AuthManager(
|
|
|
70
70
|
@auth.require_auth(roles=["admin"])
|
|
71
71
|
def protected_route():
|
|
72
72
|
return "Protected content"
|
|
73
|
+
|
|
74
|
+
@app.route("/public")
|
|
75
|
+
@auth.public_endpoint
|
|
76
|
+
def custom_public_route():
|
|
77
|
+
return "Public content"
|
|
73
78
|
```
|
|
74
79
|
|
|
80
|
+
`AuthManager`'s blueprint now registers a global error handler for
|
|
81
|
+
`AuthError` and authenticates requests for all of its routes by default.
|
|
82
|
+
Authenticated users are made available as `flask.g.requesting_user`.
|
|
83
|
+
Only the login, OAuth, token refresh, registration and role listing
|
|
84
|
+
endpoints are exempt from this check. Additional routes can be marked as
|
|
85
|
+
public using the `@auth.public_endpoint` decorator or
|
|
86
|
+
`auth.add_public_endpoint("auth.some_endpoint")`.
|
|
87
|
+
|
|
75
88
|
## Configuration
|
|
76
89
|
|
|
77
90
|
### Required Parameters
|
|
@@ -144,7 +157,9 @@ def protected_route():
|
|
|
144
157
|
- Get redirect URL from `/api/v1/users/login/oauth`.
|
|
145
158
|
- Complete OAuth flow via `/api/v1/users/login/oauth2callback`.
|
|
146
159
|
4. **Protected Routes:**
|
|
147
|
-
-
|
|
160
|
+
- All routes inside the provided blueprint are authenticated by default.
|
|
161
|
+
The authenticated user can be accessed via `g.requesting_user`.
|
|
162
|
+
Use `@auth.require_auth()` to protect custom routes in your application.
|
|
148
163
|
|
|
149
164
|
## User Object
|
|
150
165
|
|
|
File without changes
|
{the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/__init__.py
RENAMED
|
File without changes
|
{the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/db.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|