the37lab-authlib 0.1.1750840415__py3-none-any.whl → 0.1.1750844514__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.
Potentially problematic release.
This version of the37lab-authlib might be problematic. Click here for more details.
- the37lab_authlib/auth.py +56 -43
- {the37lab_authlib-0.1.1750840415.dist-info → the37lab_authlib-0.1.1750844514.dist-info}/METADATA +19 -4
- {the37lab_authlib-0.1.1750840415.dist-info → the37lab_authlib-0.1.1750844514.dist-info}/RECORD +5 -5
- {the37lab_authlib-0.1.1750840415.dist-info → the37lab_authlib-0.1.1750844514.dist-info}/WHEEL +0 -0
- {the37lab_authlib-0.1.1750840415.dist-info → the37lab_authlib-0.1.1750844514.dist-info}/top_level.txt +0 -0
the37lab_authlib/auth.py
CHANGED
|
@@ -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,16 +120,50 @@ 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
|
|
130
143
|
app.register_blueprint(self.create_blueprint())
|
|
144
|
+
@app.errorhandler(AuthError)
|
|
145
|
+
def handle_auth_error(e):
|
|
146
|
+
response = jsonify(e.to_dict())
|
|
147
|
+
response.status_code = e.status_code
|
|
148
|
+
return response
|
|
131
149
|
|
|
132
150
|
def create_blueprint(self):
|
|
133
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()
|
|
134
165
|
|
|
135
166
|
@bp.route('/login', methods=['POST'])
|
|
136
|
-
@handle_auth_errors
|
|
137
167
|
def login():
|
|
138
168
|
data = request.get_json()
|
|
139
169
|
username = data.get('username')
|
|
@@ -168,7 +198,6 @@ class AuthManager:
|
|
|
168
198
|
})
|
|
169
199
|
|
|
170
200
|
@bp.route('/login/oauth', methods=['POST'])
|
|
171
|
-
@handle_auth_errors
|
|
172
201
|
def oauth_login():
|
|
173
202
|
provider = request.json.get('provider')
|
|
174
203
|
if provider not in self.oauth_config:
|
|
@@ -180,7 +209,6 @@ class AuthManager:
|
|
|
180
209
|
})
|
|
181
210
|
|
|
182
211
|
@bp.route('/login/oauth2callback')
|
|
183
|
-
@handle_auth_errors
|
|
184
212
|
def oauth_callback():
|
|
185
213
|
code = request.args.get('code')
|
|
186
214
|
provider = request.args.get('state')
|
|
@@ -197,28 +225,22 @@ class AuthManager:
|
|
|
197
225
|
return redirect(f"{frontend_url}/oauth-callback?token={token}&refresh_token={refresh_token}")
|
|
198
226
|
|
|
199
227
|
@bp.route('/login/profile')
|
|
200
|
-
@handle_auth_errors
|
|
201
228
|
def profile():
|
|
202
|
-
|
|
203
|
-
user = self.validate_token(token)
|
|
229
|
+
user = g.requesting_user
|
|
204
230
|
return jsonify(user)
|
|
205
231
|
|
|
206
232
|
@bp.route('/api-tokens', methods=['GET'])
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def get_tokens(requesting_user):
|
|
210
|
-
tokens = self.get_user_api_tokens(requesting_user['id'])
|
|
233
|
+
def get_tokens():
|
|
234
|
+
tokens = self.get_user_api_tokens(g.requesting_user['id'])
|
|
211
235
|
return jsonify(tokens)
|
|
212
236
|
|
|
213
237
|
@bp.route('/api-tokens', methods=['POST'])
|
|
214
|
-
|
|
215
|
-
@self.require_auth
|
|
216
|
-
def create_token(requesting_user):
|
|
238
|
+
def create_token():
|
|
217
239
|
name = request.json.get('name')
|
|
218
240
|
expires_in_days = request.json.get('expires_in_days')
|
|
219
241
|
if not name:
|
|
220
242
|
raise AuthError('Token name is required', 400)
|
|
221
|
-
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)
|
|
222
244
|
return jsonify({
|
|
223
245
|
'id': api_token.id,
|
|
224
246
|
'name': api_token.name,
|
|
@@ -228,7 +250,6 @@ class AuthManager:
|
|
|
228
250
|
})
|
|
229
251
|
|
|
230
252
|
@bp.route('/token-refresh', methods=['POST'])
|
|
231
|
-
@handle_auth_errors
|
|
232
253
|
def refresh_token():
|
|
233
254
|
refresh_token = request.json.get('refresh_token')
|
|
234
255
|
if not refresh_token:
|
|
@@ -253,20 +274,16 @@ class AuthManager:
|
|
|
253
274
|
raise AuthError('Invalid refresh token', 401)
|
|
254
275
|
|
|
255
276
|
@bp.route('/api-tokens', methods=['POST'])
|
|
256
|
-
|
|
257
|
-
@self.require_auth
|
|
258
|
-
def create_api_token(requesting_user):
|
|
277
|
+
def create_api_token():
|
|
259
278
|
name = request.json.get('name')
|
|
260
279
|
if not name:
|
|
261
280
|
raise AuthError('Token name required', 400)
|
|
262
281
|
|
|
263
|
-
token = self.create_api_token(requesting_user['id'], name)
|
|
282
|
+
token = self.create_api_token(g.requesting_user['id'], name)
|
|
264
283
|
return jsonify({'token': token.token})
|
|
265
284
|
|
|
266
285
|
@bp.route('/api-tokens/validate', methods=['GET'])
|
|
267
|
-
|
|
268
|
-
@self.require_auth
|
|
269
|
-
def validate_api_token(requesting_user):
|
|
286
|
+
def validate_api_token():
|
|
270
287
|
token = request.json.get('token')
|
|
271
288
|
if not token:
|
|
272
289
|
raise AuthError('No API token provided', 401)
|
|
@@ -276,7 +293,7 @@ class AuthManager:
|
|
|
276
293
|
cur.execute("""
|
|
277
294
|
SELECT * FROM api_tokens
|
|
278
295
|
WHERE user_id = %s AND id = %s
|
|
279
|
-
""", (requesting_user['id'], token))
|
|
296
|
+
""", (g.requesting_user['id'], token))
|
|
280
297
|
api_token = cur.fetchone()
|
|
281
298
|
|
|
282
299
|
if not api_token:
|
|
@@ -297,9 +314,7 @@ class AuthManager:
|
|
|
297
314
|
return jsonify({'valid': True})
|
|
298
315
|
|
|
299
316
|
@bp.route('/api-tokens', methods=['DELETE'])
|
|
300
|
-
|
|
301
|
-
@self.require_auth
|
|
302
|
-
def delete_api_token(requesting_user):
|
|
317
|
+
def delete_api_token():
|
|
303
318
|
token = request.json.get('token')
|
|
304
319
|
if not token:
|
|
305
320
|
raise AuthError('Token required', 400)
|
|
@@ -310,7 +325,7 @@ class AuthManager:
|
|
|
310
325
|
DELETE FROM api_tokens
|
|
311
326
|
WHERE user_id = %s AND id = %s
|
|
312
327
|
RETURNING id
|
|
313
|
-
""", (requesting_user['id'], token))
|
|
328
|
+
""", (g.requesting_user['id'], token))
|
|
314
329
|
deleted_id = cur.fetchone()
|
|
315
330
|
if not deleted_id:
|
|
316
331
|
raise ValueError('Token not found or already deleted')
|
|
@@ -318,7 +333,6 @@ class AuthManager:
|
|
|
318
333
|
return jsonify({'deleted': True})
|
|
319
334
|
|
|
320
335
|
@bp.route('/register', methods=['POST'])
|
|
321
|
-
@handle_auth_errors
|
|
322
336
|
def register():
|
|
323
337
|
data = request.get_json()
|
|
324
338
|
|
|
@@ -357,7 +371,6 @@ class AuthManager:
|
|
|
357
371
|
return jsonify({'id': user.id}), 201
|
|
358
372
|
|
|
359
373
|
@bp.route('/roles', methods=['GET'])
|
|
360
|
-
@handle_auth_errors
|
|
361
374
|
def get_roles():
|
|
362
375
|
with self.db.get_cursor() as cur:
|
|
363
376
|
cur.execute("SELECT * FROM roles")
|
{the37lab_authlib-0.1.1750840415.dist-info → the37lab_authlib-0.1.1750844514.dist-info}/METADATA
RENAMED
|
@@ -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
|
|
@@ -50,7 +50,7 @@ pip install -e .
|
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
52
|
from flask import Flask
|
|
53
|
-
from authlib import AuthManager
|
|
53
|
+
from authlib import AuthManager
|
|
54
54
|
|
|
55
55
|
app = Flask(__name__)
|
|
56
56
|
|
|
@@ -67,11 +67,24 @@ auth = AuthManager(
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
@app.route("/protected")
|
|
70
|
-
@require_auth(roles=["admin"])
|
|
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
|
|
{the37lab_authlib-0.1.1750840415.dist-info → the37lab_authlib-0.1.1750844514.dist-info}/RECORD
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
|
|
2
|
-
the37lab_authlib/auth.py,sha256=
|
|
2
|
+
the37lab_authlib/auth.py,sha256=Djqm_kwVwMO2VhFoGkHptejw66WRp787twS0GWRwwM4,21102
|
|
3
3
|
the37lab_authlib/db.py,sha256=fTXxnfju0lmbFGPVbXpTMeDmJMeBgURVZTndyxyRyCc,2734
|
|
4
4
|
the37lab_authlib/decorators.py,sha256=AEQfix31fHUZvhEZd4Ud8Zh2KBGjV6O_braiPL-BU7w,1219
|
|
5
5
|
the37lab_authlib/exceptions.py,sha256=mdplK5sKNtagPAzSGq5NGsrQ4r-k03DKJBKx6myWwZc,317
|
|
6
6
|
the37lab_authlib/models.py,sha256=-PlvQlHGIsSdrH0H9Cdh_vTPlltGV8G1Z1mmGQvAg9Y,3422
|
|
7
|
-
the37lab_authlib-0.1.
|
|
8
|
-
the37lab_authlib-0.1.
|
|
9
|
-
the37lab_authlib-0.1.
|
|
10
|
-
the37lab_authlib-0.1.
|
|
7
|
+
the37lab_authlib-0.1.1750844514.dist-info/METADATA,sha256=sxPtONgXu1hIGi1UvtH9rPT642oK3jdIT1OTBU2Uu4w,6352
|
|
8
|
+
the37lab_authlib-0.1.1750844514.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
the37lab_authlib-0.1.1750844514.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
|
|
10
|
+
the37lab_authlib-0.1.1750844514.dist-info/RECORD,,
|
{the37lab_authlib-0.1.1750840415.dist-info → the37lab_authlib-0.1.1750844514.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|