the37lab-authlib 0.1.1750187527__tar.gz → 0.1.1750837371__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.
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/PKG-INFO +17 -2
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/README.md +16 -1
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/pyproject.toml +1 -1
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/auth.py +45 -41
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib.egg-info/PKG-INFO +17 -2
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/setup.cfg +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/__init__.py +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/db.py +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/decorators.py +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/exceptions.py +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/models.py +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib.egg-info/requires.txt +0 -0
- {the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/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.
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
7
|
+
version = "0.1.1750837371"
|
|
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"]
|
{the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/auth.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
-
|
|
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
|
|
|
File without changes
|
{the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/__init__.py
RENAMED
|
File without changes
|
{the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/db.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{the37lab_authlib-0.1.1750187527 → the37lab_authlib-0.1.1750837371}/src/the37lab_authlib/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|