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.

Files changed (15) hide show
  1. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/PKG-INFO +17 -2
  2. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/README.md +16 -1
  3. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/pyproject.toml +1 -1
  4. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/auth.py +51 -43
  5. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/PKG-INFO +17 -2
  6. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/setup.cfg +0 -0
  7. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/__init__.py +0 -0
  8. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/db.py +0 -0
  9. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/decorators.py +0 -0
  10. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/exceptions.py +0 -0
  11. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib/models.py +0 -0
  12. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
  13. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
  14. {the37lab_authlib-0.1.1750840398 → the37lab_authlib-0.1.1750844514}/src/the37lab_authlib.egg-info/requires.txt +0 -0
  15. {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.1750840398
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
- - 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
 
@@ -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
- - Use `@auth.require_auth()` decorator to protect Flask routes.
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.1750840398"
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"]
@@ -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
- token = request.headers.get('Authorization', '').split(' ')[-1]
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
- @handle_auth_errors
213
- @self.require_auth
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
- @handle_auth_errors
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
- @handle_auth_errors
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
- @handle_auth_errors
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
- @handle_auth_errors
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.1750840398
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
- - 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