the37lab-authlib 0.1.1750837371__tar.gz → 0.1.1750840354__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.1750837371 → the37lab_authlib-0.1.1750840354}/PKG-INFO +2 -17
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/README.md +1 -16
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/pyproject.toml +1 -1
- the37lab_authlib-0.1.1750840354/src/the37lab_authlib/__init__.py +5 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/auth.py +60 -42
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/decorators.py +7 -1
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/PKG-INFO +2 -17
- the37lab_authlib-0.1.1750837371/src/the37lab_authlib/__init__.py +0 -5
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/setup.cfg +0 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/db.py +0 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/exceptions.py +0 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/models.py +0 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/requires.txt +0 -0
- {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/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.1750840354
|
|
4
4
|
Summary: Python SDK for the Authlib
|
|
5
5
|
Author-email: the37lab <info@the37lab.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -70,21 +70,8 @@ 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"
|
|
78
73
|
```
|
|
79
74
|
|
|
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
|
-
|
|
88
75
|
## Configuration
|
|
89
76
|
|
|
90
77
|
### Required Parameters
|
|
@@ -157,9 +144,7 @@ public using the `@auth.public_endpoint` decorator or
|
|
|
157
144
|
- Get redirect URL from `/api/v1/users/login/oauth`.
|
|
158
145
|
- Complete OAuth flow via `/api/v1/users/login/oauth2callback`.
|
|
159
146
|
4. **Protected Routes:**
|
|
160
|
-
-
|
|
161
|
-
The authenticated user can be accessed via `g.requesting_user`.
|
|
162
|
-
Use `@auth.require_auth()` to protect custom routes in your application.
|
|
147
|
+
- Use `@auth.require_auth()` decorator to protect Flask routes.
|
|
163
148
|
|
|
164
149
|
## User Object
|
|
165
150
|
|
|
@@ -53,21 +53,8 @@ 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"
|
|
61
56
|
```
|
|
62
57
|
|
|
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
|
-
|
|
71
58
|
## Configuration
|
|
72
59
|
|
|
73
60
|
### Required Parameters
|
|
@@ -140,9 +127,7 @@ public using the `@auth.public_endpoint` decorator or
|
|
|
140
127
|
- Get redirect URL from `/api/v1/users/login/oauth`.
|
|
141
128
|
- Complete OAuth flow via `/api/v1/users/login/oauth2callback`.
|
|
142
129
|
4. **Protected Routes:**
|
|
143
|
-
-
|
|
144
|
-
The authenticated user can be accessed via `g.requesting_user`.
|
|
145
|
-
Use `@auth.require_auth()` to protect custom routes in your application.
|
|
130
|
+
- Use `@auth.require_auth()` decorator to protect Flask routes.
|
|
146
131
|
|
|
147
132
|
## User Object
|
|
148
133
|
|
|
@@ -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.1750840354"
|
|
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.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/auth.py
RENAMED
|
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
|
|
|
5
5
|
from .db import Database
|
|
6
6
|
from .models import User, Role, ApiToken
|
|
7
7
|
from .exceptions import AuthError
|
|
8
|
+
from .decorators import public_endpoint
|
|
8
9
|
import uuid
|
|
9
10
|
import requests
|
|
10
11
|
import bcrypt
|
|
@@ -15,6 +16,17 @@ from functools import wraps
|
|
|
15
16
|
logging.basicConfig(level=logging.DEBUG)
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
19
|
+
def handle_auth_errors(f):
|
|
20
|
+
@wraps(f)
|
|
21
|
+
def decorated(*args, **kwargs):
|
|
22
|
+
try:
|
|
23
|
+
return f(*args, **kwargs)
|
|
24
|
+
except AuthError as e:
|
|
25
|
+
response = jsonify(e.to_dict())
|
|
26
|
+
response.status_code = e.status_code
|
|
27
|
+
return response
|
|
28
|
+
return decorated
|
|
29
|
+
|
|
18
30
|
class AuthManager:
|
|
19
31
|
def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer'):
|
|
20
32
|
logger.info("INITIALIZING AUTHMANAGER {} - {} - {}".format(db_dsn, jwt_secret, not app))
|
|
@@ -22,15 +34,6 @@ class AuthManager:
|
|
|
22
34
|
self.jwt_secret = jwt_secret
|
|
23
35
|
logger.debug(f"Initializing AuthManager with JWT secret: {jwt_secret[:5]}..." if jwt_secret else "No JWT secret provided")
|
|
24
36
|
self.oauth_config = oauth_config or {}
|
|
25
|
-
self.public_endpoints = {
|
|
26
|
-
'auth.login',
|
|
27
|
-
'auth.oauth_login',
|
|
28
|
-
'auth.oauth_callback',
|
|
29
|
-
'auth.refresh_token',
|
|
30
|
-
'auth.register',
|
|
31
|
-
'auth.get_roles'
|
|
32
|
-
}
|
|
33
|
-
self.bp = None
|
|
34
37
|
|
|
35
38
|
if app:
|
|
36
39
|
self.init_app(app)
|
|
@@ -122,17 +125,6 @@ class AuthManager:
|
|
|
122
125
|
|
|
123
126
|
return f(*args, **kwargs)
|
|
124
127
|
return decorated
|
|
125
|
-
|
|
126
|
-
def add_public_endpoint(self, endpoint):
|
|
127
|
-
"""Mark an endpoint as public so it bypasses authentication."""
|
|
128
|
-
self.public_endpoints.add(endpoint)
|
|
129
|
-
|
|
130
|
-
def public_endpoint(self, f):
|
|
131
|
-
"""Decorator to mark a view function as public."""
|
|
132
|
-
if self.bp:
|
|
133
|
-
endpoint = f"{self.bp.name}.{f.__name__}"
|
|
134
|
-
self.add_public_endpoint(endpoint)
|
|
135
|
-
return f
|
|
136
128
|
|
|
137
129
|
def init_app(self, app):
|
|
138
130
|
app.auth_manager = self
|
|
@@ -140,21 +132,22 @@ class AuthManager:
|
|
|
140
132
|
|
|
141
133
|
def create_blueprint(self):
|
|
142
134
|
bp = Blueprint('auth', __name__, url_prefix='/api/v1/users')
|
|
143
|
-
self.bp = bp
|
|
144
|
-
bp.public_endpoint = self.public_endpoint
|
|
145
|
-
|
|
146
|
-
@bp.errorhandler(AuthError)
|
|
147
|
-
def handle_auth_error(err):
|
|
148
|
-
response = jsonify(err.to_dict())
|
|
149
|
-
response.status_code = err.status_code
|
|
150
|
-
return response
|
|
151
135
|
|
|
152
136
|
@bp.before_request
|
|
153
137
|
def load_user():
|
|
154
|
-
|
|
155
|
-
|
|
138
|
+
view = current_app.view_functions.get(request.endpoint)
|
|
139
|
+
if getattr(view, '_auth_public', False):
|
|
140
|
+
return
|
|
141
|
+
try:
|
|
142
|
+
g.current_user = self._authenticate_request()
|
|
143
|
+
except AuthError as e:
|
|
144
|
+
response = jsonify(e.to_dict())
|
|
145
|
+
response.status_code = e.status_code
|
|
146
|
+
return response
|
|
156
147
|
|
|
157
148
|
@bp.route('/login', methods=['POST'])
|
|
149
|
+
@public_endpoint
|
|
150
|
+
@handle_auth_errors
|
|
158
151
|
def login():
|
|
159
152
|
data = request.get_json()
|
|
160
153
|
username = data.get('username')
|
|
@@ -189,6 +182,8 @@ class AuthManager:
|
|
|
189
182
|
})
|
|
190
183
|
|
|
191
184
|
@bp.route('/login/oauth', methods=['POST'])
|
|
185
|
+
@public_endpoint
|
|
186
|
+
@handle_auth_errors
|
|
192
187
|
def oauth_login():
|
|
193
188
|
provider = request.json.get('provider')
|
|
194
189
|
if provider not in self.oauth_config:
|
|
@@ -200,6 +195,8 @@ class AuthManager:
|
|
|
200
195
|
})
|
|
201
196
|
|
|
202
197
|
@bp.route('/login/oauth2callback')
|
|
198
|
+
@public_endpoint
|
|
199
|
+
@handle_auth_errors
|
|
203
200
|
def oauth_callback():
|
|
204
201
|
code = request.args.get('code')
|
|
205
202
|
provider = request.args.get('state')
|
|
@@ -216,22 +213,28 @@ class AuthManager:
|
|
|
216
213
|
return redirect(f"{frontend_url}/oauth-callback?token={token}&refresh_token={refresh_token}")
|
|
217
214
|
|
|
218
215
|
@bp.route('/login/profile')
|
|
216
|
+
@handle_auth_errors
|
|
219
217
|
def profile():
|
|
220
|
-
|
|
218
|
+
token = request.headers.get('Authorization', '').split(' ')[-1]
|
|
219
|
+
user = self.validate_token(token)
|
|
221
220
|
return jsonify(user)
|
|
222
221
|
|
|
223
222
|
@bp.route('/api-tokens', methods=['GET'])
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
@handle_auth_errors
|
|
224
|
+
@self.require_auth
|
|
225
|
+
def get_tokens(requesting_user):
|
|
226
|
+
tokens = self.get_user_api_tokens(requesting_user['id'])
|
|
226
227
|
return jsonify(tokens)
|
|
227
228
|
|
|
228
229
|
@bp.route('/api-tokens', methods=['POST'])
|
|
229
|
-
|
|
230
|
+
@handle_auth_errors
|
|
231
|
+
@self.require_auth
|
|
232
|
+
def create_token(requesting_user):
|
|
230
233
|
name = request.json.get('name')
|
|
231
234
|
expires_in_days = request.json.get('expires_in_days')
|
|
232
235
|
if not name:
|
|
233
236
|
raise AuthError('Token name is required', 400)
|
|
234
|
-
api_token = self.create_api_token(
|
|
237
|
+
api_token = self.create_api_token(requesting_user['id'], name, expires_in_days)
|
|
235
238
|
return jsonify({
|
|
236
239
|
'id': api_token.id,
|
|
237
240
|
'name': api_token.name,
|
|
@@ -241,6 +244,8 @@ class AuthManager:
|
|
|
241
244
|
})
|
|
242
245
|
|
|
243
246
|
@bp.route('/token-refresh', methods=['POST'])
|
|
247
|
+
@public_endpoint
|
|
248
|
+
@handle_auth_errors
|
|
244
249
|
def refresh_token():
|
|
245
250
|
refresh_token = request.json.get('refresh_token')
|
|
246
251
|
if not refresh_token:
|
|
@@ -265,16 +270,20 @@ class AuthManager:
|
|
|
265
270
|
raise AuthError('Invalid refresh token', 401)
|
|
266
271
|
|
|
267
272
|
@bp.route('/api-tokens', methods=['POST'])
|
|
268
|
-
|
|
273
|
+
@handle_auth_errors
|
|
274
|
+
@self.require_auth
|
|
275
|
+
def create_api_token(requesting_user):
|
|
269
276
|
name = request.json.get('name')
|
|
270
277
|
if not name:
|
|
271
278
|
raise AuthError('Token name required', 400)
|
|
272
279
|
|
|
273
|
-
token = self.create_api_token(
|
|
280
|
+
token = self.create_api_token(requesting_user['id'], name)
|
|
274
281
|
return jsonify({'token': token.token})
|
|
275
282
|
|
|
276
283
|
@bp.route('/api-tokens/validate', methods=['GET'])
|
|
277
|
-
|
|
284
|
+
@handle_auth_errors
|
|
285
|
+
@self.require_auth
|
|
286
|
+
def validate_api_token(requesting_user):
|
|
278
287
|
token = request.json.get('token')
|
|
279
288
|
if not token:
|
|
280
289
|
raise AuthError('No API token provided', 401)
|
|
@@ -284,7 +293,7 @@ class AuthManager:
|
|
|
284
293
|
cur.execute("""
|
|
285
294
|
SELECT * FROM api_tokens
|
|
286
295
|
WHERE user_id = %s AND id = %s
|
|
287
|
-
""", (
|
|
296
|
+
""", (requesting_user['id'], token))
|
|
288
297
|
api_token = cur.fetchone()
|
|
289
298
|
|
|
290
299
|
if not api_token:
|
|
@@ -305,7 +314,9 @@ class AuthManager:
|
|
|
305
314
|
return jsonify({'valid': True})
|
|
306
315
|
|
|
307
316
|
@bp.route('/api-tokens', methods=['DELETE'])
|
|
308
|
-
|
|
317
|
+
@handle_auth_errors
|
|
318
|
+
@self.require_auth
|
|
319
|
+
def delete_api_token(requesting_user):
|
|
309
320
|
token = request.json.get('token')
|
|
310
321
|
if not token:
|
|
311
322
|
raise AuthError('Token required', 400)
|
|
@@ -316,7 +327,7 @@ class AuthManager:
|
|
|
316
327
|
DELETE FROM api_tokens
|
|
317
328
|
WHERE user_id = %s AND id = %s
|
|
318
329
|
RETURNING id
|
|
319
|
-
""", (
|
|
330
|
+
""", (requesting_user['id'], token))
|
|
320
331
|
deleted_id = cur.fetchone()
|
|
321
332
|
if not deleted_id:
|
|
322
333
|
raise ValueError('Token not found or already deleted')
|
|
@@ -324,6 +335,8 @@ class AuthManager:
|
|
|
324
335
|
return jsonify({'deleted': True})
|
|
325
336
|
|
|
326
337
|
@bp.route('/register', methods=['POST'])
|
|
338
|
+
@public_endpoint
|
|
339
|
+
@handle_auth_errors
|
|
327
340
|
def register():
|
|
328
341
|
data = request.get_json()
|
|
329
342
|
|
|
@@ -362,6 +375,8 @@ class AuthManager:
|
|
|
362
375
|
return jsonify({'id': user.id}), 201
|
|
363
376
|
|
|
364
377
|
@bp.route('/roles', methods=['GET'])
|
|
378
|
+
@public_endpoint
|
|
379
|
+
@handle_auth_errors
|
|
365
380
|
def get_roles():
|
|
366
381
|
with self.db.get_cursor() as cur:
|
|
367
382
|
cur.execute("SELECT * FROM roles")
|
|
@@ -401,6 +416,8 @@ class AuthManager:
|
|
|
401
416
|
raise AuthError(str(e), 500)
|
|
402
417
|
|
|
403
418
|
def get_current_user(self):
|
|
419
|
+
if hasattr(g, 'current_user'):
|
|
420
|
+
return g.current_user
|
|
404
421
|
return self._authenticate_request()
|
|
405
422
|
|
|
406
423
|
def get_user_api_tokens(self, user_id):
|
|
@@ -455,6 +472,7 @@ class AuthManager:
|
|
|
455
472
|
return f'https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={scope}&state={state}'
|
|
456
473
|
raise AuthError('Invalid OAuth provider')
|
|
457
474
|
|
|
475
|
+
|
|
458
476
|
def _get_oauth_user_info(self, provider, code):
|
|
459
477
|
if provider == 'google':
|
|
460
478
|
client_id = self.oauth_config['google']['client_id']
|
|
@@ -516,4 +534,4 @@ class AuthManager:
|
|
|
516
534
|
user['real_name'] = userinfo.get('name', userinfo['email'])
|
|
517
535
|
|
|
518
536
|
return user
|
|
519
|
-
raise AuthError('Invalid OAuth provider')
|
|
537
|
+
raise AuthError('Invalid OAuth provider')
|
|
@@ -2,6 +2,12 @@ from functools import wraps
|
|
|
2
2
|
from flask import request, current_app, jsonify
|
|
3
3
|
from .exceptions import AuthError
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
def public_endpoint(f):
|
|
7
|
+
"""Mark an endpoint as public (no authentication required)."""
|
|
8
|
+
f._auth_public = True
|
|
9
|
+
return f
|
|
10
|
+
|
|
5
11
|
def require_auth(roles=None):
|
|
6
12
|
def decorator(f):
|
|
7
13
|
@wraps(f)
|
|
@@ -28,4 +34,4 @@ def require_auth(roles=None):
|
|
|
28
34
|
response.status_code = e.status_code
|
|
29
35
|
return response
|
|
30
36
|
return decorated
|
|
31
|
-
return decorator
|
|
37
|
+
return decorator
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: the37lab_authlib
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1750840354
|
|
4
4
|
Summary: Python SDK for the Authlib
|
|
5
5
|
Author-email: the37lab <info@the37lab.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -70,21 +70,8 @@ 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"
|
|
78
73
|
```
|
|
79
74
|
|
|
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
|
-
|
|
88
75
|
## Configuration
|
|
89
76
|
|
|
90
77
|
### Required Parameters
|
|
@@ -157,9 +144,7 @@ public using the `@auth.public_endpoint` decorator or
|
|
|
157
144
|
- Get redirect URL from `/api/v1/users/login/oauth`.
|
|
158
145
|
- Complete OAuth flow via `/api/v1/users/login/oauth2callback`.
|
|
159
146
|
4. **Protected Routes:**
|
|
160
|
-
-
|
|
161
|
-
The authenticated user can be accessed via `g.requesting_user`.
|
|
162
|
-
Use `@auth.require_auth()` to protect custom routes in your application.
|
|
147
|
+
- Use `@auth.require_auth()` decorator to protect Flask routes.
|
|
163
148
|
|
|
164
149
|
## User Object
|
|
165
150
|
|
|
File without changes
|
{the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/db.py
RENAMED
|
File without changes
|
|
File without changes
|
{the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|