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.

Files changed (16) hide show
  1. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/PKG-INFO +2 -17
  2. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/README.md +1 -16
  3. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/pyproject.toml +1 -1
  4. the37lab_authlib-0.1.1750840354/src/the37lab_authlib/__init__.py +5 -0
  5. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/auth.py +60 -42
  6. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/decorators.py +7 -1
  7. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/PKG-INFO +2 -17
  8. the37lab_authlib-0.1.1750837371/src/the37lab_authlib/__init__.py +0 -5
  9. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/setup.cfg +0 -0
  10. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/db.py +0 -0
  11. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/exceptions.py +0 -0
  12. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib/models.py +0 -0
  13. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
  14. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
  15. {the37lab_authlib-0.1.1750837371 → the37lab_authlib-0.1.1750840354}/src/the37lab_authlib.egg-info/requires.txt +0 -0
  16. {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.1750837371
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
- - 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.
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
- - 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.
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.1750837371"
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"]
@@ -0,0 +1,5 @@
1
+ from .auth import AuthManager
2
+ from .decorators import require_auth, public_endpoint
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["AuthManager", "require_auth", "public_endpoint"]
@@ -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
- if request.endpoint not in self.public_endpoints:
155
- g.requesting_user = self._authenticate_request()
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
- user = g.requesting_user
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
- def get_tokens():
225
- tokens = self.get_user_api_tokens(g.requesting_user['id'])
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
- def create_token():
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(g.requesting_user['id'], name, expires_in_days)
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
- def create_api_token():
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(g.requesting_user['id'], name)
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
- def validate_api_token():
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
- """, (g.requesting_user['id'], token))
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
- def delete_api_token():
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
- """, (g.requesting_user['id'], token))
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.1750837371
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
- - 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.
147
+ - Use `@auth.require_auth()` decorator to protect Flask routes.
163
148
 
164
149
  ## User Object
165
150
 
@@ -1,5 +0,0 @@
1
- from .auth import AuthManager
2
- from .decorators import require_auth
3
-
4
- __version__ = "0.1.0"
5
- __all__ = ["AuthManager", "require_auth"]