auth0-server-python 1.0.0b1__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.
- auth0_server_python-1.0.0b1.dist-info/LICENSE +21 -0
- auth0_server_python-1.0.0b1.dist-info/METADATA +160 -0
- auth0_server_python-1.0.0b1.dist-info/RECORD +14 -0
- auth0_server_python-1.0.0b1.dist-info/WHEEL +4 -0
- auth_server/__init__.py +4 -0
- auth_server/server_client.py +1123 -0
- auth_types/__init__.py +222 -0
- encryption/__init__.py +4 -0
- encryption/encrypt.py +73 -0
- error/__init__.py +119 -0
- store/__init__.py +7 -0
- store/abstract.py +113 -0
- utils/__init__.py +7 -0
- utils/helpers.py +225 -0
utils/helpers.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
import time
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
from urllib.parse import urlencode, urlparse, parse_qs
|
|
8
|
+
|
|
9
|
+
class PKCE:
|
|
10
|
+
@classmethod
|
|
11
|
+
def generate_random_string(cls, length: int = 64) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Generate a cryptographically secure random string.
|
|
14
|
+
"""
|
|
15
|
+
alphabet = string.ascii_letters + string.digits
|
|
16
|
+
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def generate_code_verifier(cls, length: int = 64) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Generate a PKCE code verifier.
|
|
22
|
+
"""
|
|
23
|
+
return cls.generate_random_string(length)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def generate_code_challenge(cls, code_verifier: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Generate a PKCE code challenge from a code verifier.
|
|
29
|
+
"""
|
|
30
|
+
digest = hashlib.sha256(code_verifier.encode()).digest()
|
|
31
|
+
challenge = base64.urlsafe_b64encode(digest).decode('utf-8')
|
|
32
|
+
return challenge.rstrip('=')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class State:
|
|
36
|
+
@classmethod
|
|
37
|
+
def update_state_data(
|
|
38
|
+
cls,
|
|
39
|
+
audience: str,
|
|
40
|
+
state_data: Optional[Dict[str, Any]],
|
|
41
|
+
token_endpoint_response: Dict[str, Any]
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Utility function to update the state with a new response from the token endpoint
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
audience: The audience of the token endpoint response
|
|
48
|
+
state_data: The existing state data to update, or None if no state data available
|
|
49
|
+
token_endpoint_response: The response from the token endpoint
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Updated state data
|
|
53
|
+
"""
|
|
54
|
+
current_time = int(time.time())
|
|
55
|
+
|
|
56
|
+
if state_data and hasattr(state_data, "dict") and callable(state_data.dict):
|
|
57
|
+
state_data_dict = state_data.dict()
|
|
58
|
+
else:
|
|
59
|
+
state_data_dict = state_data or {}
|
|
60
|
+
|
|
61
|
+
if state_data_dict:
|
|
62
|
+
# Check if we need to add a new token set or update an existing one
|
|
63
|
+
is_new_token_set = True
|
|
64
|
+
token_sets = state_data_dict.get("token_sets", [])
|
|
65
|
+
|
|
66
|
+
for token_set in token_sets:
|
|
67
|
+
if (token_set.get("audience") == audience and
|
|
68
|
+
token_set.get("scope") == token_endpoint_response.get("scope")):
|
|
69
|
+
is_new_token_set = False
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
# Create the updated token set
|
|
73
|
+
updated_token_set = {
|
|
74
|
+
"audience": audience,
|
|
75
|
+
"access_token": token_endpoint_response.get("access_token"),
|
|
76
|
+
"scope": token_endpoint_response.get("scope"),
|
|
77
|
+
"expires_at": current_time + int(token_endpoint_response.get("expires_in", 0))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Update or add the token set
|
|
81
|
+
if is_new_token_set:
|
|
82
|
+
token_sets = token_sets + [updated_token_set]
|
|
83
|
+
else:
|
|
84
|
+
token_sets = [
|
|
85
|
+
updated_token_set if (ts.get("audience") == audience and
|
|
86
|
+
ts.get("scope") == token_endpoint_response.get("scope"))
|
|
87
|
+
else ts
|
|
88
|
+
for ts in token_sets
|
|
89
|
+
]
|
|
90
|
+
# Return updated state data
|
|
91
|
+
return {
|
|
92
|
+
**state_data_dict,
|
|
93
|
+
"id_token": token_endpoint_response.get("id_token"),
|
|
94
|
+
"refresh_token": token_endpoint_response.get("refresh_token") or state_data_dict.get("refresh_token"),
|
|
95
|
+
"token_sets": token_sets
|
|
96
|
+
}
|
|
97
|
+
else:
|
|
98
|
+
# Create completely new state data
|
|
99
|
+
user = token_endpoint_response.get("claims", {})
|
|
100
|
+
return {
|
|
101
|
+
"user": user,
|
|
102
|
+
"id_token": token_endpoint_response.get("id_token"),
|
|
103
|
+
"refresh_token": token_endpoint_response.get("refresh_token"),
|
|
104
|
+
"token_sets": [
|
|
105
|
+
{
|
|
106
|
+
"audience": audience,
|
|
107
|
+
"access_token": token_endpoint_response.get("access_token"),
|
|
108
|
+
"scope": token_endpoint_response.get("scope"),
|
|
109
|
+
"expires_at": current_time + int(token_endpoint_response.get("expires_in", 0))
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"internal": {
|
|
113
|
+
"sid": user.get("sid", ""),
|
|
114
|
+
"created_at": current_time
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def update_state_data_for_connection_token_set(
|
|
121
|
+
cls,
|
|
122
|
+
options: Dict[str, Any],
|
|
123
|
+
state_data: Dict[str, Any],
|
|
124
|
+
token_endpoint_response: Dict[str, Any]
|
|
125
|
+
) -> Dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Update state data with connection token set information
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
options: Options containing connection details
|
|
131
|
+
state_data: Existing state data
|
|
132
|
+
token_endpoint_response: Response from token endpoint
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Updated state data
|
|
136
|
+
"""
|
|
137
|
+
# Initialize connection_token_sets if it doesn't exist
|
|
138
|
+
connection_token_sets = state_data.get("connection_token_sets", [])
|
|
139
|
+
|
|
140
|
+
# Check if we need to add a new token set or update an existing one
|
|
141
|
+
is_new_token_set = True
|
|
142
|
+
|
|
143
|
+
for token_set in connection_token_sets:
|
|
144
|
+
if (token_set.get("connection") == options.get("connection") and
|
|
145
|
+
(not options.get("login_hint") or token_set.get("login_hint") == options.get("login_hint"))):
|
|
146
|
+
is_new_token_set = False
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
# Create the connection token set
|
|
150
|
+
connection_token_set = {
|
|
151
|
+
"connection": options.get("connection"),
|
|
152
|
+
"login_hint": options.get("login_hint"),
|
|
153
|
+
"access_token": token_endpoint_response.get("access_token"),
|
|
154
|
+
"scope": token_endpoint_response.get("scope"),
|
|
155
|
+
"expires_at": int(time.time()) + int(token_endpoint_response.get("expires_in", 0))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Update or add the token set
|
|
159
|
+
if is_new_token_set:
|
|
160
|
+
connection_token_sets = connection_token_sets + [connection_token_set]
|
|
161
|
+
else:
|
|
162
|
+
connection_token_sets = [
|
|
163
|
+
connection_token_set if (ts.get("connection") == options.get("connection") and
|
|
164
|
+
(not options.get("login_hint") or
|
|
165
|
+
ts.get("login_hint") == options.get("login_hint")))
|
|
166
|
+
else ts
|
|
167
|
+
for ts in connection_token_sets
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
# Return updated state data
|
|
171
|
+
return {
|
|
172
|
+
**state_data,
|
|
173
|
+
"connection_token_sets": connection_token_sets
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class URL:
|
|
177
|
+
@staticmethod
|
|
178
|
+
def build_url(base_url: str, params: Dict[str, Any]) -> str:
|
|
179
|
+
"""
|
|
180
|
+
Build a complete URL by appending query parameters to a base URL.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
base_url: The base URL without query parameters.
|
|
184
|
+
params: A dictionary of query parameters to add.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The complete URL with the query parameters appended.
|
|
188
|
+
"""
|
|
189
|
+
query_string = urlencode(params)
|
|
190
|
+
separator = '?' if '?' not in base_url else '&'
|
|
191
|
+
return f"{base_url}{separator}{query_string}" if query_string else base_url
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def parse_url_params(url: str) -> Dict[str, str]:
|
|
195
|
+
"""
|
|
196
|
+
Parse the query parameters from a URL.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
url: The URL to parse.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A dictionary of query parameters, converting list values to a single string.
|
|
203
|
+
"""
|
|
204
|
+
parsed_url = urlparse(url)
|
|
205
|
+
query_params = parse_qs(parsed_url.query)
|
|
206
|
+
return {k: v[0] if v and len(v) > 0 else '' for k, v in query_params.items()}
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def create_logout_url(domain: str, client_id: str, return_to: Optional[str] = None) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Create an Auth0 logout URL.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
domain: Auth0 domain.
|
|
215
|
+
client_id: Auth0 client ID.
|
|
216
|
+
return_to: Optional URL to redirect to after logout.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The complete logout URL.
|
|
220
|
+
"""
|
|
221
|
+
base_url = f"https://{domain}/v2/logout"
|
|
222
|
+
params = {"client_id": client_id}
|
|
223
|
+
if return_to:
|
|
224
|
+
params["returnTo"] = return_to
|
|
225
|
+
return URL.build_url(base_url, params)
|