the37lab-authlib 0.1.1750187527__py3-none-any.whl → 0.1.1750837371__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,17 +15,6 @@ 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
20
  logger.info("INITIALIZING AUTHMANAGER {} - {} - {}".format(db_dsn, jwt_secret, not app))
@@ -33,6 +22,15 @@ class AuthManager:
33
22
  self.jwt_secret = jwt_secret
34
23
  logger.debug(f"Initializing AuthManager with JWT secret: {jwt_secret[:5]}..." if jwt_secret else "No JWT secret provided")
35
24
  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
36
34
 
37
35
  if app:
38
36
  self.init_app(app)
@@ -124,6 +122,17 @@ class AuthManager:
124
122
 
125
123
  return f(*args, **kwargs)
126
124
  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
127
136
 
128
137
  def init_app(self, app):
129
138
  app.auth_manager = self
@@ -131,9 +140,21 @@ class AuthManager:
131
140
 
132
141
  def create_blueprint(self):
133
142
  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
+
152
+ @bp.before_request
153
+ def load_user():
154
+ if request.endpoint not in self.public_endpoints:
155
+ g.requesting_user = self._authenticate_request()
134
156
 
135
157
  @bp.route('/login', methods=['POST'])
136
- @handle_auth_errors
137
158
  def login():
138
159
  data = request.get_json()
139
160
  username = data.get('username')
@@ -168,7 +189,6 @@ class AuthManager:
168
189
  })
169
190
 
170
191
  @bp.route('/login/oauth', methods=['POST'])
171
- @handle_auth_errors
172
192
  def oauth_login():
173
193
  provider = request.json.get('provider')
174
194
  if provider not in self.oauth_config:
@@ -180,7 +200,6 @@ class AuthManager:
180
200
  })
181
201
 
182
202
  @bp.route('/login/oauth2callback')
183
- @handle_auth_errors
184
203
  def oauth_callback():
185
204
  code = request.args.get('code')
186
205
  provider = request.args.get('state')
@@ -197,28 +216,22 @@ class AuthManager:
197
216
  return redirect(f"{frontend_url}/oauth-callback?token={token}&refresh_token={refresh_token}")
198
217
 
199
218
  @bp.route('/login/profile')
200
- @handle_auth_errors
201
219
  def profile():
202
- token = request.headers.get('Authorization', '').split(' ')[-1]
203
- user = self.validate_token(token)
220
+ user = g.requesting_user
204
221
  return jsonify(user)
205
222
 
206
223
  @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'])
224
+ def get_tokens():
225
+ tokens = self.get_user_api_tokens(g.requesting_user['id'])
211
226
  return jsonify(tokens)
212
227
 
213
228
  @bp.route('/api-tokens', methods=['POST'])
214
- @handle_auth_errors
215
- @self.require_auth
216
- def create_token(requesting_user):
229
+ def create_token():
217
230
  name = request.json.get('name')
218
231
  expires_in_days = request.json.get('expires_in_days')
219
232
  if not name:
220
233
  raise AuthError('Token name is required', 400)
221
- api_token = self.create_api_token(requesting_user['id'], name, expires_in_days)
234
+ api_token = self.create_api_token(g.requesting_user['id'], name, expires_in_days)
222
235
  return jsonify({
223
236
  'id': api_token.id,
224
237
  'name': api_token.name,
@@ -228,7 +241,6 @@ class AuthManager:
228
241
  })
229
242
 
230
243
  @bp.route('/token-refresh', methods=['POST'])
231
- @handle_auth_errors
232
244
  def refresh_token():
233
245
  refresh_token = request.json.get('refresh_token')
234
246
  if not refresh_token:
@@ -253,20 +265,16 @@ class AuthManager:
253
265
  raise AuthError('Invalid refresh token', 401)
254
266
 
255
267
  @bp.route('/api-tokens', methods=['POST'])
256
- @handle_auth_errors
257
- @self.require_auth
258
- def create_api_token(requesting_user):
268
+ def create_api_token():
259
269
  name = request.json.get('name')
260
270
  if not name:
261
271
  raise AuthError('Token name required', 400)
262
272
 
263
- token = self.create_api_token(requesting_user['id'], name)
273
+ token = self.create_api_token(g.requesting_user['id'], name)
264
274
  return jsonify({'token': token.token})
265
275
 
266
276
  @bp.route('/api-tokens/validate', methods=['GET'])
267
- @handle_auth_errors
268
- @self.require_auth
269
- def validate_api_token(requesting_user):
277
+ def validate_api_token():
270
278
  token = request.json.get('token')
271
279
  if not token:
272
280
  raise AuthError('No API token provided', 401)
@@ -276,7 +284,7 @@ class AuthManager:
276
284
  cur.execute("""
277
285
  SELECT * FROM api_tokens
278
286
  WHERE user_id = %s AND id = %s
279
- """, (requesting_user['id'], token))
287
+ """, (g.requesting_user['id'], token))
280
288
  api_token = cur.fetchone()
281
289
 
282
290
  if not api_token:
@@ -297,9 +305,7 @@ class AuthManager:
297
305
  return jsonify({'valid': True})
298
306
 
299
307
  @bp.route('/api-tokens', methods=['DELETE'])
300
- @handle_auth_errors
301
- @self.require_auth
302
- def delete_api_token(requesting_user):
308
+ def delete_api_token():
303
309
  token = request.json.get('token')
304
310
  if not token:
305
311
  raise AuthError('Token required', 400)
@@ -310,7 +316,7 @@ class AuthManager:
310
316
  DELETE FROM api_tokens
311
317
  WHERE user_id = %s AND id = %s
312
318
  RETURNING id
313
- """, (requesting_user['id'], token))
319
+ """, (g.requesting_user['id'], token))
314
320
  deleted_id = cur.fetchone()
315
321
  if not deleted_id:
316
322
  raise ValueError('Token not found or already deleted')
@@ -318,7 +324,6 @@ class AuthManager:
318
324
  return jsonify({'deleted': True})
319
325
 
320
326
  @bp.route('/register', methods=['POST'])
321
- @handle_auth_errors
322
327
  def register():
323
328
  data = request.get_json()
324
329
 
@@ -357,7 +362,6 @@ class AuthManager:
357
362
  return jsonify({'id': user.id}), 201
358
363
 
359
364
  @bp.route('/roles', methods=['GET'])
360
- @handle_auth_errors
361
365
  def get_roles():
362
366
  with self.db.get_cursor() as cur:
363
367
  cur.execute("SELECT * FROM roles")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: the37lab_authlib
3
- Version: 0.1.1750187527
3
+ Version: 0.1.1750837371
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
- - Use `@auth.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=qj3b5PBT2egtygQAAPBiHFcWEgfD-9cIOlMwP5_HJ3M,20835
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.1750187527.dist-info/METADATA,sha256=I1q0GUs96_gBGEDq4no_p0t_UXXyw5IlWsPVbaJJTnM,5641
8
- the37lab_authlib-0.1.1750187527.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- the37lab_authlib-0.1.1750187527.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
- the37lab_authlib-0.1.1750187527.dist-info/RECORD,,
7
+ the37lab_authlib-0.1.1750837371.dist-info/METADATA,sha256=nAsElJNxxbQgDfwvVe0Ia4_22MJ3i0VOyW8ZJk3vmO0,6352
8
+ the37lab_authlib-0.1.1750837371.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ the37lab_authlib-0.1.1750837371.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
+ the37lab_authlib-0.1.1750837371.dist-info/RECORD,,