earthscope-sdk 0.2.1__py3-none-any.whl → 1.0.0b0__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 +224 -347
- earthscope_sdk/auth/client_credentials_flow.py +46 -156
- earthscope_sdk/auth/device_code_flow.py +154 -207
- 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 +54 -0
- earthscope_sdk/config/__init__.py +0 -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 +208 -0
- earthscope_sdk/config/settings.py +284 -0
- {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0b0.dist-info}/METADATA +144 -123
- earthscope_sdk-1.0.0b0.dist-info/RECORD +28 -0
- {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0b0.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.0b0.dist-info}/LICENSE +0 -0
- {earthscope_sdk-0.2.1.dist-info → earthscope_sdk-1.0.0b0.dist-info}/top_level.txt +0 -0
earthscope_sdk/__init__.py
CHANGED
earthscope_sdk/auth/auth_flow.py
CHANGED
@@ -1,441 +1,318 @@
|
|
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
|
-
|
43
|
+
Access token body
|
153
44
|
|
154
|
-
|
155
|
-
|
156
|
-
NoTokensError
|
157
|
-
If no token to retrieve
|
158
|
-
|
159
|
-
Returns
|
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
|
-
|
232
|
-
|
233
|
-
Raises
|
234
|
-
------
|
235
|
-
NoRefreshTokenError
|
236
|
-
If there is no refresh token to be retrieved
|
100
|
+
Access token scope
|
237
101
|
|
238
|
-
|
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
|
133
|
+
|
134
|
+
# Local state
|
135
|
+
self._access_token_body: Optional[AccessTokenBody] = None
|
136
|
+
self._tokens: Optional[Tokens] = None
|
272
137
|
|
273
|
-
|
274
|
-
|
275
|
-
self.
|
276
|
-
self.
|
277
|
-
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)
|
278
143
|
|
279
|
-
|
280
|
-
|
281
|
-
|
144
|
+
# Initialize with provided tokens
|
145
|
+
with suppress(ValidationError):
|
146
|
+
self._validate_tokens(self._settings)
|
282
147
|
|
283
|
-
|
284
|
-
def load_tokens(self):
|
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
|
+
refresh_token = self.refresh_token
|
161
|
+
scope = scope or self._settings.scope
|
162
|
+
|
163
|
+
r = await self._ctx.httpx_client.post(
|
164
|
+
f"{self._settings.domain}oauth/token",
|
165
|
+
auth=None, # override client default
|
166
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
167
|
+
data={
|
168
|
+
"grant_type": "refresh_token",
|
169
|
+
"client_id": self._settings.client_id,
|
170
|
+
"refresh_token": refresh_token,
|
171
|
+
"scopes": scope,
|
172
|
+
},
|
173
|
+
)
|
174
|
+
if r.status_code != 200:
|
175
|
+
logger.error(f"error during token refresh: {r.content}")
|
176
|
+
raise InvalidRefreshTokenError("refresh token exchange failed")
|
177
|
+
|
178
|
+
# add previous refresh token to new tokens if omitted from resp
|
179
|
+
# (i.e. we have a non-rotating refresh token)
|
180
|
+
resp: dict = r.json()
|
181
|
+
resp.setdefault("refresh_token", refresh_token)
|
182
|
+
|
183
|
+
self._validate_and_save_tokens(resp)
|
184
|
+
|
185
|
+
logger.debug(f"Refreshed tokens: {self._tokens}")
|
186
|
+
return self
|
187
|
+
|
188
|
+
async def async_refresh_if_necessary(
|
189
|
+
self,
|
190
|
+
scope: Optional[str] = None,
|
191
|
+
auto_refresh_threshold: int = 60,
|
192
|
+
):
|
287
193
|
"""
|
288
|
-
|
194
|
+
Refresh the access token if it is expired or bootstrap the access token from a refresh token
|
195
|
+
|
196
|
+
Args:
|
197
|
+
scope: the specific oauth2 scopes to request
|
198
|
+
auto_refresh_threshold: access token TTL remaining (in seconds) before auto-refreshing
|
289
199
|
|
290
|
-
|
291
|
-
|
200
|
+
Raises:
|
201
|
+
NoTokensError: no tokens (at all) are present
|
202
|
+
NoRefreshTokenError: no refresh token is present
|
203
|
+
InvalidRefreshTokenError: the token refresh failed
|
292
204
|
"""
|
293
|
-
|
205
|
+
# Suppress to allow bootstrapping with a refresh token
|
206
|
+
with suppress(NoAccessTokenError):
|
207
|
+
if self.ttl >= dt.timedelta(seconds=auto_refresh_threshold):
|
208
|
+
return self
|
209
|
+
|
210
|
+
return await self.async_refresh(scope=scope)
|
294
211
|
|
295
|
-
|
296
|
-
----------
|
297
|
-
creds : ValidTokens
|
212
|
+
async def async_revoke_refresh_token(self):
|
298
213
|
"""
|
299
|
-
|
214
|
+
Revoke the refresh token.
|
300
215
|
|
301
|
-
|
216
|
+
This invalidates the refresh token server-side so it may never again be used to
|
217
|
+
get new access tokens.
|
218
|
+
|
219
|
+
Raises:
|
220
|
+
NoTokensError: no tokens (at all) are present
|
221
|
+
NoRefreshTokenError: no refresh token is present
|
222
|
+
InvalidRefreshTokenError: the token revocation failed
|
302
223
|
"""
|
303
|
-
|
224
|
+
refresh_token = self.refresh_token
|
304
225
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
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
|
-
)
|
349
|
-
|
350
|
-
# add previous refresh token to new access token
|
351
|
-
if r.status_code == 200:
|
352
|
-
refresh_token = self.refresh_token
|
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
|
226
|
+
r = await self._ctx.httpx_client.post(
|
227
|
+
f"{self._settings.domain}oauth/revoke",
|
228
|
+
auth=None, # override client default
|
229
|
+
headers={"content-type": "application/json"},
|
230
|
+
json={
|
231
|
+
"client_id": self._settings.client_id,
|
232
|
+
"token": refresh_token,
|
233
|
+
},
|
234
|
+
)
|
235
|
+
|
236
|
+
if r.status_code != 200:
|
237
|
+
logger.error(f"error while revoking refresh token: {r.content}")
|
238
|
+
raise InvalidRefreshTokenError("refresh token revocation failed")
|
359
239
|
|
360
|
-
|
240
|
+
logger.debug(f"Refresh token revoked: {refresh_token}")
|
241
|
+
return self
|
361
242
|
|
362
|
-
def
|
243
|
+
def _validate_and_save_tokens(self, unvalidated_tokens: dict):
|
363
244
|
"""
|
364
|
-
Validate
|
245
|
+
Validate then save tokens to local storage
|
365
246
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
247
|
+
Args:
|
248
|
+
unvalidated_tokens: tokens that have yet to be validated
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
this auth flow
|
252
|
+
|
253
|
+
Raises:
|
254
|
+
pydantic.ValidationError: the token's body could not be decoded
|
370
255
|
"""
|
371
|
-
self.
|
256
|
+
self._validate_tokens(unvalidated_tokens)
|
257
|
+
|
372
258
|
try:
|
373
|
-
self.
|
259
|
+
self._ctx.settings.write_tokens(self._tokens)
|
374
260
|
except Exception as e:
|
375
261
|
logger.error("Error while persisting tokens", exc_info=e)
|
262
|
+
raise
|
376
263
|
|
377
264
|
return self
|
378
265
|
|
379
|
-
def
|
266
|
+
def _validate_tokens(self, unvalidated_tokens: dict):
|
380
267
|
"""
|
381
|
-
Validate the
|
268
|
+
Validate the tokens
|
269
|
+
|
270
|
+
Args:
|
271
|
+
unvalidated_tokens: tokens that have yet to be validated
|
382
272
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
273
|
+
Returns:
|
274
|
+
this auth flow
|
275
|
+
|
276
|
+
Raises:
|
277
|
+
pydantic.ValidationError: the token's body could not be decoded
|
387
278
|
"""
|
388
|
-
creds = UnvalidatedTokens.from_dict(unvalidated_creds)
|
389
|
-
idt_body: Optional[Dict] = None
|
390
279
|
|
391
|
-
|
392
|
-
at_body = jwt.decode(
|
393
|
-
creds.access_token,
|
394
|
-
options={"verify_signature": False},
|
395
|
-
)
|
396
|
-
|
397
|
-
if creds.id_token:
|
398
|
-
idt_body = jwt.decode(
|
399
|
-
creds.id_token,
|
400
|
-
options={"verify_signature": False},
|
401
|
-
)
|
402
|
-
except Exception as e:
|
403
|
-
logger.error("Invalid tokens", exc_info=e)
|
404
|
-
raise
|
280
|
+
tokens = Tokens.model_validate(unvalidated_tokens)
|
405
281
|
|
406
|
-
self._access_token_body =
|
407
|
-
self.
|
408
|
-
self._tokens = ValidTokens.from_unvalidated(
|
409
|
-
creds=creds,
|
410
|
-
body=at_body,
|
411
|
-
)
|
412
|
-
self.token_scope = self._tokens.scope
|
282
|
+
self._access_token_body = tokens.access_token_body
|
283
|
+
self._tokens = tokens
|
413
284
|
|
414
285
|
return self
|
415
286
|
|
416
|
-
|
287
|
+
##########
|
288
|
+
# The following methods make this class compatible with httpx.Auth.
|
289
|
+
##########
|
290
|
+
|
291
|
+
async def async_auth_flow(self, request: httpx.Request):
|
292
|
+
"""
|
293
|
+
Injects authorization into the request
|
294
|
+
|
295
|
+
(this method makes this class httpx.Auth compatible)
|
417
296
|
"""
|
418
|
-
|
297
|
+
super().async_auth_flow
|
298
|
+
if request.headers.get("authorization") is None:
|
299
|
+
await self.async_refresh_if_necessary()
|
300
|
+
access_token = self.access_token
|
301
|
+
request.headers["authorization"] = f"Bearer {access_token}"
|
419
302
|
|
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)
|
303
|
+
yield request
|
304
|
+
|
305
|
+
def sync_auth_flow(self, request: httpx.Request):
|
306
|
+
"""
|
307
|
+
Injects authorization into the request
|
426
308
|
|
427
|
-
|
428
|
-
________
|
429
|
-
access_token: str
|
309
|
+
(this method makes this class httpx.Auth compatible)
|
430
310
|
"""
|
311
|
+
# NOTE: we explicitly redefine this sync method because ctx.syncify()
|
312
|
+
# does not support generators
|
313
|
+
if request.headers.get("authorization") is None:
|
314
|
+
self.refresh_if_necessary()
|
315
|
+
access_token = self.access_token
|
316
|
+
request.headers["authorization"] = f"Bearer {access_token}"
|
431
317
|
|
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
|
318
|
+
yield request
|