lumberfi-authorization 0.1.0__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.
@@ -0,0 +1,25 @@
1
+ from .descope_client import init_client, validate_session
2
+ from .authorization import validate_token_optional, validate_token_required
3
+ from .exceptions import (
4
+ SDKError,
5
+ ClientNotConfigured,
6
+ TokenMissing,
7
+ TokenInvalid,
8
+ PermissionDenied,
9
+ NotAuthorized,
10
+ ValidationError,
11
+ )
12
+
13
+ __all__ = [
14
+ "init_client",
15
+ "validate_session",
16
+ "validate_token_optional",
17
+ "validate_token_required",
18
+ "SDKError",
19
+ "ClientNotConfigured",
20
+ "TokenMissing",
21
+ "TokenInvalid",
22
+ "PermissionDenied",
23
+ "NotAuthorized",
24
+ "ValidationError",
25
+ ]
@@ -0,0 +1,3 @@
1
+ """Version information for lumberfi-authorization package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,73 @@
1
+ import re
2
+ from typing import Tuple, Optional, Protocol, Mapping
3
+ from .descope_client import validate_session
4
+ from .exceptions import NotAuthorized, PermissionDenied, SDKError
5
+ import logging
6
+ AUTHORIZATION_HEADER_NAME = "Authorization"
7
+ BEARER_RE = re.compile(r"^[Bb]earer (.+)$")
8
+
9
+
10
+ class RequestLike(Protocol):
11
+ headers: Mapping[str, str]
12
+
13
+
14
+ class TokenValidator:
15
+ """Token validator that only authorizes tokens by validating with Descope.
16
+
17
+ Methods return the validated session dict (from Descope) and the raw token.
18
+ """
19
+
20
+ def parse_bearer(self, request: RequestLike) -> Optional[str]:
21
+ headers = getattr(request, "headers", {}) or {}
22
+ if isinstance(headers, dict):
23
+ auth_header_value = headers.get(AUTHORIZATION_HEADER_NAME) or headers.get(AUTHORIZATION_HEADER_NAME.lower())
24
+ else:
25
+ try:
26
+ auth_header_value = headers.get(AUTHORIZATION_HEADER_NAME)
27
+ except Exception:
28
+ auth_header_value = None
29
+ if not auth_header_value:
30
+ return None
31
+ match = BEARER_RE.match(auth_header_value)
32
+ if not match:
33
+ return None
34
+ return match.group(1)
35
+
36
+ def validate_optional(self, request: RequestLike) -> Tuple[Optional[dict], Optional[str]]:
37
+ token = self.parse_bearer(request)
38
+ if not token:
39
+ return (None, None)
40
+ try:
41
+ session = validate_session(token)
42
+ return (session, token)
43
+ except SDKError:
44
+ # For optional validation, silently return None on any SDK error
45
+ return (None, None)
46
+ except Exception as e:
47
+ # Catch any unexpected errors and return None
48
+ logging.debug(f"Unexpected error during optional token validation: {e}")
49
+ return (None, None)
50
+
51
+ def validate_required(self, request: RequestLike) -> Tuple[dict, str]:
52
+ token = self.parse_bearer(request)
53
+ if not token:
54
+ raise NotAuthorized(f'Request does not contain "{AUTHORIZATION_HEADER_NAME}" header')
55
+ try:
56
+ logging.debug("Validating token with Descope")
57
+ session = validate_session(token)
58
+ return (session, token)
59
+ except SDKError:
60
+ # Re-raise SDK errors as-is (ClientNotConfigured, etc.)
61
+ raise
62
+ except Exception as error:
63
+ # Wrap unexpected errors in PermissionDenied for invalid tokens
64
+ raise PermissionDenied(f"Token validation failed: {str(error)}")
65
+
66
+
67
+ # Convenience functions
68
+ def validate_token_optional(request: RequestLike) -> Tuple[Optional[dict], Optional[str]]:
69
+ return TokenValidator().validate_optional(request)
70
+
71
+
72
+ def validate_token_required(request: RequestLike) -> Tuple[dict, str]:
73
+ return TokenValidator().validate_required(request)
@@ -0,0 +1,92 @@
1
+ import os
2
+ from typing import Optional, Any
3
+ from .exceptions import ClientNotConfigured, ValidationError
4
+
5
+ try:
6
+ from descope import DescopeClient
7
+ except ImportError:
8
+ DescopeClient = None
9
+
10
+ # internally stored client instance
11
+ _descope_client: Optional[Any] = None
12
+
13
+
14
+ def init_client(
15
+ project_id: Optional[str] = None,
16
+ management_key: Optional[str] = None,
17
+ client: Optional[Any] = None
18
+ ) -> Any:
19
+ """Initialize the descope client for SDK use.
20
+
21
+ You can either pass an existing `client` instance or provide `project_id` and
22
+ `management_key` to construct one. If the Descope SDK isn't installed, attempting
23
+ to build a client will raise ImportError.
24
+
25
+ Args:
26
+ project_id: Descope project ID. If not provided, will try to use
27
+ DESCOPE_PROJECT_ID environment variable.
28
+ management_key: Descope management key. If not provided, will try to use
29
+ DESCOPE_MANAGEMENT_KEY environment variable.
30
+ client: Optional existing DescopeClient instance to use instead of creating one.
31
+
32
+ Returns:
33
+ The configured DescopeClient instance.
34
+
35
+ Raises:
36
+ ImportError: If descope package is not available and client is not provided.
37
+ RuntimeError: If project_id and management_key are not provided and client is None.
38
+ """
39
+ global _descope_client
40
+
41
+ if client is not None:
42
+ _descope_client = client
43
+ return _descope_client
44
+
45
+ if DescopeClient is None:
46
+ raise ImportError("descope package is not available. Install it with: pip install descope")
47
+
48
+ pid = project_id or os.getenv("DESCOPE_PROJECT_ID")
49
+ mkey = management_key or os.getenv("DESCOPE_MANAGEMENT_KEY")
50
+
51
+ if not pid or not mkey:
52
+ raise RuntimeError(
53
+ "project_id and management_key are required to create DescopeClient. "
54
+ "Either pass them as arguments or set DESCOPE_PROJECT_ID and "
55
+ "DESCOPE_MANAGEMENT_KEY environment variables."
56
+ )
57
+
58
+ _descope_client = DescopeClient(project_id=pid, management_key=mkey)
59
+ return _descope_client
60
+
61
+
62
+ def validate_session(session_token: str) -> dict:
63
+ """Validate a session token using the configured Descope client.
64
+
65
+ Args:
66
+ session_token: The session token to validate.
67
+
68
+ Returns:
69
+ The validated session dictionary from Descope.
70
+
71
+ Raises:
72
+ ClientNotConfigured: If the client is not configured. Call init_client(...) first.
73
+ ValidationError: If token validation fails due to an unexpected error.
74
+ """
75
+ global _descope_client
76
+
77
+ if _descope_client is None:
78
+ # Try to auto-initialize from environment variables
79
+ try:
80
+ init_client()
81
+ except (ImportError, RuntimeError):
82
+ raise ClientNotConfigured(
83
+ "Descope client not configured. Call init_client(...) to configure the SDK"
84
+ )
85
+
86
+ try:
87
+ return _descope_client.validate_session(session_token=session_token)
88
+ except Exception as e:
89
+ # Wrap Descope client errors in ValidationError for better error handling
90
+ if isinstance(e, ClientNotConfigured):
91
+ raise
92
+ raise ValidationError(f"Token validation failed: {str(e)}") from e
@@ -0,0 +1,214 @@
1
+ """Custom exceptions for the lumberfi-authorization SDK.
2
+
3
+ These exceptions are framework-agnostic and can be used in any Python environment
4
+ (Django, Flask, FastAPI, etc.). All exceptions inherit from SDKError for easy
5
+ exception handling.
6
+
7
+ Example:
8
+ try:
9
+ session, token = validate_token_required(request)
10
+ except NotAuthenticated:
11
+ # Handle missing or malformed token
12
+ pass
13
+ except PermissionDenied:
14
+ # Handle invalid or expired token
15
+ pass
16
+ except SDKError:
17
+ # Catch any SDK-related error
18
+ pass
19
+ """
20
+
21
+ from typing import List, Optional
22
+
23
+
24
+ class SDKError(Exception):
25
+ """Base exception for all SDK-related errors.
26
+
27
+ All exceptions in this module inherit from this class, making it easy to
28
+ catch any SDK-related error with a single except clause.
29
+
30
+ Attributes:
31
+ message: Human-readable error message.
32
+ error_code: Optional error code for programmatic error handling.
33
+
34
+ Example:
35
+ try:
36
+ # SDK operations
37
+ pass
38
+ except SDKError as e:
39
+ # Handle any SDK error
40
+ print(f"SDK Error: {e}")
41
+ """
42
+
43
+ def __init__(self, message: str, error_code: Optional[str] = None):
44
+ """Initialize SDKError.
45
+
46
+ Args:
47
+ message: Human-readable error message.
48
+ error_code: Optional error code for programmatic handling.
49
+ """
50
+ super().__init__(message)
51
+ self.message = message
52
+ self.error_code = error_code
53
+
54
+ def __str__(self) -> str:
55
+ """Return string representation of the error."""
56
+ if self.error_code:
57
+ return f"[{self.error_code}] {self.message}"
58
+ return self.message
59
+
60
+
61
+ class ClientNotConfigured(SDKError):
62
+ """Raised when the Descope client has not been initialized.
63
+
64
+ This exception is raised when attempting to validate a token before
65
+ initializing the SDK with init_client().
66
+
67
+ Example:
68
+ # Without calling init_client() first
69
+ try:
70
+ validate_session("token")
71
+ except ClientNotConfigured:
72
+ init_client(project_id="...", management_key="...")
73
+ """
74
+
75
+ def __init__(self, message: str = "Descope client not configured. Call init_client(...) to configure the SDK"):
76
+ """Initialize ClientNotConfigured.
77
+
78
+ Args:
79
+ message: Custom error message. Defaults to a helpful message.
80
+ """
81
+ super().__init__(message, error_code="CLIENT_NOT_CONFIGURED")
82
+
83
+
84
+ class TokenMissing(SDKError):
85
+ """Raised when a required token is missing from the request.
86
+
87
+ This exception indicates that the Authorization header is completely
88
+ missing from the request, or the header format is incorrect.
89
+
90
+ Example:
91
+ try:
92
+ session, token = validate_token_required(request)
93
+ except TokenMissing:
94
+ # Request doesn't have Authorization header
95
+ return {"error": "Authentication required"}, 401
96
+ """
97
+
98
+ def __init__(self, message: str = "Authorization token is missing from the request"):
99
+ """Initialize TokenMissing.
100
+
101
+ Args:
102
+ message: Custom error message. Defaults to a helpful message.
103
+ """
104
+ super().__init__(message, error_code="TOKEN_MISSING")
105
+
106
+
107
+ class TokenInvalid(SDKError):
108
+ """Raised when a token is invalid according to the authorization provider.
109
+
110
+ This exception is raised when the token exists but is invalid, expired,
111
+ or malformed according to Descope's validation.
112
+
113
+ Attributes:
114
+ token: The token that failed validation (if available).
115
+
116
+ Example:
117
+ try:
118
+ session = validate_session("invalid_token")
119
+ except TokenInvalid as e:
120
+ # Token is invalid or expired
121
+ print(f"Invalid token: {e}")
122
+ """
123
+
124
+ def __init__(self, message: str = "Token is invalid or expired", token: Optional[str] = None):
125
+ """Initialize TokenInvalid.
126
+
127
+ Args:
128
+ message: Custom error message. Defaults to a helpful message.
129
+ token: The invalid token (optional, for logging purposes).
130
+ """
131
+ super().__init__(message, error_code="TOKEN_INVALID")
132
+ self.token = token
133
+
134
+
135
+ class PermissionDenied(SDKError):
136
+ """Raised when an operation is not permitted due to authorization failure.
137
+
138
+ This exception is raised when token validation fails, indicating that
139
+ the user does not have permission to access the resource. This can
140
+ happen due to invalid tokens, expired tokens, or insufficient permissions.
141
+
142
+ Example:
143
+ try:
144
+ session, token = validate_token_required(request)
145
+ except PermissionDenied:
146
+ # Token validation failed
147
+ return {"error": "Access denied"}, 403
148
+ """
149
+
150
+ def __init__(self, message: str = "Permission denied"):
151
+ """Initialize PermissionDenied.
152
+
153
+ Args:
154
+ message: Custom error message. Defaults to a helpful message.
155
+ """
156
+ super().__init__(message, error_code="PERMISSION_DENIED")
157
+
158
+
159
+ class NotAuthorized(SDKError):
160
+ """Raised when an authentication/authorization token is missing or malformed.
161
+
162
+ This exception is raised when the Authorization header is missing,
163
+ malformed, or doesn't contain a valid Bearer token format.
164
+
165
+ Example:
166
+ try:
167
+ session, token = validate_token_required(request)
168
+ except NotAuthorized:
169
+ # Missing or malformed Authorization header
170
+ return {"error": "Authentication required"}, 401
171
+ """
172
+
173
+ def __init__(self, message: str = "Authentication required"):
174
+ """Initialize NotAuthorized.
175
+
176
+ Args:
177
+ message: Custom error message. Defaults to a helpful message.
178
+ """
179
+ super().__init__(message, error_code="NOT_AUTHENTICATED")
180
+
181
+
182
+ class ValidationError(SDKError):
183
+ """Raised when token validation fails due to an unexpected error.
184
+
185
+ This exception is raised when token validation encounters an unexpected
186
+ error during the validation process (e.g., network issues, service
187
+ unavailability).
188
+
189
+ Example:
190
+ try:
191
+ session = validate_session(token)
192
+ except ValidationError as e:
193
+ # Unexpected error during validation
194
+ logger.error(f"Validation error: {e}")
195
+ """
196
+
197
+ def __init__(self, message: str = "Token validation failed"):
198
+ """Initialize ValidationError.
199
+
200
+ Args:
201
+ message: Custom error message. Defaults to a helpful message.
202
+ """
203
+ super().__init__(message, error_code="VALIDATION_ERROR")
204
+
205
+
206
+ __all__: List[str] = [
207
+ "SDKError",
208
+ "ClientNotConfigured",
209
+ "TokenMissing",
210
+ "TokenInvalid",
211
+ "PermissionDenied",
212
+ "NotAuthorized",
213
+ "ValidationError",
214
+ ]
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: lumberfi-authorization
3
+ Version: 0.1.0
4
+ Summary: SDK for Descope token authorization and validation
5
+ Author: Lumberfi
6
+ Maintainer: Lumberfi
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/lumberfi/lumberfi-authorization
9
+ Project-URL: Documentation, https://github.com/lumberfi/lumberfi-authorization#readme
10
+ Project-URL: Repository, https://github.com/lumberfi/lumberfi-authorization
11
+ Project-URL: Issues, https://github.com/lumberfi/lumberfi-authorization/issues
12
+ Keywords: descope,authorization,authentication,sdk,token,validation
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Security
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: descope==1.6.7
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest==8.3.2; extra == "dev"
29
+ Requires-Dist: pytest-cov==5.0.0; extra == "dev"
30
+ Requires-Dist: black==24.8.0; extra == "dev"
31
+ Requires-Dist: flake8==6.1.0; extra == "dev"
32
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
33
+ Dynamic: license-file
@@ -0,0 +1,10 @@
1
+ authorization/__init__.py,sha256=yQUywlR5tI533sP8IbblmFXPuMmXMLGbblzGinnm3J8,570
2
+ authorization/__version__.py,sha256=b1FPckCmbhW1sdf17fwBpMTnxm8zEaMzzZeGmyuJ2ZI,85
3
+ authorization/authorization.py,sha256=d8P7qyFf63-Ae96K_f1BjxBqFVbCfLw7dcn-PPm46Hw,2797
4
+ authorization/descope_client.py,sha256=IrBSePEV8cf6h4CtlFb0WdyEfCCaNVMPEdO1-C0upG0,3203
5
+ authorization/exceptions.py,sha256=wMzieKeksDUFJtMPNR1AiDbXeWyF3arnas1vakfDIOE,6691
6
+ lumberfi_authorization-0.1.0.dist-info/licenses/LICENSE,sha256=4lZHi0OmL4hr3P_9OzgNpDPa0BWArET8vn02ovTBDA4,1065
7
+ lumberfi_authorization-0.1.0.dist-info/METADATA,sha256=sPUjBJflv5yPPfTk11iTjynKEhySBmfdxT8BiIsJ0XY,1436
8
+ lumberfi_authorization-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
9
+ lumberfi_authorization-0.1.0.dist-info/top_level.txt,sha256=1Fzfnhfl_A4cLRIexFIl5IfF-Q1AGm-10TmkiQo3f3Y,14
10
+ lumberfi_authorization-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lumberfi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ authorization