the37lab-authlib 0.1.1750836881__py3-none-any.whl → 0.1.1750840354__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/__init__.py +2 -2
- the37lab_authlib/auth.py +60 -29
- the37lab_authlib/decorators.py +7 -1
- {the37lab_authlib-0.1.1750836881.dist-info → the37lab_authlib-0.1.1750840354.dist-info}/METADATA +2 -10
- the37lab_authlib-0.1.1750840354.dist-info/RECORD +10 -0
- the37lab_authlib-0.1.1750836881.dist-info/RECORD +0 -10
- {the37lab_authlib-0.1.1750836881.dist-info → the37lab_authlib-0.1.1750840354.dist-info}/WHEEL +0 -0
- {the37lab_authlib-0.1.1750836881.dist-info → the37lab_authlib-0.1.1750840354.dist-info}/top_level.txt +0 -0
the37lab_authlib/__init__.py
CHANGED
the37lab_authlib/auth.py
CHANGED
|
@@ -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))
|
|
@@ -121,27 +133,21 @@ class AuthManager:
|
|
|
121
133
|
def create_blueprint(self):
|
|
122
134
|
bp = Blueprint('auth', __name__, url_prefix='/api/v1/users')
|
|
123
135
|
|
|
124
|
-
public_endpoints = {
|
|
125
|
-
'auth.login',
|
|
126
|
-
'auth.oauth_login',
|
|
127
|
-
'auth.oauth_callback',
|
|
128
|
-
'auth.refresh_token',
|
|
129
|
-
'auth.register',
|
|
130
|
-
'auth.get_roles'
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
@bp.errorhandler(AuthError)
|
|
134
|
-
def handle_auth_error(err):
|
|
135
|
-
response = jsonify(err.to_dict())
|
|
136
|
-
response.status_code = err.status_code
|
|
137
|
-
return response
|
|
138
|
-
|
|
139
136
|
@bp.before_request
|
|
140
137
|
def load_user():
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
143
147
|
|
|
144
148
|
@bp.route('/login', methods=['POST'])
|
|
149
|
+
@public_endpoint
|
|
150
|
+
@handle_auth_errors
|
|
145
151
|
def login():
|
|
146
152
|
data = request.get_json()
|
|
147
153
|
username = data.get('username')
|
|
@@ -176,6 +182,8 @@ class AuthManager:
|
|
|
176
182
|
})
|
|
177
183
|
|
|
178
184
|
@bp.route('/login/oauth', methods=['POST'])
|
|
185
|
+
@public_endpoint
|
|
186
|
+
@handle_auth_errors
|
|
179
187
|
def oauth_login():
|
|
180
188
|
provider = request.json.get('provider')
|
|
181
189
|
if provider not in self.oauth_config:
|
|
@@ -187,6 +195,8 @@ class AuthManager:
|
|
|
187
195
|
})
|
|
188
196
|
|
|
189
197
|
@bp.route('/login/oauth2callback')
|
|
198
|
+
@public_endpoint
|
|
199
|
+
@handle_auth_errors
|
|
190
200
|
def oauth_callback():
|
|
191
201
|
code = request.args.get('code')
|
|
192
202
|
provider = request.args.get('state')
|
|
@@ -203,22 +213,28 @@ class AuthManager:
|
|
|
203
213
|
return redirect(f"{frontend_url}/oauth-callback?token={token}&refresh_token={refresh_token}")
|
|
204
214
|
|
|
205
215
|
@bp.route('/login/profile')
|
|
216
|
+
@handle_auth_errors
|
|
206
217
|
def profile():
|
|
207
|
-
|
|
218
|
+
token = request.headers.get('Authorization', '').split(' ')[-1]
|
|
219
|
+
user = self.validate_token(token)
|
|
208
220
|
return jsonify(user)
|
|
209
221
|
|
|
210
222
|
@bp.route('/api-tokens', methods=['GET'])
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
@handle_auth_errors
|
|
224
|
+
@self.require_auth
|
|
225
|
+
def get_tokens(requesting_user):
|
|
226
|
+
tokens = self.get_user_api_tokens(requesting_user['id'])
|
|
213
227
|
return jsonify(tokens)
|
|
214
228
|
|
|
215
229
|
@bp.route('/api-tokens', methods=['POST'])
|
|
216
|
-
|
|
230
|
+
@handle_auth_errors
|
|
231
|
+
@self.require_auth
|
|
232
|
+
def create_token(requesting_user):
|
|
217
233
|
name = request.json.get('name')
|
|
218
234
|
expires_in_days = request.json.get('expires_in_days')
|
|
219
235
|
if not name:
|
|
220
236
|
raise AuthError('Token name is required', 400)
|
|
221
|
-
api_token = self.create_api_token(
|
|
237
|
+
api_token = self.create_api_token(requesting_user['id'], name, expires_in_days)
|
|
222
238
|
return jsonify({
|
|
223
239
|
'id': api_token.id,
|
|
224
240
|
'name': api_token.name,
|
|
@@ -228,6 +244,8 @@ class AuthManager:
|
|
|
228
244
|
})
|
|
229
245
|
|
|
230
246
|
@bp.route('/token-refresh', methods=['POST'])
|
|
247
|
+
@public_endpoint
|
|
248
|
+
@handle_auth_errors
|
|
231
249
|
def refresh_token():
|
|
232
250
|
refresh_token = request.json.get('refresh_token')
|
|
233
251
|
if not refresh_token:
|
|
@@ -252,16 +270,20 @@ class AuthManager:
|
|
|
252
270
|
raise AuthError('Invalid refresh token', 401)
|
|
253
271
|
|
|
254
272
|
@bp.route('/api-tokens', methods=['POST'])
|
|
255
|
-
|
|
273
|
+
@handle_auth_errors
|
|
274
|
+
@self.require_auth
|
|
275
|
+
def create_api_token(requesting_user):
|
|
256
276
|
name = request.json.get('name')
|
|
257
277
|
if not name:
|
|
258
278
|
raise AuthError('Token name required', 400)
|
|
259
279
|
|
|
260
|
-
token = self.create_api_token(
|
|
280
|
+
token = self.create_api_token(requesting_user['id'], name)
|
|
261
281
|
return jsonify({'token': token.token})
|
|
262
282
|
|
|
263
283
|
@bp.route('/api-tokens/validate', methods=['GET'])
|
|
264
|
-
|
|
284
|
+
@handle_auth_errors
|
|
285
|
+
@self.require_auth
|
|
286
|
+
def validate_api_token(requesting_user):
|
|
265
287
|
token = request.json.get('token')
|
|
266
288
|
if not token:
|
|
267
289
|
raise AuthError('No API token provided', 401)
|
|
@@ -271,7 +293,7 @@ class AuthManager:
|
|
|
271
293
|
cur.execute("""
|
|
272
294
|
SELECT * FROM api_tokens
|
|
273
295
|
WHERE user_id = %s AND id = %s
|
|
274
|
-
""", (
|
|
296
|
+
""", (requesting_user['id'], token))
|
|
275
297
|
api_token = cur.fetchone()
|
|
276
298
|
|
|
277
299
|
if not api_token:
|
|
@@ -292,7 +314,9 @@ class AuthManager:
|
|
|
292
314
|
return jsonify({'valid': True})
|
|
293
315
|
|
|
294
316
|
@bp.route('/api-tokens', methods=['DELETE'])
|
|
295
|
-
|
|
317
|
+
@handle_auth_errors
|
|
318
|
+
@self.require_auth
|
|
319
|
+
def delete_api_token(requesting_user):
|
|
296
320
|
token = request.json.get('token')
|
|
297
321
|
if not token:
|
|
298
322
|
raise AuthError('Token required', 400)
|
|
@@ -303,7 +327,7 @@ class AuthManager:
|
|
|
303
327
|
DELETE FROM api_tokens
|
|
304
328
|
WHERE user_id = %s AND id = %s
|
|
305
329
|
RETURNING id
|
|
306
|
-
""", (
|
|
330
|
+
""", (requesting_user['id'], token))
|
|
307
331
|
deleted_id = cur.fetchone()
|
|
308
332
|
if not deleted_id:
|
|
309
333
|
raise ValueError('Token not found or already deleted')
|
|
@@ -311,6 +335,8 @@ class AuthManager:
|
|
|
311
335
|
return jsonify({'deleted': True})
|
|
312
336
|
|
|
313
337
|
@bp.route('/register', methods=['POST'])
|
|
338
|
+
@public_endpoint
|
|
339
|
+
@handle_auth_errors
|
|
314
340
|
def register():
|
|
315
341
|
data = request.get_json()
|
|
316
342
|
|
|
@@ -349,6 +375,8 @@ class AuthManager:
|
|
|
349
375
|
return jsonify({'id': user.id}), 201
|
|
350
376
|
|
|
351
377
|
@bp.route('/roles', methods=['GET'])
|
|
378
|
+
@public_endpoint
|
|
379
|
+
@handle_auth_errors
|
|
352
380
|
def get_roles():
|
|
353
381
|
with self.db.get_cursor() as cur:
|
|
354
382
|
cur.execute("SELECT * FROM roles")
|
|
@@ -388,6 +416,8 @@ class AuthManager:
|
|
|
388
416
|
raise AuthError(str(e), 500)
|
|
389
417
|
|
|
390
418
|
def get_current_user(self):
|
|
419
|
+
if hasattr(g, 'current_user'):
|
|
420
|
+
return g.current_user
|
|
391
421
|
return self._authenticate_request()
|
|
392
422
|
|
|
393
423
|
def get_user_api_tokens(self, user_id):
|
|
@@ -442,6 +472,7 @@ class AuthManager:
|
|
|
442
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}'
|
|
443
473
|
raise AuthError('Invalid OAuth provider')
|
|
444
474
|
|
|
475
|
+
|
|
445
476
|
def _get_oauth_user_info(self, provider, code):
|
|
446
477
|
if provider == 'google':
|
|
447
478
|
client_id = self.oauth_config['google']['client_id']
|
|
@@ -503,4 +534,4 @@ class AuthManager:
|
|
|
503
534
|
user['real_name'] = userinfo.get('name', userinfo['email'])
|
|
504
535
|
|
|
505
536
|
return user
|
|
506
|
-
raise AuthError('Invalid OAuth provider')
|
|
537
|
+
raise AuthError('Invalid OAuth provider')
|
the37lab_authlib/decorators.py
CHANGED
|
@@ -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
|
{the37lab_authlib-0.1.1750836881.dist-info → the37lab_authlib-0.1.1750840354.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: the37lab_authlib
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
@@ -72,12 +72,6 @@ def protected_route():
|
|
|
72
72
|
return "Protected content"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
`AuthManager`'s blueprint now registers a global error handler for
|
|
76
|
-
`AuthError` and authenticates requests for all of its routes by default.
|
|
77
|
-
Authenticated users are made available as `flask.g.requesting_user`.
|
|
78
|
-
Only the login, OAuth, token refresh, registration and role listing
|
|
79
|
-
endpoints are exempt from this check.
|
|
80
|
-
|
|
81
75
|
## Configuration
|
|
82
76
|
|
|
83
77
|
### Required Parameters
|
|
@@ -150,9 +144,7 @@ endpoints are exempt from this check.
|
|
|
150
144
|
- Get redirect URL from `/api/v1/users/login/oauth`.
|
|
151
145
|
- Complete OAuth flow via `/api/v1/users/login/oauth2callback`.
|
|
152
146
|
4. **Protected Routes:**
|
|
153
|
-
-
|
|
154
|
-
The authenticated user can be accessed via `g.requesting_user`.
|
|
155
|
-
Use `@auth.require_auth()` to protect custom routes in your application.
|
|
147
|
+
- Use `@auth.require_auth()` decorator to protect Flask routes.
|
|
156
148
|
|
|
157
149
|
## User Object
|
|
158
150
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
the37lab_authlib/__init__.py,sha256=YV1C1iaIs-8cD5dFe-VEC6dRhrT6mglgTdcveT6AMCQ,168
|
|
2
|
+
the37lab_authlib/auth.py,sha256=dkmRfkJ03W8FsLO1OcT_erG0DzP7kMlpPdrw_jts0IE,21372
|
|
3
|
+
the37lab_authlib/db.py,sha256=fTXxnfju0lmbFGPVbXpTMeDmJMeBgURVZTndyxyRyCc,2734
|
|
4
|
+
the37lab_authlib/decorators.py,sha256=oBO3fbRo7H0rcXeUq6M8yK-5mgHKfaJEDG6XdNsxQPI,1351
|
|
5
|
+
the37lab_authlib/exceptions.py,sha256=mdplK5sKNtagPAzSGq5NGsrQ4r-k03DKJBKx6myWwZc,317
|
|
6
|
+
the37lab_authlib/models.py,sha256=-PlvQlHGIsSdrH0H9Cdh_vTPlltGV8G1Z1mmGQvAg9Y,3422
|
|
7
|
+
the37lab_authlib-0.1.1750840354.dist-info/METADATA,sha256=01Z5Jknra_QYDGgu-_qNC9Rqz0sCafF2TdzI7LaVmgU,5641
|
|
8
|
+
the37lab_authlib-0.1.1750840354.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
the37lab_authlib-0.1.1750840354.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
|
|
10
|
+
the37lab_authlib-0.1.1750840354.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
|
|
2
|
-
the37lab_authlib/auth.py,sha256=tBs-THT_sJeolT9hxwOmtCcsHCMfVEQXtYdVJOCRdAs,20338
|
|
3
|
-
the37lab_authlib/db.py,sha256=fTXxnfju0lmbFGPVbXpTMeDmJMeBgURVZTndyxyRyCc,2734
|
|
4
|
-
the37lab_authlib/decorators.py,sha256=AEQfix31fHUZvhEZd4Ud8Zh2KBGjV6O_braiPL-BU7w,1219
|
|
5
|
-
the37lab_authlib/exceptions.py,sha256=mdplK5sKNtagPAzSGq5NGsrQ4r-k03DKJBKx6myWwZc,317
|
|
6
|
-
the37lab_authlib/models.py,sha256=-PlvQlHGIsSdrH0H9Cdh_vTPlltGV8G1Z1mmGQvAg9Y,3422
|
|
7
|
-
the37lab_authlib-0.1.1750836881.dist-info/METADATA,sha256=Jdhr9dU1rR4pDX9m5VJooujmEOpAB0zAxCkmXKf0N_M,6113
|
|
8
|
-
the37lab_authlib-0.1.1750836881.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
the37lab_authlib-0.1.1750836881.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
|
|
10
|
-
the37lab_authlib-0.1.1750836881.dist-info/RECORD,,
|
{the37lab_authlib-0.1.1750836881.dist-info → the37lab_authlib-0.1.1750840354.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|