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 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
- token = request.headers.get('Authorization', '').split(' ')[-1]
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
- @handle_auth_errors
208
- @self.require_auth
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
- @handle_auth_errors
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
- @handle_auth_errors
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
- @handle_auth_errors
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
- @handle_auth_errors
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: the37lab_authlib
3
- Version: 0.1.1750840415
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, require_auth
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
- - Use `@require_auth()` decorator to protect Flask routes.
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
 
@@ -1,10 +1,10 @@
1
1
  the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
2
- the37lab_authlib/auth.py,sha256=dQkE6z9GZZpnl0nfqulcveho8W5lM95XUBLmtE-5JIc,20660
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.1750840415.dist-info/METADATA,sha256=VVfAVdbm_v4u0-7_f7V0mvcumU-iJZPzkUgRqgfP2c4,5645
8
- the37lab_authlib-0.1.1750840415.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- the37lab_authlib-0.1.1750840415.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
- the37lab_authlib-0.1.1750840415.dist-info/RECORD,,
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,,