deepailab 0.2.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.
- deepailab/__init__.py +142 -0
- deepailab/auth.py +369 -0
- deepailab/client.py +1075 -0
- deepailab/exceptions.py +280 -0
- deepailab/portal.py +86 -0
- deepailab/types.py +194 -0
- deepailab-0.2.0b1.dist-info/METADATA +362 -0
- deepailab-0.2.0b1.dist-info/RECORD +9 -0
- deepailab-0.2.0b1.dist-info/WHEEL +4 -0
deepailab/__init__.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeepAI Lab Python SDK
|
|
3
|
+
|
|
4
|
+
Official Python SDK for the DeepAI Lab API platform.
|
|
5
|
+
Supports OpenAI-compatible endpoints, model marketplace, and enterprise features.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import DeepAILab, AsyncDeepAILab
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
DeepAILabError,
|
|
11
|
+
APIError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
TimeoutError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .auth import (
|
|
19
|
+
OIDCClient,
|
|
20
|
+
OIDCError,
|
|
21
|
+
OIDCTokenSet,
|
|
22
|
+
DeviceAuthorization,
|
|
23
|
+
TokenStore,
|
|
24
|
+
MemoryTokenStore,
|
|
25
|
+
FileTokenStore,
|
|
26
|
+
generate_code_verifier,
|
|
27
|
+
code_challenge,
|
|
28
|
+
DEFAULT_ISSUER,
|
|
29
|
+
DEFAULT_SCOPES,
|
|
30
|
+
)
|
|
31
|
+
from .portal import (
|
|
32
|
+
PortalClient,
|
|
33
|
+
PortalError,
|
|
34
|
+
DEFAULT_BRIDGE_BASE_URL,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Type exports
|
|
38
|
+
from .types import (
|
|
39
|
+
# Client configuration
|
|
40
|
+
ClientOptions,
|
|
41
|
+
RequestOptions,
|
|
42
|
+
OnBehalfOfOptions,
|
|
43
|
+
|
|
44
|
+
# OpenAI-compatible types
|
|
45
|
+
ChatCompletionMessage,
|
|
46
|
+
ChatCompletionRequest,
|
|
47
|
+
ChatCompletionResponse,
|
|
48
|
+
ChatCompletionChoice,
|
|
49
|
+
ChatCompletionUsage,
|
|
50
|
+
|
|
51
|
+
EmbeddingRequest,
|
|
52
|
+
EmbeddingResponse,
|
|
53
|
+
Embedding,
|
|
54
|
+
|
|
55
|
+
ModelListResponse,
|
|
56
|
+
Model,
|
|
57
|
+
|
|
58
|
+
# Model marketplace types
|
|
59
|
+
InferenceRequest,
|
|
60
|
+
InferenceResponse,
|
|
61
|
+
BatchRequest,
|
|
62
|
+
BatchResponse,
|
|
63
|
+
ModelStatus,
|
|
64
|
+
|
|
65
|
+
# Observability types
|
|
66
|
+
RequestMetrics,
|
|
67
|
+
UsageInfo,
|
|
68
|
+
CostInfo,
|
|
69
|
+
|
|
70
|
+
# Streaming types
|
|
71
|
+
StreamingResponse,
|
|
72
|
+
StreamChunk,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Version information
|
|
76
|
+
__version__ = "0.2.0b1"
|
|
77
|
+
__author__ = "DeepAI Lab"
|
|
78
|
+
__email__ = "sdk@deepailab.ai"
|
|
79
|
+
__license__ = "MIT"
|
|
80
|
+
|
|
81
|
+
# Public API
|
|
82
|
+
__all__ = [
|
|
83
|
+
# Main client
|
|
84
|
+
"DeepAILab",
|
|
85
|
+
"AsyncDeepAILab",
|
|
86
|
+
|
|
87
|
+
# Auth (Keycloak OIDC)
|
|
88
|
+
"OIDCClient",
|
|
89
|
+
"OIDCError",
|
|
90
|
+
"OIDCTokenSet",
|
|
91
|
+
"DeviceAuthorization",
|
|
92
|
+
"TokenStore",
|
|
93
|
+
"MemoryTokenStore",
|
|
94
|
+
"FileTokenStore",
|
|
95
|
+
"generate_code_verifier",
|
|
96
|
+
"code_challenge",
|
|
97
|
+
"DEFAULT_ISSUER",
|
|
98
|
+
"DEFAULT_SCOPES",
|
|
99
|
+
# Portal aggregation
|
|
100
|
+
"PortalClient",
|
|
101
|
+
"PortalError",
|
|
102
|
+
"DEFAULT_BRIDGE_BASE_URL",
|
|
103
|
+
|
|
104
|
+
# Exceptions
|
|
105
|
+
"DeepAILabError",
|
|
106
|
+
"APIError",
|
|
107
|
+
"AuthenticationError",
|
|
108
|
+
"RateLimitError",
|
|
109
|
+
"TimeoutError",
|
|
110
|
+
"ValidationError",
|
|
111
|
+
|
|
112
|
+
# Types
|
|
113
|
+
"ClientOptions",
|
|
114
|
+
"RequestOptions",
|
|
115
|
+
"OnBehalfOfOptions",
|
|
116
|
+
"ChatCompletionMessage",
|
|
117
|
+
"ChatCompletionRequest",
|
|
118
|
+
"ChatCompletionResponse",
|
|
119
|
+
"ChatCompletionChoice",
|
|
120
|
+
"ChatCompletionUsage",
|
|
121
|
+
"EmbeddingRequest",
|
|
122
|
+
"EmbeddingResponse",
|
|
123
|
+
"Embedding",
|
|
124
|
+
"ModelListResponse",
|
|
125
|
+
"Model",
|
|
126
|
+
"InferenceRequest",
|
|
127
|
+
"InferenceResponse",
|
|
128
|
+
"BatchRequest",
|
|
129
|
+
"BatchResponse",
|
|
130
|
+
"ModelStatus",
|
|
131
|
+
"RequestMetrics",
|
|
132
|
+
"UsageInfo",
|
|
133
|
+
"CostInfo",
|
|
134
|
+
"StreamingResponse",
|
|
135
|
+
"StreamChunk",
|
|
136
|
+
|
|
137
|
+
# Version info
|
|
138
|
+
"__version__",
|
|
139
|
+
"__author__",
|
|
140
|
+
"__email__",
|
|
141
|
+
"__license__",
|
|
142
|
+
]
|
deepailab/auth.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Keycloak OIDC authentication for the DeepAILab platform (Python).
|
|
3
|
+
|
|
4
|
+
Implements the same contract as the JS SDK:
|
|
5
|
+
- OIDC discovery (cached)
|
|
6
|
+
- PKCE (S256) helpers
|
|
7
|
+
- Device Authorization Grant (RFC 8628) for CLI
|
|
8
|
+
- Authorization Code + PKCE for desktop/web
|
|
9
|
+
- refresh_token rotation
|
|
10
|
+
- logout (end-session)
|
|
11
|
+
|
|
12
|
+
Authoritative contract: docs/architecture/stack-a2/OAUTH2_CLIENT_INTEGRATION.md
|
|
13
|
+
Compatible with Python 3.8+.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import hashlib
|
|
18
|
+
import os
|
|
19
|
+
import secrets
|
|
20
|
+
import time
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
27
|
+
DEFAULT_ISSUER = "https://auth.deepailab.ai/realms/deepailab"
|
|
28
|
+
DEFAULT_SCOPES = "openid profile email offline_access"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OIDCError(Exception):
|
|
32
|
+
"""Raised on any OIDC/token error."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str, code: str = "oidc_error", status: Optional[int] = None) -> None:
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.code = code
|
|
37
|
+
self.status = status
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class OIDCTokenSet:
|
|
42
|
+
access_token: str
|
|
43
|
+
expires_at: int # epoch seconds
|
|
44
|
+
refresh_token: Optional[str] = None
|
|
45
|
+
id_token: Optional[str] = None
|
|
46
|
+
scope: Optional[str] = None
|
|
47
|
+
token_type: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> Dict[str, object]:
|
|
50
|
+
return {k: v for k, v in self.__dict__.items() if v is not None}
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, d: Dict[str, object]) -> "OIDCTokenSet":
|
|
54
|
+
return cls(
|
|
55
|
+
access_token=str(d["access_token"]),
|
|
56
|
+
expires_at=int(d["expires_at"]), # type: ignore[arg-type]
|
|
57
|
+
refresh_token=_opt_str(d.get("refresh_token")),
|
|
58
|
+
id_token=_opt_str(d.get("id_token")),
|
|
59
|
+
scope=_opt_str(d.get("scope")),
|
|
60
|
+
token_type=_opt_str(d.get("token_type")),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class DeviceAuthorization:
|
|
66
|
+
device_code: str
|
|
67
|
+
user_code: str
|
|
68
|
+
verification_uri: str
|
|
69
|
+
expires_in: int
|
|
70
|
+
interval: int
|
|
71
|
+
verification_uri_complete: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------------
|
|
75
|
+
# Token storage
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
class TokenStore(ABC):
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def load(self) -> Optional[OIDCTokenSet]: ...
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def save(self, tokens: OIDCTokenSet) -> None: ...
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def clear(self) -> None: ...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MemoryTokenStore(TokenStore):
|
|
90
|
+
def __init__(self) -> None:
|
|
91
|
+
self._tokens: Optional[OIDCTokenSet] = None
|
|
92
|
+
|
|
93
|
+
def load(self) -> Optional[OIDCTokenSet]:
|
|
94
|
+
return self._tokens
|
|
95
|
+
|
|
96
|
+
def save(self, tokens: OIDCTokenSet) -> None:
|
|
97
|
+
self._tokens = tokens
|
|
98
|
+
|
|
99
|
+
def clear(self) -> None:
|
|
100
|
+
self._tokens = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class FileTokenStore(TokenStore):
|
|
104
|
+
"""Persists tokens as JSON with 0600 permissions (CLI/desktop)."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, file_path: str) -> None:
|
|
107
|
+
if not file_path:
|
|
108
|
+
raise ValueError("FileTokenStore requires a file path")
|
|
109
|
+
self.file_path = file_path
|
|
110
|
+
|
|
111
|
+
def load(self) -> Optional[OIDCTokenSet]:
|
|
112
|
+
import json
|
|
113
|
+
try:
|
|
114
|
+
with open(self.file_path, "r", encoding="utf-8") as fh:
|
|
115
|
+
data = json.load(fh)
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
return None
|
|
118
|
+
if not isinstance(data, dict) or "access_token" not in data:
|
|
119
|
+
return None
|
|
120
|
+
return OIDCTokenSet.from_dict(data)
|
|
121
|
+
|
|
122
|
+
def save(self, tokens: OIDCTokenSet) -> None:
|
|
123
|
+
import json
|
|
124
|
+
directory = os.path.dirname(self.file_path)
|
|
125
|
+
if directory:
|
|
126
|
+
os.makedirs(directory, exist_ok=True)
|
|
127
|
+
tmp = self.file_path + ".tmp"
|
|
128
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
129
|
+
try:
|
|
130
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
131
|
+
json.dump(tokens.to_dict(), fh)
|
|
132
|
+
finally:
|
|
133
|
+
os.chmod(tmp, 0o600)
|
|
134
|
+
os.replace(tmp, self.file_path)
|
|
135
|
+
os.chmod(self.file_path, 0o600)
|
|
136
|
+
|
|
137
|
+
def clear(self) -> None:
|
|
138
|
+
try:
|
|
139
|
+
os.unlink(self.file_path)
|
|
140
|
+
except FileNotFoundError:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# --------------------------------------------------------------------------
|
|
145
|
+
# PKCE helpers
|
|
146
|
+
# --------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def _b64url(raw: bytes) -> str:
|
|
149
|
+
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def generate_code_verifier() -> str:
|
|
153
|
+
return _b64url(secrets.token_bytes(64))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def code_challenge(verifier: str) -> str:
|
|
157
|
+
return _b64url(hashlib.sha256(verifier.encode("ascii")).digest())
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def random_state() -> str:
|
|
161
|
+
return _b64url(secrets.token_bytes(16))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _opt_str(v: object) -> Optional[str]:
|
|
165
|
+
return None if v is None else str(v)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# --------------------------------------------------------------------------
|
|
169
|
+
# OIDC client
|
|
170
|
+
# --------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
class OIDCClient:
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
client_id: str,
|
|
176
|
+
issuer: str = DEFAULT_ISSUER,
|
|
177
|
+
scope: str = DEFAULT_SCOPES,
|
|
178
|
+
store: Optional[TokenStore] = None,
|
|
179
|
+
refresh_skew: int = 60,
|
|
180
|
+
http: Optional[httpx.Client] = None,
|
|
181
|
+
now: Optional[Callable[[], float]] = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
if not client_id:
|
|
184
|
+
raise OIDCError("client_id is required", "config_error")
|
|
185
|
+
self.client_id = client_id
|
|
186
|
+
self.issuer = issuer.rstrip("/")
|
|
187
|
+
self.scope = scope
|
|
188
|
+
self.store: TokenStore = store or MemoryTokenStore()
|
|
189
|
+
self.refresh_skew = refresh_skew
|
|
190
|
+
self._http = http or httpx.Client(timeout=30.0)
|
|
191
|
+
self._now = now or time.time
|
|
192
|
+
self._discovery: Optional[Dict[str, object]] = None
|
|
193
|
+
|
|
194
|
+
# --- discovery --------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def discover(self) -> Dict[str, object]:
|
|
197
|
+
if self._discovery is not None:
|
|
198
|
+
return self._discovery
|
|
199
|
+
url = self.issuer + "/.well-known/openid-configuration"
|
|
200
|
+
resp = self._http.get(url, headers={"Accept": "application/json"})
|
|
201
|
+
if resp.status_code != 200:
|
|
202
|
+
raise OIDCError("Discovery failed: %s %s" % (resp.status_code, url), "discovery_failed", resp.status_code)
|
|
203
|
+
self._discovery = resp.json()
|
|
204
|
+
return self._discovery
|
|
205
|
+
|
|
206
|
+
def _endpoint(self, key: str) -> str:
|
|
207
|
+
disc = self.discover()
|
|
208
|
+
val = disc.get(key)
|
|
209
|
+
if not isinstance(val, str):
|
|
210
|
+
raise OIDCError("Realm discovery has no %s" % key, "endpoint_missing")
|
|
211
|
+
return val
|
|
212
|
+
|
|
213
|
+
def _post_token(self, params: Dict[str, str]) -> OIDCTokenSet:
|
|
214
|
+
resp = self._http.post(
|
|
215
|
+
self._endpoint("token_endpoint"),
|
|
216
|
+
data=params,
|
|
217
|
+
headers={"Accept": "application/json"},
|
|
218
|
+
)
|
|
219
|
+
try:
|
|
220
|
+
body = resp.json()
|
|
221
|
+
except Exception:
|
|
222
|
+
raise OIDCError("Non-JSON token response", "invalid_response", resp.status_code)
|
|
223
|
+
if resp.status_code >= 400 or "error" in body:
|
|
224
|
+
raise OIDCError(
|
|
225
|
+
str(body.get("error_description") or body.get("error") or resp.status_code),
|
|
226
|
+
str(body.get("error") or "token_error"),
|
|
227
|
+
resp.status_code,
|
|
228
|
+
)
|
|
229
|
+
return self._to_token_set(body)
|
|
230
|
+
|
|
231
|
+
def _to_token_set(self, body: Dict[str, object]) -> OIDCTokenSet:
|
|
232
|
+
expires_in = int(body.get("expires_in", 300)) # type: ignore[arg-type]
|
|
233
|
+
tokens = OIDCTokenSet(
|
|
234
|
+
access_token=str(body["access_token"]),
|
|
235
|
+
expires_at=int(self._now()) + expires_in,
|
|
236
|
+
refresh_token=_opt_str(body.get("refresh_token")),
|
|
237
|
+
id_token=_opt_str(body.get("id_token")),
|
|
238
|
+
scope=_opt_str(body.get("scope")),
|
|
239
|
+
token_type=_opt_str(body.get("token_type")),
|
|
240
|
+
)
|
|
241
|
+
return tokens
|
|
242
|
+
|
|
243
|
+
# --- device flow ------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def start_device_authorization(self) -> DeviceAuthorization:
|
|
246
|
+
endpoint = self._endpoint("device_authorization_endpoint")
|
|
247
|
+
resp = self._http.post(
|
|
248
|
+
endpoint,
|
|
249
|
+
data={"client_id": self.client_id, "scope": self.scope},
|
|
250
|
+
headers={"Accept": "application/json"},
|
|
251
|
+
)
|
|
252
|
+
body = resp.json()
|
|
253
|
+
if resp.status_code >= 400:
|
|
254
|
+
raise OIDCError(str(body.get("error_description") or body.get("error") or resp.status_code), "device_start_failed", resp.status_code)
|
|
255
|
+
return DeviceAuthorization(
|
|
256
|
+
device_code=str(body["device_code"]),
|
|
257
|
+
user_code=str(body["user_code"]),
|
|
258
|
+
verification_uri=str(body["verification_uri"]),
|
|
259
|
+
expires_in=int(body.get("expires_in", 600)),
|
|
260
|
+
interval=int(body.get("interval", 5)),
|
|
261
|
+
verification_uri_complete=_opt_str(body.get("verification_uri_complete")),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def poll_device_token(self, device: DeviceAuthorization, sleep: Optional[Callable[[float], None]] = None) -> OIDCTokenSet:
|
|
265
|
+
do_sleep = sleep or time.sleep
|
|
266
|
+
interval = device.interval
|
|
267
|
+
deadline = self._now() + device.expires_in
|
|
268
|
+
while True:
|
|
269
|
+
if self._now() > deadline:
|
|
270
|
+
raise OIDCError("Device code expired", "expired_token")
|
|
271
|
+
do_sleep(interval)
|
|
272
|
+
try:
|
|
273
|
+
tokens = self._post_token({
|
|
274
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
275
|
+
"device_code": device.device_code,
|
|
276
|
+
"client_id": self.client_id,
|
|
277
|
+
})
|
|
278
|
+
self.store.save(tokens)
|
|
279
|
+
return tokens
|
|
280
|
+
except OIDCError as err:
|
|
281
|
+
if err.code == "authorization_pending":
|
|
282
|
+
continue
|
|
283
|
+
if err.code == "slow_down":
|
|
284
|
+
interval += 5
|
|
285
|
+
continue
|
|
286
|
+
raise
|
|
287
|
+
|
|
288
|
+
def login_with_device_flow(self, on_prompt: Callable[[DeviceAuthorization], None], sleep: Optional[Callable[[float], None]] = None) -> OIDCTokenSet:
|
|
289
|
+
device = self.start_device_authorization()
|
|
290
|
+
on_prompt(device)
|
|
291
|
+
return self.poll_device_token(device, sleep=sleep)
|
|
292
|
+
|
|
293
|
+
# --- authorization code + PKCE ---------------------------------------
|
|
294
|
+
|
|
295
|
+
def create_authorization_request(self, redirect_uri: str) -> Tuple[str, str, str]:
|
|
296
|
+
"""Returns (authorization_url, code_verifier, state)."""
|
|
297
|
+
from urllib.parse import urlencode
|
|
298
|
+
verifier = generate_code_verifier()
|
|
299
|
+
challenge = code_challenge(verifier)
|
|
300
|
+
state = random_state()
|
|
301
|
+
query = urlencode({
|
|
302
|
+
"response_type": "code",
|
|
303
|
+
"client_id": self.client_id,
|
|
304
|
+
"redirect_uri": redirect_uri,
|
|
305
|
+
"scope": self.scope,
|
|
306
|
+
"state": state,
|
|
307
|
+
"code_challenge": challenge,
|
|
308
|
+
"code_challenge_method": "S256",
|
|
309
|
+
})
|
|
310
|
+
return self._endpoint("authorization_endpoint") + "?" + query, verifier, state
|
|
311
|
+
|
|
312
|
+
def exchange_authorization_code(self, code: str, code_verifier: str, redirect_uri: str) -> OIDCTokenSet:
|
|
313
|
+
tokens = self._post_token({
|
|
314
|
+
"grant_type": "authorization_code",
|
|
315
|
+
"code": code,
|
|
316
|
+
"redirect_uri": redirect_uri,
|
|
317
|
+
"client_id": self.client_id,
|
|
318
|
+
"code_verifier": code_verifier,
|
|
319
|
+
})
|
|
320
|
+
self.store.save(tokens)
|
|
321
|
+
return tokens
|
|
322
|
+
|
|
323
|
+
# --- session ----------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
def get_access_token(self) -> str:
|
|
326
|
+
current = self.store.load()
|
|
327
|
+
if current is None:
|
|
328
|
+
raise OIDCError("Not authenticated", "no_session")
|
|
329
|
+
if current.expires_at - self.refresh_skew > int(self._now()):
|
|
330
|
+
return current.access_token
|
|
331
|
+
if not current.refresh_token:
|
|
332
|
+
raise OIDCError("Access token expired and no refresh_token available", "session_expired")
|
|
333
|
+
return self.refresh().access_token
|
|
334
|
+
|
|
335
|
+
def refresh(self) -> OIDCTokenSet:
|
|
336
|
+
current = self.store.load()
|
|
337
|
+
if current is None or not current.refresh_token:
|
|
338
|
+
raise OIDCError("No refresh_token to refresh", "no_refresh_token")
|
|
339
|
+
tokens = self._post_token({
|
|
340
|
+
"grant_type": "refresh_token",
|
|
341
|
+
"refresh_token": current.refresh_token,
|
|
342
|
+
"client_id": self.client_id,
|
|
343
|
+
})
|
|
344
|
+
if not tokens.refresh_token and current.refresh_token:
|
|
345
|
+
tokens.refresh_token = current.refresh_token
|
|
346
|
+
self.store.save(tokens)
|
|
347
|
+
return tokens
|
|
348
|
+
|
|
349
|
+
def is_authenticated(self) -> bool:
|
|
350
|
+
current = self.store.load()
|
|
351
|
+
if current is None:
|
|
352
|
+
return False
|
|
353
|
+
if current.expires_at > int(self._now()):
|
|
354
|
+
return True
|
|
355
|
+
return bool(current.refresh_token)
|
|
356
|
+
|
|
357
|
+
def logout(self) -> None:
|
|
358
|
+
current = self.store.load()
|
|
359
|
+
try:
|
|
360
|
+
if current is not None and current.refresh_token:
|
|
361
|
+
disc = self.discover()
|
|
362
|
+
endpoint = disc.get("end_session_endpoint")
|
|
363
|
+
if isinstance(endpoint, str):
|
|
364
|
+
try:
|
|
365
|
+
self._http.post(endpoint, data={"client_id": self.client_id, "refresh_token": current.refresh_token})
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
finally:
|
|
369
|
+
self.store.clear()
|