earthscope-sdk 0.2.1__py3-none-any.whl → 1.0.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.
- earthscope_sdk/__init__.py +5 -1
- earthscope_sdk/auth/auth_flow.py +240 -346
- earthscope_sdk/auth/client_credentials_flow.py +42 -162
- earthscope_sdk/auth/device_code_flow.py +169 -213
- earthscope_sdk/auth/error.py +46 -0
- earthscope_sdk/client/__init__.py +3 -0
- earthscope_sdk/client/_client.py +35 -0
- earthscope_sdk/client/user/_base.py +39 -0
- earthscope_sdk/client/user/_service.py +94 -0
- earthscope_sdk/client/user/models.py +53 -0
- earthscope_sdk/common/__init__.py +0 -0
- earthscope_sdk/common/_sync_runner.py +141 -0
- earthscope_sdk/common/client.py +99 -0
- earthscope_sdk/common/context.py +174 -0
- earthscope_sdk/common/service.py +59 -0
- earthscope_sdk/config/__init__.py +0 -0
- earthscope_sdk/config/_bootstrap.py +42 -0
- earthscope_sdk/config/_compat.py +148 -0
- earthscope_sdk/config/_util.py +48 -0
- earthscope_sdk/config/error.py +4 -0
- earthscope_sdk/config/models.py +310 -0
- earthscope_sdk/config/settings.py +295 -0
- earthscope_sdk/model/secret.py +29 -0
- {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info}/METADATA +147 -123
- earthscope_sdk-1.0.0.dist-info/RECORD +30 -0
- {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info}/WHEEL +1 -1
- earthscope_sdk/user/user.py +0 -24
- earthscope_sdk-0.2.1.dist-info/RECORD +0 -12
- /earthscope_sdk/{user → client/user}/__init__.py +0 -0
- {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info/licenses}/LICENSE +0 -0
- {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0.dist-info}/top_level.txt +0 -0
earthscope_sdk/__init__.py
CHANGED
earthscope_sdk/auth/auth_flow.py
CHANGED
@@ -1,441 +1,335 @@
|
|
1
1
|
import datetime as dt
|
2
|
-
import json
|
3
2
|
import logging
|
3
|
+
from contextlib import suppress
|
4
|
+
from typing import Optional
|
4
5
|
|
5
|
-
|
6
|
-
from
|
7
|
-
from typing import Dict, Optional, Union
|
8
|
-
import jwt
|
6
|
+
import httpx
|
7
|
+
from pydantic import ValidationError
|
9
8
|
|
10
|
-
import
|
9
|
+
from earthscope_sdk.auth.error import (
|
10
|
+
InvalidRefreshTokenError,
|
11
|
+
NoAccessTokenError,
|
12
|
+
NoRefreshTokenError,
|
13
|
+
NoTokensError,
|
14
|
+
)
|
15
|
+
from earthscope_sdk.common.context import SdkContext
|
16
|
+
from earthscope_sdk.config.models import AccessTokenBody, Tokens
|
11
17
|
|
12
18
|
logger = logging.getLogger(__name__)
|
13
19
|
|
14
20
|
|
15
|
-
class
|
16
|
-
"""Generic authentication flow error"""
|
17
|
-
|
18
|
-
|
19
|
-
class UnauthorizedError(AuthFlowError):
|
20
|
-
pass
|
21
|
-
|
22
|
-
|
23
|
-
class NoTokensError(AuthFlowError):
|
24
|
-
pass
|
25
|
-
|
26
|
-
|
27
|
-
class NoIdTokenError(NoTokensError):
|
28
|
-
pass
|
29
|
-
|
30
|
-
|
31
|
-
class NoRefreshTokenError(NoTokensError):
|
32
|
-
pass
|
33
|
-
|
34
|
-
|
35
|
-
class InvalidRefreshTokenError(AuthFlowError):
|
36
|
-
pass
|
37
|
-
|
38
|
-
|
39
|
-
@dataclass
|
40
|
-
class TokensResponse:
|
41
|
-
access_token: str
|
42
|
-
refresh_token: Optional[str]
|
43
|
-
id_token: Optional[str]
|
44
|
-
token_type: str
|
45
|
-
expires_in: int
|
46
|
-
scope: Optional[str]
|
47
|
-
|
48
|
-
|
49
|
-
@dataclass
|
50
|
-
class UnvalidatedTokens:
|
51
|
-
access_token: str
|
52
|
-
id_token: Optional[str]
|
53
|
-
refresh_token: Optional[str]
|
54
|
-
scope: str
|
55
|
-
|
56
|
-
@classmethod
|
57
|
-
def from_dict(cls, o: Dict):
|
58
|
-
return cls(
|
59
|
-
access_token=o.get("access_token"),
|
60
|
-
id_token=o.get("id_token"),
|
61
|
-
refresh_token=o.get("refresh_token"),
|
62
|
-
scope=o.get("scope"),
|
63
|
-
)
|
64
|
-
|
65
|
-
|
66
|
-
@dataclass
|
67
|
-
class ValidTokens(UnvalidatedTokens):
|
68
|
-
expires_at: int
|
69
|
-
issued_at: int
|
70
|
-
|
71
|
-
@classmethod
|
72
|
-
def from_unvalidated(cls, creds: UnvalidatedTokens, body: Dict):
|
73
|
-
return cls(
|
74
|
-
access_token=creds.access_token,
|
75
|
-
id_token=creds.id_token,
|
76
|
-
refresh_token=creds.refresh_token,
|
77
|
-
scope=creds.scope,
|
78
|
-
expires_at=body.get("exp"),
|
79
|
-
issued_at=body.get("iat"),
|
80
|
-
)
|
81
|
-
|
82
|
-
|
83
|
-
class AuthFlow(ABC):
|
21
|
+
class AuthFlow(httpx.Auth):
|
84
22
|
"""
|
85
|
-
|
86
|
-
|
87
|
-
Attributes
|
88
|
-
__________
|
89
|
-
domain : str
|
90
|
-
Auth0 tenant domain URL or custom domain (default is 'login.earthscope.org')
|
91
|
-
audience : str
|
92
|
-
Auth0 API Identifier (default is 'https://account.earthscope.org')
|
93
|
-
client_id : str
|
94
|
-
Identification value of Auth0 Application (default is 'b9DtAFBd6QvMg761vI3YhYquNZbJX5G0')
|
95
|
-
scope : str
|
96
|
-
The specific actions Auth0 applications can be allowed to do or information that they can request on a user’s behalf.
|
97
|
-
_tokens : str, optional
|
98
|
-
Access or ID Token
|
99
|
-
_access_token_body : dict, optional
|
100
|
-
Body of the decrypted access token
|
101
|
-
_id_token_body : dict, optional
|
102
|
-
Body of the decrypted id token
|
103
|
-
|
104
|
-
Methods
|
105
|
-
_______
|
106
|
-
load_tokens
|
107
|
-
Load the token (abstract method)
|
108
|
-
save_tokens(creds)
|
109
|
-
Save the token (abstract method)
|
110
|
-
refresh(scope=None, revoke=False)
|
111
|
-
Refresh the access token or revoke the refresh token
|
112
|
-
validate_and_save_tokens(unvalidated_creds)
|
113
|
-
Validate and save the token
|
114
|
-
validate_tokens(unvalidated_creds)
|
115
|
-
Validate the token
|
116
|
-
get_access_token_refresh_if_necessary(no_auto_refresh=False, auto_refresh_threshold=3600)
|
117
|
-
Retrieve the access token. Automatically refreshes the token when necessary.
|
118
|
-
|
23
|
+
Generic oauth2 flow class for handling retrieving, validating, saving, and refreshing access tokens.
|
119
24
|
"""
|
25
|
+
|
120
26
|
@property
|
121
27
|
def access_token(self):
|
122
28
|
"""
|
123
|
-
|
124
|
-
|
125
|
-
Returns
|
126
|
-
-------
|
127
|
-
access token: str
|
128
|
-
"""
|
129
|
-
return self.tokens.access_token
|
29
|
+
Access token
|
130
30
|
|
131
|
-
|
132
|
-
|
31
|
+
Raises:
|
32
|
+
NoTokensError: no tokens (at all) are present
|
33
|
+
NoAccessTokenError: no access token is present
|
133
34
|
"""
|
134
|
-
|
35
|
+
if at := self.tokens.access_token:
|
36
|
+
return at.get_secret_value()
|
135
37
|
|
136
|
-
|
137
|
-
______
|
138
|
-
NoTokensError
|
139
|
-
If there is no token
|
140
|
-
|
141
|
-
Returns
|
142
|
-
-------
|
143
|
-
access_token_body: dict
|
144
|
-
"""
|
145
|
-
if self._access_token_body is None:
|
146
|
-
raise NoTokensError
|
147
|
-
return self._access_token_body
|
38
|
+
raise NoAccessTokenError("No access token was found. Please re-authenticate.")
|
148
39
|
|
149
40
|
@property
|
150
|
-
def
|
41
|
+
def access_token_body(self):
|
151
42
|
"""
|
152
|
-
|
153
|
-
|
154
|
-
raises
|
155
|
-
------
|
156
|
-
NoTokensError
|
157
|
-
If no token to retrieve
|
43
|
+
Access token body
|
158
44
|
|
159
|
-
|
160
|
-
|
161
|
-
tokens
|
45
|
+
Raises:
|
46
|
+
NoAccessTokenError: no access token is present
|
162
47
|
"""
|
163
|
-
if
|
164
|
-
|
48
|
+
if body := self._access_token_body:
|
49
|
+
return body
|
165
50
|
|
166
|
-
|
51
|
+
raise NoAccessTokenError("No access token was found. Please re-authenticate.")
|
167
52
|
|
168
53
|
@property
|
169
54
|
def expires_at(self):
|
170
55
|
"""
|
171
|
-
|
56
|
+
Time of access token expiration
|
172
57
|
|
173
|
-
|
174
|
-
|
175
|
-
datetime
|
58
|
+
Raises:
|
59
|
+
NoAccessTokenError: no access token is present
|
176
60
|
"""
|
177
|
-
return
|
61
|
+
return self.access_token_body.expires_at
|
178
62
|
|
179
63
|
@property
|
180
|
-
def
|
64
|
+
def has_refresh_token(self):
|
181
65
|
"""
|
182
|
-
|
183
|
-
|
184
|
-
Raises
|
185
|
-
------
|
186
|
-
NoIDTokenError
|
187
|
-
If there is no id token to be retrieved
|
188
|
-
|
189
|
-
Returns
|
190
|
-
-------
|
191
|
-
id token: srt
|
66
|
+
Whether or not we have a refresh token
|
192
67
|
"""
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
return idt
|
68
|
+
try:
|
69
|
+
return self.refresh_token is not None
|
70
|
+
except NoTokensError:
|
71
|
+
return False
|
198
72
|
|
199
73
|
@property
|
200
|
-
def
|
74
|
+
def issued_at(self):
|
201
75
|
"""
|
202
|
-
|
203
|
-
|
204
|
-
Raises
|
205
|
-
______
|
206
|
-
NoIdTokenError
|
207
|
-
If there is no id token
|
76
|
+
Time of access token creation
|
208
77
|
|
209
|
-
|
210
|
-
|
211
|
-
id_token_body: dict
|
78
|
+
Raises:
|
79
|
+
NoAccessTokenError: no access token is present
|
212
80
|
"""
|
213
|
-
|
214
|
-
raise NoIdTokenError
|
215
|
-
return self._id_token_body
|
81
|
+
return self.access_token_body.issued_at
|
216
82
|
|
217
83
|
@property
|
218
|
-
def
|
84
|
+
def refresh_token(self):
|
219
85
|
"""
|
220
|
-
|
86
|
+
Refresh token
|
221
87
|
|
222
|
-
|
223
|
-
|
224
|
-
|
88
|
+
Raises:
|
89
|
+
NoTokensError: no tokens (at all) are present
|
90
|
+
NoRefreshTokenError: no refresh token is present
|
225
91
|
"""
|
226
|
-
|
92
|
+
if rt := self.tokens.refresh_token:
|
93
|
+
return rt.get_secret_value()
|
94
|
+
|
95
|
+
raise NoRefreshTokenError("No refresh token was found. Please re-authenticate.")
|
227
96
|
|
228
97
|
@property
|
229
|
-
def
|
98
|
+
def scope(self):
|
230
99
|
"""
|
231
|
-
|
100
|
+
Access token scope
|
232
101
|
|
233
|
-
Raises
|
234
|
-
|
235
|
-
NoRefreshTokenError
|
236
|
-
If there is no refresh token to be retrieved
|
237
|
-
|
238
|
-
Returns
|
239
|
-
-------
|
240
|
-
refresh token: srt
|
102
|
+
Raises:
|
103
|
+
NoAccessTokenError: no access token is present
|
241
104
|
"""
|
242
|
-
|
243
|
-
if not rt:
|
244
|
-
raise NoRefreshTokenError
|
245
|
-
|
246
|
-
return rt
|
105
|
+
return set(self.access_token_body.scope.split())
|
247
106
|
|
248
107
|
@property
|
249
|
-
def
|
108
|
+
def tokens(self):
|
250
109
|
"""
|
251
|
-
|
110
|
+
Oauth2 tokens managed by this auth flow
|
252
111
|
|
253
|
-
|
254
|
-
|
255
|
-
boolean
|
112
|
+
Raises:
|
113
|
+
NoTokensError: no tokens (at all) are present
|
256
114
|
"""
|
257
|
-
|
258
|
-
return
|
259
|
-
|
260
|
-
|
115
|
+
if tokens := self._tokens:
|
116
|
+
return tokens
|
117
|
+
|
118
|
+
raise NoTokensError("No tokens were found. Please re-authenticate.")
|
261
119
|
|
262
120
|
@property
|
263
121
|
def ttl(self):
|
264
122
|
"""
|
265
|
-
|
123
|
+
Access token time-to-live (ttl) before expiration
|
266
124
|
|
267
|
-
|
268
|
-
|
269
|
-
datetime
|
125
|
+
Raises:
|
126
|
+
NoAccessTokenError: no access token is present
|
270
127
|
"""
|
271
|
-
return self.
|
128
|
+
return self.access_token_body.ttl
|
129
|
+
|
130
|
+
def __init__(self, ctx: SdkContext) -> None:
|
131
|
+
self._ctx = ctx
|
132
|
+
self._settings = ctx.settings.oauth2
|
272
133
|
|
273
|
-
|
274
|
-
self.
|
275
|
-
self.
|
276
|
-
self.auth0_client_id = client_id
|
277
|
-
self.token_scope = scope
|
134
|
+
# Local state
|
135
|
+
self._access_token_body: Optional[AccessTokenBody] = None
|
136
|
+
self._tokens: Optional[Tokens] = None
|
278
137
|
|
279
|
-
|
280
|
-
|
281
|
-
self.
|
138
|
+
# httpx.Auth objects can be used in either sync or async clients so we
|
139
|
+
# facilitate both from the same class
|
140
|
+
self.refresh = ctx.syncify(self.async_refresh)
|
141
|
+
self.refresh_if_necessary = ctx.syncify(self.async_refresh_if_necessary)
|
142
|
+
self.revoke_refresh_token = ctx.syncify(self.async_revoke_refresh_token)
|
282
143
|
|
283
|
-
|
284
|
-
|
144
|
+
# Initialize with provided tokens
|
145
|
+
with suppress(ValidationError):
|
146
|
+
self._validate_tokens(self._settings)
|
147
|
+
|
148
|
+
async def async_refresh(self, scope: Optional[str] = None):
|
285
149
|
"""
|
286
|
-
|
150
|
+
Refresh the access token
|
151
|
+
|
152
|
+
Args:
|
153
|
+
scope: the specific oauth2 scopes to request
|
154
|
+
|
155
|
+
Raises:
|
156
|
+
NoTokensError: no tokens (at all) are present
|
157
|
+
NoRefreshTokenError: no refresh token is present
|
158
|
+
InvalidRefreshTokenError: the token refresh failed
|
159
|
+
"""
|
160
|
+
from httpx import HTTPStatusError, ReadTimeout
|
161
|
+
|
162
|
+
refresh_token = self.refresh_token
|
163
|
+
scope = scope or self._settings.scope
|
164
|
+
|
165
|
+
request = self._ctx.httpx_client.build_request(
|
166
|
+
"POST",
|
167
|
+
f"{self._settings.domain}oauth/token",
|
168
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
169
|
+
data={
|
170
|
+
"grant_type": "refresh_token",
|
171
|
+
"client_id": self._settings.client_id,
|
172
|
+
"refresh_token": refresh_token,
|
173
|
+
"scopes": scope,
|
174
|
+
},
|
175
|
+
)
|
176
|
+
|
177
|
+
try:
|
178
|
+
async for attempt in self._settings.retry.retry_context(ReadTimeout):
|
179
|
+
with attempt:
|
180
|
+
r = await self._ctx.httpx_client.send(request, auth=None)
|
181
|
+
r.raise_for_status()
|
182
|
+
except HTTPStatusError as e:
|
183
|
+
logger.error(
|
184
|
+
f"error during token refresh ({attempt.num} attempts): {e.response.content}"
|
185
|
+
)
|
186
|
+
raise InvalidRefreshTokenError("refresh token exchange failed")
|
187
|
+
except Exception as e:
|
188
|
+
logger.error(
|
189
|
+
f"error during token refresh ({attempt.num} attempts)", exc_info=e
|
190
|
+
)
|
191
|
+
raise InvalidRefreshTokenError("refresh token exchange failed") from e
|
192
|
+
|
193
|
+
# add previous refresh token to new tokens if omitted from resp
|
194
|
+
# (i.e. we have a non-rotating refresh token)
|
195
|
+
resp: dict = r.json()
|
196
|
+
resp.setdefault("refresh_token", refresh_token)
|
197
|
+
|
198
|
+
self._validate_and_save_tokens(resp)
|
199
|
+
|
200
|
+
logger.debug(f"Refreshed tokens: {self._tokens}")
|
201
|
+
return self
|
202
|
+
|
203
|
+
async def async_refresh_if_necessary(
|
204
|
+
self,
|
205
|
+
scope: Optional[str] = None,
|
206
|
+
auto_refresh_threshold: int = 60,
|
207
|
+
):
|
287
208
|
"""
|
288
|
-
|
209
|
+
Refresh the access token if it is expired or bootstrap the access token from a refresh token
|
210
|
+
|
211
|
+
Args:
|
212
|
+
scope: the specific oauth2 scopes to request
|
213
|
+
auto_refresh_threshold: access token TTL remaining (in seconds) before auto-refreshing
|
289
214
|
|
290
|
-
|
291
|
-
|
215
|
+
Raises:
|
216
|
+
NoTokensError: no tokens (at all) are present
|
217
|
+
NoRefreshTokenError: no refresh token is present
|
218
|
+
InvalidRefreshTokenError: the token refresh failed
|
292
219
|
"""
|
293
|
-
|
220
|
+
# Suppress to allow bootstrapping with a refresh token
|
221
|
+
with suppress(NoAccessTokenError):
|
222
|
+
if self.ttl >= dt.timedelta(seconds=auto_refresh_threshold):
|
223
|
+
return self
|
224
|
+
|
225
|
+
return await self.async_refresh(scope=scope)
|
294
226
|
|
295
|
-
|
296
|
-
----------
|
297
|
-
creds : ValidTokens
|
227
|
+
async def async_revoke_refresh_token(self):
|
298
228
|
"""
|
299
|
-
|
229
|
+
Revoke the refresh token.
|
230
|
+
|
231
|
+
This invalidates the refresh token server-side so it may never again be used to
|
232
|
+
get new access tokens.
|
300
233
|
|
301
|
-
|
234
|
+
Raises:
|
235
|
+
NoTokensError: no tokens (at all) are present
|
236
|
+
NoRefreshTokenError: no refresh token is present
|
237
|
+
InvalidRefreshTokenError: the token revocation failed
|
302
238
|
"""
|
303
|
-
|
239
|
+
refresh_token = self.refresh_token
|
304
240
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
InvalidRefreshTokenError
|
315
|
-
If the token refresh fails
|
316
|
-
"""
|
317
|
-
scope = scope or self.token_scope
|
318
|
-
|
319
|
-
if not self.has_refresh_token:
|
320
|
-
raise NoRefreshTokenError("No refresh token was found. Please re-authenticate.")
|
321
|
-
|
322
|
-
if revoke:
|
323
|
-
r = requests.post(
|
324
|
-
f"https://{self.auth0_domain}/oauth/revoke",
|
325
|
-
headers={"content-type": "application/json"},
|
326
|
-
data=json.dumps({
|
327
|
-
"client_id": self.auth0_client_id,
|
328
|
-
"token": self.refresh_token,
|
329
|
-
}),
|
330
|
-
)
|
331
|
-
if r.status_code == 200:
|
332
|
-
logger.debug(f"Refresh token revoked: {self.refresh_token}")
|
333
|
-
return
|
334
|
-
else:
|
335
|
-
raise RuntimeError(r.json()["detail"])
|
336
|
-
|
337
|
-
|
338
|
-
else:
|
339
|
-
r = requests.post(
|
340
|
-
f"https://{self.auth0_domain}/oauth/token",
|
341
|
-
headers={"content-type": "application/x-www-form-urlencoded"},
|
342
|
-
data={
|
343
|
-
"grant_type": "refresh_token",
|
344
|
-
"client_id": self.auth0_client_id,
|
345
|
-
"refresh_token": self.refresh_token,
|
346
|
-
"scopes": scope,
|
347
|
-
},
|
348
|
-
)
|
241
|
+
r = await self._ctx.httpx_client.post(
|
242
|
+
f"{self._settings.domain}oauth/revoke",
|
243
|
+
auth=None, # override client default
|
244
|
+
headers={"content-type": "application/json"},
|
245
|
+
json={
|
246
|
+
"client_id": self._settings.client_id,
|
247
|
+
"token": refresh_token,
|
248
|
+
},
|
249
|
+
)
|
349
250
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
self.validate_and_save_tokens(r.json())
|
354
|
-
if not self.has_refresh_token:
|
355
|
-
self._tokens.refresh_token = refresh_token
|
356
|
-
self.save_tokens(self._tokens)
|
357
|
-
logger.debug(f"Refreshed tokens: {self._tokens}")
|
358
|
-
return self
|
251
|
+
if r.status_code != 200:
|
252
|
+
logger.error(f"error while revoking refresh token: {r.content}")
|
253
|
+
raise InvalidRefreshTokenError("refresh token revocation failed")
|
359
254
|
|
360
|
-
|
255
|
+
logger.debug(f"Refresh token revoked: {refresh_token}")
|
256
|
+
return self
|
361
257
|
|
362
|
-
def
|
258
|
+
def _validate_and_save_tokens(self, unvalidated_tokens: dict):
|
363
259
|
"""
|
364
|
-
Validate
|
260
|
+
Validate then save tokens to local storage
|
365
261
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
262
|
+
Args:
|
263
|
+
unvalidated_tokens: tokens that have yet to be validated
|
264
|
+
|
265
|
+
Returns:
|
266
|
+
this auth flow
|
267
|
+
|
268
|
+
Raises:
|
269
|
+
pydantic.ValidationError: the token's body could not be decoded
|
370
270
|
"""
|
371
|
-
self.
|
271
|
+
self._validate_tokens(unvalidated_tokens)
|
272
|
+
|
372
273
|
try:
|
373
|
-
self.
|
274
|
+
self._ctx.settings.write_tokens(self._tokens)
|
374
275
|
except Exception as e:
|
375
276
|
logger.error("Error while persisting tokens", exc_info=e)
|
277
|
+
raise
|
376
278
|
|
377
279
|
return self
|
378
280
|
|
379
|
-
def
|
281
|
+
def _validate_tokens(self, unvalidated_tokens: dict):
|
380
282
|
"""
|
381
|
-
Validate the
|
283
|
+
Validate the tokens
|
382
284
|
|
383
|
-
|
384
|
-
|
385
|
-
unvalidated_creds: dict
|
386
|
-
The unvalidated credentials to be validated
|
387
|
-
"""
|
388
|
-
creds = UnvalidatedTokens.from_dict(unvalidated_creds)
|
389
|
-
idt_body: Optional[Dict] = None
|
285
|
+
Args:
|
286
|
+
unvalidated_tokens: tokens that have yet to be validated
|
390
287
|
|
391
|
-
|
392
|
-
|
393
|
-
creds.access_token,
|
394
|
-
options={"verify_signature": False},
|
395
|
-
)
|
288
|
+
Returns:
|
289
|
+
this auth flow
|
396
290
|
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
options={"verify_signature": False},
|
401
|
-
)
|
402
|
-
except Exception as e:
|
403
|
-
logger.error("Invalid tokens", exc_info=e)
|
404
|
-
raise
|
291
|
+
Raises:
|
292
|
+
pydantic.ValidationError: the token's body could not be decoded
|
293
|
+
"""
|
405
294
|
|
406
|
-
|
407
|
-
|
408
|
-
self.
|
409
|
-
|
410
|
-
body=at_body,
|
411
|
-
)
|
412
|
-
self.token_scope = self._tokens.scope
|
295
|
+
tokens = Tokens.model_validate(unvalidated_tokens)
|
296
|
+
|
297
|
+
self._access_token_body = tokens.access_token_body
|
298
|
+
self._tokens = tokens
|
413
299
|
|
414
300
|
return self
|
415
301
|
|
416
|
-
|
302
|
+
##########
|
303
|
+
# The following methods make this class compatible with httpx.Auth.
|
304
|
+
##########
|
305
|
+
|
306
|
+
async def async_auth_flow(self, request: httpx.Request):
|
307
|
+
"""
|
308
|
+
Injects authorization into the request
|
309
|
+
|
310
|
+
(this method makes this class httpx.Auth compatible)
|
417
311
|
"""
|
418
|
-
|
312
|
+
super().async_auth_flow
|
313
|
+
if request.headers.get("authorization") is None:
|
314
|
+
if self._settings.is_host_allowed(request.url.host):
|
315
|
+
await self.async_refresh_if_necessary()
|
316
|
+
access_token = self.access_token
|
317
|
+
request.headers["authorization"] = f"Bearer {access_token}"
|
419
318
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
The amount of time remaining (in seconds) before token expiration after which a refresh is automatically attempted (default is 3600)
|
319
|
+
yield request
|
320
|
+
|
321
|
+
def sync_auth_flow(self, request: httpx.Request):
|
322
|
+
"""
|
323
|
+
Injects authorization into the request
|
426
324
|
|
427
|
-
|
428
|
-
________
|
429
|
-
access_token: str
|
325
|
+
(this method makes this class httpx.Auth compatible)
|
430
326
|
"""
|
327
|
+
# NOTE: we explicitly redefine this sync method because ctx.syncify()
|
328
|
+
# does not support generators
|
329
|
+
if request.headers.get("authorization") is None:
|
330
|
+
if self._settings.is_host_allowed(request.url.host):
|
331
|
+
self.refresh_if_necessary()
|
332
|
+
access_token = self.access_token
|
333
|
+
request.headers["authorization"] = f"Bearer {access_token}"
|
431
334
|
|
432
|
-
|
433
|
-
if (
|
434
|
-
not no_auto_refresh
|
435
|
-
and self.ttl < dt.timedelta(seconds=auto_refresh_threshold)
|
436
|
-
):
|
437
|
-
try:
|
438
|
-
self.refresh()
|
439
|
-
except InvalidRefreshTokenError:
|
440
|
-
raise InvalidRefreshTokenError("Unable to refresh because the refresh token is not valid. Use 'no-auto-refresh' option to get token anyway. To resolve, re-authenticate")
|
441
|
-
return self.access_token
|
335
|
+
yield request
|