the37lab-authlib 0.1.1750840415__py3-none-any.whl → 0.1.1750952155__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/db.py CHANGED
@@ -1,74 +1,74 @@
1
- import psycopg2
2
- from psycopg2.extras import RealDictCursor
3
- from contextlib import contextmanager
4
- from .models import UUIDGenerator, IntegerGenerator
5
-
6
- class Database:
7
- def __init__(self, dsn, id_type='uuid'):
8
- self.dsn = dsn
9
- self.id_generator = UUIDGenerator() if id_type == 'uuid' else IntegerGenerator()
10
- self.id_type = id_type
11
- self._init_db()
12
-
13
- def _init_db(self):
14
- with self.get_connection() as conn:
15
- with conn.cursor() as cur:
16
- # Create users table with configurable ID type
17
- cur.execute(f"""
18
- CREATE TABLE IF NOT EXISTS users (
19
- id {self._get_id_type()} PRIMARY KEY,
20
- username VARCHAR(255) UNIQUE NOT NULL,
21
- email VARCHAR(255) UNIQUE NOT NULL,
22
- real_name VARCHAR(255) NOT NULL,
23
- password_hash VARCHAR(255),
24
- created_at TIMESTAMP NOT NULL,
25
- updated_at TIMESTAMP NOT NULL
26
- );
27
-
28
- CREATE TABLE IF NOT EXISTS roles (
29
- id {self._get_id_type()} PRIMARY KEY,
30
- name VARCHAR(255) UNIQUE NOT NULL,
31
- description TEXT,
32
- created_at TIMESTAMP NOT NULL
33
- );
34
-
35
- CREATE TABLE IF NOT EXISTS user_roles (
36
- user_id {self._get_id_type()} REFERENCES users(id),
37
- role_id {self._get_id_type()} REFERENCES roles(id),
38
- PRIMARY KEY (user_id, role_id)
39
- );
40
-
41
- CREATE TABLE IF NOT EXISTS api_tokens (
42
- id VARCHAR(8) PRIMARY KEY,
43
- user_id INTEGER REFERENCES users(id),
44
- name VARCHAR(255) NOT NULL,
45
- token VARCHAR(255) NOT NULL,
46
- created_at TIMESTAMP NOT NULL,
47
- expires_at TIMESTAMP,
48
- last_used_at TIMESTAMP
49
- );
50
- """)
51
-
52
- def _get_id_type(self):
53
- return 'UUID' if self.id_type == 'uuid' else 'SERIAL'
54
-
55
- @contextmanager
56
- def get_connection(self):
57
- conn = psycopg2.connect(self.dsn, cursor_factory=RealDictCursor)
58
- try:
59
- yield conn
60
- conn.commit()
61
- except Exception:
62
- conn.rollback()
63
- raise
64
- finally:
65
- conn.close()
66
-
67
- @contextmanager
68
- def get_cursor(self):
69
- with self.get_connection() as conn:
70
- with conn.cursor() as cur:
71
- yield cur
72
-
73
- def get_id_generator(self):
1
+ import psycopg2
2
+ from psycopg2.extras import RealDictCursor
3
+ from contextlib import contextmanager
4
+ from .models import UUIDGenerator, IntegerGenerator
5
+
6
+ class Database:
7
+ def __init__(self, dsn, id_type='uuid'):
8
+ self.dsn = dsn
9
+ self.id_generator = UUIDGenerator() if id_type == 'uuid' else IntegerGenerator()
10
+ self.id_type = id_type
11
+ self._init_db()
12
+
13
+ def _init_db(self):
14
+ with self.get_connection() as conn:
15
+ with conn.cursor() as cur:
16
+ # Create users table with configurable ID type
17
+ cur.execute(f"""
18
+ CREATE TABLE IF NOT EXISTS users (
19
+ id {self._get_id_type()} PRIMARY KEY,
20
+ username VARCHAR(255) UNIQUE NOT NULL,
21
+ email VARCHAR(255) UNIQUE NOT NULL,
22
+ real_name VARCHAR(255) NOT NULL,
23
+ password_hash VARCHAR(255),
24
+ created_at TIMESTAMP NOT NULL,
25
+ updated_at TIMESTAMP NOT NULL
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS roles (
29
+ id {self._get_id_type()} PRIMARY KEY,
30
+ name VARCHAR(255) UNIQUE NOT NULL,
31
+ description TEXT,
32
+ created_at TIMESTAMP NOT NULL
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS user_roles (
36
+ user_id {self._get_id_type()} REFERENCES users(id),
37
+ role_id {self._get_id_type()} REFERENCES roles(id),
38
+ PRIMARY KEY (user_id, role_id)
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS api_tokens (
42
+ id VARCHAR(8) PRIMARY KEY,
43
+ user_id INTEGER REFERENCES users(id),
44
+ name VARCHAR(255) NOT NULL,
45
+ token VARCHAR(255) NOT NULL,
46
+ created_at TIMESTAMP NOT NULL,
47
+ expires_at TIMESTAMP,
48
+ last_used_at TIMESTAMP
49
+ );
50
+ """)
51
+
52
+ def _get_id_type(self):
53
+ return 'UUID' if self.id_type == 'uuid' else 'SERIAL'
54
+
55
+ @contextmanager
56
+ def get_connection(self):
57
+ conn = psycopg2.connect(self.dsn, cursor_factory=RealDictCursor)
58
+ try:
59
+ yield conn
60
+ conn.commit()
61
+ except Exception:
62
+ conn.rollback()
63
+ raise
64
+ finally:
65
+ conn.close()
66
+
67
+ @contextmanager
68
+ def get_cursor(self):
69
+ with self.get_connection() as conn:
70
+ with conn.cursor() as cur:
71
+ yield cur
72
+
73
+ def get_id_generator(self):
74
74
  return self.id_generator
@@ -1,31 +1,33 @@
1
- from functools import wraps
2
- from flask import request, current_app, jsonify
3
- from .exceptions import AuthError
4
-
5
- def require_auth(roles=None):
6
- def decorator(f):
7
- @wraps(f)
8
- def decorated(*args, **kwargs):
9
- try:
10
- # Get the require_auth decorator from AuthManager
11
- user = current_app.auth_manager.get_current_user()
12
- if not user:
13
- raise AuthError('User not authenticated', 401)
14
-
15
- auth_decorator = current_app.auth_manager.require_auth
16
-
17
- # Apply the AuthManager's decorator and get the result
18
- decorated_func = auth_decorator(f)
19
-
20
- # Check roles if specified
21
- if roles and not any(role in user['roles'] for role in roles):
22
- raise AuthError('Insufficient permissions', 403)
23
-
24
- # Now execute the function
25
- return decorated_func(*args, **kwargs)
26
- except AuthError as e:
27
- response = jsonify(e.to_dict())
28
- response.status_code = e.status_code
29
- return response
30
- return decorated
1
+ from functools import wraps
2
+ from flask import request, current_app, jsonify
3
+ from .exceptions import AuthError
4
+
5
+ def require_auth(roles=None):
6
+ def decorator(f):
7
+ @wraps(f)
8
+ def decorated(*args, **kwargs):
9
+ if request.method == 'OPTIONS':
10
+ return f(*args, **kwargs)
11
+ try:
12
+ # Get the require_auth decorator from AuthManager
13
+ user = current_app.auth_manager.get_current_user()
14
+ if not user:
15
+ raise AuthError('User not authenticated', 401)
16
+
17
+ auth_decorator = current_app.auth_manager.require_auth
18
+
19
+ # Apply the AuthManager's decorator and get the result
20
+ decorated_func = auth_decorator(f)
21
+
22
+ # Check roles if specified
23
+ if roles and not any(role in user['roles'] for role in roles):
24
+ raise AuthError('Insufficient permissions', 403)
25
+
26
+ # Now execute the function
27
+ return decorated_func(*args, **kwargs)
28
+ except AuthError as e:
29
+ response = jsonify(e.to_dict())
30
+ response.status_code = e.status_code
31
+ return response
32
+ return decorated
31
33
  return decorator
@@ -1,11 +1,11 @@
1
- class AuthError(Exception):
2
- def __init__(self, message, status_code=401):
3
- self.message = message
4
- self.status_code = status_code
5
- super().__init__(self.message)
6
-
7
- def to_dict(self):
8
- return {
9
- 'error': self.message,
10
- 'status_code': self.status_code
1
+ class AuthError(Exception):
2
+ def __init__(self, message, status_code=401):
3
+ self.message = message
4
+ self.status_code = status_code
5
+ super().__init__(self.message)
6
+
7
+ def to_dict(self):
8
+ return {
9
+ 'error': self.message,
10
+ 'status_code': self.status_code
11
11
  }
@@ -1,95 +1,95 @@
1
- from datetime import datetime, timedelta
2
- import uuid
3
- import bcrypt
4
- import secrets
5
- import string
6
- from abc import ABC, abstractmethod
7
-
8
- def generate_random_string(length, alphabet=string.ascii_letters + string.digits):
9
- """Generate a random string of specified length using the given alphabet."""
10
- return ''.join(secrets.choice(alphabet) for _ in range(length))
11
-
12
- class IDGenerator(ABC):
13
- @abstractmethod
14
- def generate(self):
15
- pass
16
-
17
- class UUIDGenerator(IDGenerator):
18
- def generate(self):
19
- return str(uuid.uuid4())
20
-
21
- class IntegerGenerator(IDGenerator):
22
- def generate(self):
23
- return None # Let the database handle ID generation with SERIAL
24
-
25
- class User:
26
- def __init__(self, username, email, real_name, roles=None, id_generator=None):
27
- self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
28
- if self.id is None: # Let database handle ID generation
29
- self.id = None
30
- self.username = username
31
- self.email = email
32
- self.real_name = real_name
33
- self.roles = roles or []
34
- self.created_at = datetime.utcnow()
35
- self.updated_at = datetime.utcnow()
36
-
37
- class Role:
38
- def __init__(self, name, description=None, id_generator=None):
39
- self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
40
- self.name = name
41
- self.description = description
42
- self.created_at = datetime.utcnow()
43
-
44
- class ApiToken:
45
- def __init__(self, user_id, name, expires_in_days=None):
46
- self.id = generate_random_string(8) # 8 character ID
47
- self.user_id = user_id
48
- self.name = name
49
- self.nonce = generate_random_string(32) # 32 character nonce
50
- self.token = self._hash_nonce(self.nonce) # Hash the nonce
51
- self.created_at = datetime.utcnow()
52
- self.expires_at = datetime.utcnow() + timedelta(days=expires_in_days) if expires_in_days else None
53
- self.last_used_at = None
54
-
55
- def _hash_nonce(self, nonce):
56
- """Hash the nonce using bcrypt."""
57
- salt = bcrypt.gensalt()
58
- return bcrypt.hashpw(nonce.encode('utf-8'), salt).decode('utf-8')
59
-
60
- def get_full_token(self):
61
- """Get the full token string in the format api_IDNONCE."""
62
- return f"api_{self.id}{self.nonce}"
63
-
64
- @staticmethod
65
- def parse_token(token_string):
66
- """Parse a token string into its components."""
67
- if not token_string.startswith('api_'):
68
- raise ValueError('Invalid token format')
69
-
70
- token_string = token_string[4:] # Remove 'api_' prefix
71
- if len(token_string) != 40: # 8 (id) + 32 (nonce)
72
- raise ValueError('Invalid token length')
73
-
74
- return {
75
- 'id': token_string[:8],
76
- 'nonce': token_string[8:]
77
- }
78
-
79
- @staticmethod
80
- def parse_token_id(token_string):
81
- if len(token_string) == 8:
82
- return token_string
83
- if not token_string.startswith('api_'):
84
- raise ValueError('Invalid token format')
85
- return token_string[4:][:8]
86
-
87
- def verify_token(self, token_string):
88
- """Verify if a token string matches this token."""
89
- try:
90
- parsed = self.parse_token(token_string)
91
- if parsed['id'] != self.id:
92
- return False
93
- return bcrypt.checkpw(parsed['nonce'].encode('utf-8'), self.token.encode('utf-8'))
94
- except ValueError:
1
+ from datetime import datetime, timedelta
2
+ import uuid
3
+ import bcrypt
4
+ import secrets
5
+ import string
6
+ from abc import ABC, abstractmethod
7
+
8
+ def generate_random_string(length, alphabet=string.ascii_letters + string.digits):
9
+ """Generate a random string of specified length using the given alphabet."""
10
+ return ''.join(secrets.choice(alphabet) for _ in range(length))
11
+
12
+ class IDGenerator(ABC):
13
+ @abstractmethod
14
+ def generate(self):
15
+ pass
16
+
17
+ class UUIDGenerator(IDGenerator):
18
+ def generate(self):
19
+ return str(uuid.uuid4())
20
+
21
+ class IntegerGenerator(IDGenerator):
22
+ def generate(self):
23
+ return None # Let the database handle ID generation with SERIAL
24
+
25
+ class User:
26
+ def __init__(self, username, email, real_name, roles=None, id_generator=None):
27
+ self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
28
+ if self.id is None: # Let database handle ID generation
29
+ self.id = None
30
+ self.username = username
31
+ self.email = email
32
+ self.real_name = real_name
33
+ self.roles = roles or []
34
+ self.created_at = datetime.utcnow()
35
+ self.updated_at = datetime.utcnow()
36
+
37
+ class Role:
38
+ def __init__(self, name, description=None, id_generator=None):
39
+ self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
40
+ self.name = name
41
+ self.description = description
42
+ self.created_at = datetime.utcnow()
43
+
44
+ class ApiToken:
45
+ def __init__(self, user_id, name, expires_in_days=None):
46
+ self.id = generate_random_string(8) # 8 character ID
47
+ self.user_id = user_id
48
+ self.name = name
49
+ self.nonce = generate_random_string(32) # 32 character nonce
50
+ self.token = self._hash_nonce(self.nonce) # Hash the nonce
51
+ self.created_at = datetime.utcnow()
52
+ self.expires_at = datetime.utcnow() + timedelta(days=expires_in_days) if expires_in_days else None
53
+ self.last_used_at = None
54
+
55
+ def _hash_nonce(self, nonce):
56
+ """Hash the nonce using bcrypt."""
57
+ salt = bcrypt.gensalt()
58
+ return bcrypt.hashpw(nonce.encode('utf-8'), salt).decode('utf-8')
59
+
60
+ def get_full_token(self):
61
+ """Get the full token string in the format api_IDNONCE."""
62
+ return f"api_{self.id}{self.nonce}"
63
+
64
+ @staticmethod
65
+ def parse_token(token_string):
66
+ """Parse a token string into its components."""
67
+ if not token_string.startswith('api_'):
68
+ raise ValueError('Invalid token format')
69
+
70
+ token_string = token_string[4:] # Remove 'api_' prefix
71
+ if len(token_string) != 40: # 8 (id) + 32 (nonce)
72
+ raise ValueError('Invalid token length')
73
+
74
+ return {
75
+ 'id': token_string[:8],
76
+ 'nonce': token_string[8:]
77
+ }
78
+
79
+ @staticmethod
80
+ def parse_token_id(token_string):
81
+ if len(token_string) == 8:
82
+ return token_string
83
+ if not token_string.startswith('api_'):
84
+ raise ValueError('Invalid token format')
85
+ return token_string[4:][:8]
86
+
87
+ def verify_token(self, token_string):
88
+ """Verify if a token string matches this token."""
89
+ try:
90
+ parsed = self.parse_token(token_string)
91
+ if parsed['id'] != self.id:
92
+ return False
93
+ return bcrypt.checkpw(parsed['nonce'].encode('utf-8'), self.token.encode('utf-8'))
94
+ except ValueError:
95
95
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: the37lab_authlib
3
- Version: 0.1.1750840415
3
+ Version: 0.1.1750952155
4
4
  Summary: Python SDK for the Authlib
5
5
  Author-email: the37lab <info@the37lab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -50,7 +50,7 @@ pip install -e .
50
50
 
51
51
  ```python
52
52
  from flask import Flask
53
- from authlib import AuthManager, require_auth
53
+ from authlib import AuthManager
54
54
 
55
55
  app = Flask(__name__)
56
56
 
@@ -67,11 +67,24 @@ auth = AuthManager(
67
67
  )
68
68
 
69
69
  @app.route("/protected")
70
- @require_auth(roles=["admin"])
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 `@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
 
@@ -0,0 +1,10 @@
1
+ the37lab_authlib/__init__.py,sha256=QxIyIyb-b2C91a9vSE05cFov-MFwprBnPLUTCz1rAGo,136
2
+ the37lab_authlib/auth.py,sha256=e0GyYFX49z-2isQZLp3Z7DvAQNuFy6L_gKAiAMX-jI4,21629
3
+ the37lab_authlib/db.py,sha256=iXA8kPAZ2SCZgXtrfNIoCnkDwm5W-Cl2MvT1X6ulwqY,2807
4
+ the37lab_authlib/decorators.py,sha256=hCd6XSEREgeQ8XGJzlmKD-K8sQYbK7WgZCQKdY3m7ng,1337
5
+ the37lab_authlib/exceptions.py,sha256=ONA64ktHAuj4w0ur4xUeWZQQmfZw9hHo4che1Bi-M3s,327
6
+ the37lab_authlib/models.py,sha256=9-9ndGq-o9VGjHF8VvgMHvjhYOEapfOkocfjpqEFHY4,3516
7
+ the37lab_authlib-0.1.1750952155.dist-info/METADATA,sha256=j8gkzba-uRv-uLr-eQhzm4ndR_SeOFgx4Yk5I5NEYoE,6352
8
+ the37lab_authlib-0.1.1750952155.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ the37lab_authlib-0.1.1750952155.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
+ the37lab_authlib-0.1.1750952155.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
2
- the37lab_authlib/auth.py,sha256=dQkE6z9GZZpnl0nfqulcveho8W5lM95XUBLmtE-5JIc,20660
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.1750840415.dist-info/METADATA,sha256=VVfAVdbm_v4u0-7_f7V0mvcumU-iJZPzkUgRqgfP2c4,5645
8
- the37lab_authlib-0.1.1750840415.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- the37lab_authlib-0.1.1750840415.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
- the37lab_authlib-0.1.1750840415.dist-info/RECORD,,