okta-client-python 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.
- okta_client/__init__.py +20 -0
- okta_client/authfoundation/__init__.py +197 -0
- okta_client/authfoundation/authentication.py +247 -0
- okta_client/authfoundation/coalesced_result.py +113 -0
- okta_client/authfoundation/codable.py +72 -0
- okta_client/authfoundation/expires.py +49 -0
- okta_client/authfoundation/key_provider.py +130 -0
- okta_client/authfoundation/networking/__init__.py +56 -0
- okta_client/authfoundation/networking/body.py +46 -0
- okta_client/authfoundation/networking/client.py +200 -0
- okta_client/authfoundation/networking/types.py +293 -0
- okta_client/authfoundation/oauth2/__init__.py +104 -0
- okta_client/authfoundation/oauth2/claims.py +44 -0
- okta_client/authfoundation/oauth2/client.py +402 -0
- okta_client/authfoundation/oauth2/client_authorization.py +172 -0
- okta_client/authfoundation/oauth2/config.py +298 -0
- okta_client/authfoundation/oauth2/errors.py +32 -0
- okta_client/authfoundation/oauth2/jwt_bearer_claims.py +59 -0
- okta_client/authfoundation/oauth2/jwt_bearer_utils.py +30 -0
- okta_client/authfoundation/oauth2/jwt_context.py +52 -0
- okta_client/authfoundation/oauth2/jwt_token.py +214 -0
- okta_client/authfoundation/oauth2/models.py +198 -0
- okta_client/authfoundation/oauth2/parameters.py +36 -0
- okta_client/authfoundation/oauth2/refresh_token.py +165 -0
- okta_client/authfoundation/oauth2/request_protocols.py +174 -0
- okta_client/authfoundation/oauth2/requests/__init__.py +37 -0
- okta_client/authfoundation/oauth2/requests/introspect.py +50 -0
- okta_client/authfoundation/oauth2/requests/jwks.py +44 -0
- okta_client/authfoundation/oauth2/requests/oauth_authorization_server.py +44 -0
- okta_client/authfoundation/oauth2/requests/openid_configuration.py +47 -0
- okta_client/authfoundation/oauth2/requests/revoke.py +54 -0
- okta_client/authfoundation/oauth2/requests/user_info.py +37 -0
- okta_client/authfoundation/oauth2/utils.py +25 -0
- okta_client/authfoundation/oauth2/validation_protocols.py +33 -0
- okta_client/authfoundation/oauth2/validator_registry.py +64 -0
- okta_client/authfoundation/oauth2/validators/token_hash.py +37 -0
- okta_client/authfoundation/oauth2/validators/token_validator.py +26 -0
- okta_client/authfoundation/time_coordinator.py +57 -0
- okta_client/authfoundation/token.py +201 -0
- okta_client/authfoundation/user_agent.py +80 -0
- okta_client/authfoundation/utils.py +63 -0
- okta_client/browser_signin/__init__.py +11 -0
- okta_client/directauth/__init__.py +11 -0
- okta_client/oauth2auth/__init__.py +63 -0
- okta_client/oauth2auth/authorization_code.py +594 -0
- okta_client/oauth2auth/cross_app.py +626 -0
- okta_client/oauth2auth/jwt_bearer.py +182 -0
- okta_client/oauth2auth/resource_owner.py +159 -0
- okta_client/oauth2auth/token_exchange.py +380 -0
- okta_client/oauth2auth/utils.py +87 -0
- okta_client/py.typed +0 -0
- okta_client_python-0.1.0.dist-info/METADATA +936 -0
- okta_client_python-0.1.0.dist-info/RECORD +56 -0
- okta_client_python-0.1.0.dist-info/WHEEL +5 -0
- okta_client_python-0.1.0.dist-info/licenses/LICENSE +171 -0
- okta_client_python-0.1.0.dist-info/top_level.txt +1 -0
okta_client/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
"""Okta Client SDK (Python)."""
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
__version__: str = version("okta-client-python")
|
|
17
|
+
except PackageNotFoundError: # pragma: no cover - not installed
|
|
18
|
+
__version__ = "0.0.0-dev"
|
|
19
|
+
|
|
20
|
+
__all__ = ["__version__", "authfoundation", "browser_signin", "directauth", "oauth2auth"]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
"""Core networking, OAuth2 primitives, token models, and storage interfaces."""
|
|
12
|
+
|
|
13
|
+
from .authentication import (
|
|
14
|
+
AuthenticationContext,
|
|
15
|
+
AuthenticationFlow,
|
|
16
|
+
AuthenticationListener,
|
|
17
|
+
AuthenticationState,
|
|
18
|
+
BaseAuthenticationFlow,
|
|
19
|
+
PKCEData,
|
|
20
|
+
StandardAuthenticationContext,
|
|
21
|
+
generate_pkce,
|
|
22
|
+
)
|
|
23
|
+
from .coalesced_result import CoalescedResult
|
|
24
|
+
from .codable import Codable
|
|
25
|
+
from .expires import Expires
|
|
26
|
+
from .key_provider import (
|
|
27
|
+
DefaultKeyProvider,
|
|
28
|
+
KeyProvider,
|
|
29
|
+
LocalKeyProvider,
|
|
30
|
+
get_key_provider,
|
|
31
|
+
set_key_provider,
|
|
32
|
+
)
|
|
33
|
+
from .networking import (
|
|
34
|
+
APIAuthorization,
|
|
35
|
+
APIClient,
|
|
36
|
+
APIClientConfiguration,
|
|
37
|
+
APIClientListener,
|
|
38
|
+
APIContentType,
|
|
39
|
+
APIParsingContext,
|
|
40
|
+
APIRateLimit,
|
|
41
|
+
APIRequest,
|
|
42
|
+
APIRequestBody,
|
|
43
|
+
APIRequestBodyMixin,
|
|
44
|
+
APIRequestMethod,
|
|
45
|
+
APIResponse,
|
|
46
|
+
APIRetry,
|
|
47
|
+
BaseAPIRequest,
|
|
48
|
+
DefaultNetworkInterface,
|
|
49
|
+
HTTPRequest,
|
|
50
|
+
ListenerCollection,
|
|
51
|
+
NetworkInterface,
|
|
52
|
+
RawResponse,
|
|
53
|
+
RequestValue,
|
|
54
|
+
RequestValueConvertible,
|
|
55
|
+
)
|
|
56
|
+
from .oauth2 import (
|
|
57
|
+
JWK,
|
|
58
|
+
JWKS,
|
|
59
|
+
JWT,
|
|
60
|
+
ClientAssertionAuthorization,
|
|
61
|
+
ClientAuthorization,
|
|
62
|
+
ClientIdAuthorization,
|
|
63
|
+
ClientSecretAuthorization,
|
|
64
|
+
ConfigurationError,
|
|
65
|
+
ConfigurationFileNotFoundError,
|
|
66
|
+
ConfigurationParseError,
|
|
67
|
+
DefaultTokenHashValidator,
|
|
68
|
+
DefaultTokenValidator,
|
|
69
|
+
HasClaims,
|
|
70
|
+
IdTokenClaim,
|
|
71
|
+
IDTokenValidatorContext,
|
|
72
|
+
InvalidConfigurationError,
|
|
73
|
+
JWTBearerClaims,
|
|
74
|
+
JWTType,
|
|
75
|
+
JWTUsageContext,
|
|
76
|
+
JWTValidationContext,
|
|
77
|
+
OAuth2APIRequest,
|
|
78
|
+
OAuth2APIRequestCategory,
|
|
79
|
+
OAuth2ClientConfiguration,
|
|
80
|
+
OAuth2Error,
|
|
81
|
+
OAuth2TokenRequest,
|
|
82
|
+
OAuth2TokenRequestDefaults,
|
|
83
|
+
OpenIdConfiguration,
|
|
84
|
+
ProvidesOAuth2Parameters,
|
|
85
|
+
TokenHashValidator,
|
|
86
|
+
TokenInfo,
|
|
87
|
+
TokenValidator,
|
|
88
|
+
UserInfo,
|
|
89
|
+
get_access_token_validator,
|
|
90
|
+
get_device_secret_validator,
|
|
91
|
+
get_token_validator,
|
|
92
|
+
set_access_token_validator,
|
|
93
|
+
set_device_secret_validator,
|
|
94
|
+
set_token_validator,
|
|
95
|
+
)
|
|
96
|
+
from .oauth2.client import OAuth2Client, OAuth2ClientListener
|
|
97
|
+
from .oauth2.refresh_token import RefreshTokenFlow, RefreshTokenRequest
|
|
98
|
+
from .time_coordinator import (
|
|
99
|
+
DefaultTimeCoordinator,
|
|
100
|
+
TimeCoordinator,
|
|
101
|
+
get_time_coordinator,
|
|
102
|
+
set_time_coordinator,
|
|
103
|
+
)
|
|
104
|
+
from .token import (
|
|
105
|
+
GrantedTokenType,
|
|
106
|
+
Token,
|
|
107
|
+
TokenContext,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"JWK",
|
|
112
|
+
"JWKS",
|
|
113
|
+
"JWT",
|
|
114
|
+
"APIAuthorization",
|
|
115
|
+
"APIClient",
|
|
116
|
+
"APIClientConfiguration",
|
|
117
|
+
"APIClientListener",
|
|
118
|
+
"APIContentType",
|
|
119
|
+
"APIParsingContext",
|
|
120
|
+
"APIRateLimit",
|
|
121
|
+
"APIRequest",
|
|
122
|
+
"APIRequestBody",
|
|
123
|
+
"APIRequestBodyMixin",
|
|
124
|
+
"APIRequestMethod",
|
|
125
|
+
"APIResponse",
|
|
126
|
+
"APIRetry",
|
|
127
|
+
"AuthenticationContext",
|
|
128
|
+
"AuthenticationFlow",
|
|
129
|
+
"AuthenticationListener",
|
|
130
|
+
"AuthenticationState",
|
|
131
|
+
"BaseAPIRequest",
|
|
132
|
+
"BaseAuthenticationFlow",
|
|
133
|
+
"ClientAssertionAuthorization",
|
|
134
|
+
"ClientAuthorization",
|
|
135
|
+
"ClientIdAuthorization",
|
|
136
|
+
"ClientSecretAuthorization",
|
|
137
|
+
"CoalescedResult",
|
|
138
|
+
"Codable",
|
|
139
|
+
"ConfigurationError",
|
|
140
|
+
"ConfigurationFileNotFoundError",
|
|
141
|
+
"ConfigurationParseError",
|
|
142
|
+
"DefaultKeyProvider",
|
|
143
|
+
"DefaultNetworkInterface",
|
|
144
|
+
"DefaultTimeCoordinator",
|
|
145
|
+
"DefaultTokenHashValidator",
|
|
146
|
+
"DefaultTokenValidator",
|
|
147
|
+
"Expires",
|
|
148
|
+
"GrantedTokenType",
|
|
149
|
+
"HTTPRequest",
|
|
150
|
+
"HasClaims",
|
|
151
|
+
"IDTokenValidatorContext",
|
|
152
|
+
"IdTokenClaim",
|
|
153
|
+
"InvalidConfigurationError",
|
|
154
|
+
"JWTBearerClaims",
|
|
155
|
+
"JWTType",
|
|
156
|
+
"JWTUsageContext",
|
|
157
|
+
"JWTValidationContext",
|
|
158
|
+
"KeyProvider",
|
|
159
|
+
"ListenerCollection",
|
|
160
|
+
"LocalKeyProvider",
|
|
161
|
+
"NetworkInterface",
|
|
162
|
+
"OAuth2APIRequest",
|
|
163
|
+
"OAuth2APIRequestCategory",
|
|
164
|
+
"OAuth2Client",
|
|
165
|
+
"OAuth2ClientConfiguration",
|
|
166
|
+
"OAuth2ClientListener",
|
|
167
|
+
"OAuth2Error",
|
|
168
|
+
"OAuth2TokenRequest",
|
|
169
|
+
"OAuth2TokenRequestDefaults",
|
|
170
|
+
"OpenIdConfiguration",
|
|
171
|
+
"PKCEData",
|
|
172
|
+
"ProvidesOAuth2Parameters",
|
|
173
|
+
"RawResponse",
|
|
174
|
+
"RefreshTokenFlow",
|
|
175
|
+
"RefreshTokenRequest",
|
|
176
|
+
"RequestValue",
|
|
177
|
+
"RequestValueConvertible",
|
|
178
|
+
"StandardAuthenticationContext",
|
|
179
|
+
"TimeCoordinator",
|
|
180
|
+
"Token",
|
|
181
|
+
"TokenContext",
|
|
182
|
+
"TokenHashValidator",
|
|
183
|
+
"TokenInfo",
|
|
184
|
+
"TokenValidator",
|
|
185
|
+
"UserInfo",
|
|
186
|
+
"generate_pkce",
|
|
187
|
+
"get_access_token_validator",
|
|
188
|
+
"get_device_secret_validator",
|
|
189
|
+
"get_key_provider",
|
|
190
|
+
"get_time_coordinator",
|
|
191
|
+
"get_token_validator",
|
|
192
|
+
"set_access_token_validator",
|
|
193
|
+
"set_device_secret_validator",
|
|
194
|
+
"set_key_provider",
|
|
195
|
+
"set_time_coordinator",
|
|
196
|
+
"set_token_validator",
|
|
197
|
+
]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import hashlib
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Any, Generic, Protocol, TypeVar, cast, runtime_checkable
|
|
20
|
+
|
|
21
|
+
from .networking import ListenerCollection, RequestValue
|
|
22
|
+
from .oauth2.parameters import OAuth2APIRequestCategory
|
|
23
|
+
from .utils import base64url_encode
|
|
24
|
+
|
|
25
|
+
ContextT = TypeVar("ContextT", bound="AuthenticationContext")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthenticationState(str, Enum):
|
|
29
|
+
"""Lifecycle states for authentication flows."""
|
|
30
|
+
IDLE = "idle"
|
|
31
|
+
AUTHENTICATING = "authenticating"
|
|
32
|
+
COMPLETED = "completed"
|
|
33
|
+
FAILED = "failed"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class PKCEData:
|
|
38
|
+
"""PKCE values used by authorization code flows."""
|
|
39
|
+
code_verifier: str
|
|
40
|
+
code_challenge: str
|
|
41
|
+
code_challenge_method: str = "S256"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def generate_pkce() -> PKCEData:
|
|
45
|
+
"""Generate a PKCE verifier/challenge pair using S256.
|
|
46
|
+
|
|
47
|
+
- ``code_verifier``: 32 random bytes, base64url-encoded (no padding).
|
|
48
|
+
- ``code_challenge``: SHA-256 hash of the verifier, base64url-encoded (no padding).
|
|
49
|
+
"""
|
|
50
|
+
code_verifier = base64url_encode(os.urandom(32))
|
|
51
|
+
code_challenge = base64url_encode(hashlib.sha256(code_verifier.encode("ascii")).digest())
|
|
52
|
+
return PKCEData(
|
|
53
|
+
code_verifier=code_verifier,
|
|
54
|
+
code_challenge=code_challenge,
|
|
55
|
+
code_challenge_method="S256",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@runtime_checkable
|
|
60
|
+
class AuthenticationContext(Protocol):
|
|
61
|
+
"""Protocol for per-session authentication context."""
|
|
62
|
+
@property
|
|
63
|
+
def acr_values(self) -> list[str] | None:
|
|
64
|
+
"""ACR values requested for the session."""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def persist_values(self) -> Mapping[str, str] | None:
|
|
69
|
+
"""Values to persist into token context, if any."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def additional_parameters(self) -> Mapping[str, RequestValue] | None:
|
|
74
|
+
"""Additional parameters for the session."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
def parameters(self, category: OAuth2APIRequestCategory) -> Mapping[str, RequestValue] | None:
|
|
78
|
+
"""Return OAuth2 parameters to include for the given request category."""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@runtime_checkable
|
|
83
|
+
class AuthenticationListener(Protocol):
|
|
84
|
+
"""Listener for authentication flow lifecycle events."""
|
|
85
|
+
def authentication_started(self, flow: AuthenticationFlow[Any]) -> None:
|
|
86
|
+
"""Called when a flow starts authenticating."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
def authentication_updated(self, flow: AuthenticationFlow[Any], context: AuthenticationContext) -> None:
|
|
90
|
+
"""Called when a flow updates its context."""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def authentication_completed(self, flow: AuthenticationFlow[Any], result: Any) -> None:
|
|
94
|
+
"""Called when a flow completes successfully."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
def authentication_failed(self, flow: AuthenticationFlow[Any], error: Exception) -> None:
|
|
98
|
+
"""Called when a flow fails with an error."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class StandardAuthenticationContext(AuthenticationContext):
|
|
104
|
+
"""Default authentication context for OAuth2 flows."""
|
|
105
|
+
|
|
106
|
+
_acr_values: list[str] | None = None
|
|
107
|
+
_persist_values: Mapping[str, str] | None = None
|
|
108
|
+
_additional_parameters: Mapping[str, RequestValue] | None = None
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def acr_values(self) -> list[str] | None:
|
|
112
|
+
"""ACR values requested for the session."""
|
|
113
|
+
return self._acr_values
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def persist_values(self) -> Mapping[str, str] | None:
|
|
117
|
+
"""Values to persist into token context, if any."""
|
|
118
|
+
return self._persist_values
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def additional_parameters(self) -> Mapping[str, RequestValue] | None:
|
|
122
|
+
"""Additional parameters for the session."""
|
|
123
|
+
return self._additional_parameters
|
|
124
|
+
|
|
125
|
+
def parameters(self, category: OAuth2APIRequestCategory) -> Mapping[str, RequestValue] | None:
|
|
126
|
+
"""Return OAuth2 parameters contributed by this context."""
|
|
127
|
+
result: dict[str, RequestValue] = dict(self._additional_parameters or {})
|
|
128
|
+
if category == OAuth2APIRequestCategory.AUTHORIZATION and self._acr_values:
|
|
129
|
+
result["acr_values"] = " ".join(self._acr_values)
|
|
130
|
+
return result or None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@runtime_checkable
|
|
134
|
+
class AuthenticationFlow(Protocol, Generic[ContextT]):
|
|
135
|
+
"""Protocol for authentication flows with start/resume/reset semantics."""
|
|
136
|
+
@property
|
|
137
|
+
def context(self) -> ContextT | None:
|
|
138
|
+
"""Current session context, if any."""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def state(self) -> AuthenticationState:
|
|
143
|
+
"""Current flow state."""
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def is_authenticating(self) -> bool:
|
|
148
|
+
"""Return True when authentication is in progress."""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def additional_parameters(self) -> Mapping[str, RequestValue] | None:
|
|
153
|
+
"""Flow-level parameters applied to all requests."""
|
|
154
|
+
...
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def listeners(self) -> ListenerCollection[AuthenticationListener]:
|
|
158
|
+
"""Listener collection for flow events."""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def reset(self) -> None:
|
|
162
|
+
"""Reset flow state and context."""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
async def start(self, *args: Any, context: ContextT | None = None, **kwargs: Any) -> Any:
|
|
166
|
+
"""Start authentication with optional context."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
async def resume(self, *args: Any, context: ContextT, **kwargs: Any) -> Any:
|
|
170
|
+
"""Resume authentication with additional context."""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class BaseAuthenticationFlow(Generic[ContextT]):
|
|
175
|
+
"""Base class implementing common flow state and listener behavior."""
|
|
176
|
+
def __init__(self, additional_parameters: Mapping[str, RequestValue] | None = None) -> None:
|
|
177
|
+
"""Initialize the flow with optional additional parameters."""
|
|
178
|
+
self._context: ContextT | None = None
|
|
179
|
+
self._state = AuthenticationState.IDLE
|
|
180
|
+
self._additional_parameters = additional_parameters
|
|
181
|
+
self._lock = asyncio.Lock()
|
|
182
|
+
self._listeners: ListenerCollection[AuthenticationListener] = ListenerCollection()
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def context(self) -> ContextT | None:
|
|
186
|
+
"""Return the current context."""
|
|
187
|
+
return self._context
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def state(self) -> AuthenticationState:
|
|
191
|
+
"""Return the current flow state."""
|
|
192
|
+
return self._state
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def is_authenticating(self) -> bool:
|
|
196
|
+
"""Return True when the flow is authenticating."""
|
|
197
|
+
return self._state == AuthenticationState.AUTHENTICATING
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def additional_parameters(self) -> Mapping[str, RequestValue] | None:
|
|
201
|
+
"""Return flow-level additional parameters."""
|
|
202
|
+
return self._additional_parameters
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def listeners(self) -> ListenerCollection[AuthenticationListener]:
|
|
206
|
+
"""Return the listener collection."""
|
|
207
|
+
return self._listeners
|
|
208
|
+
|
|
209
|
+
def reset(self) -> None:
|
|
210
|
+
"""Reset state and clear the current context."""
|
|
211
|
+
self._context = None
|
|
212
|
+
self._state = AuthenticationState.IDLE
|
|
213
|
+
|
|
214
|
+
async def _begin(self, context: ContextT | None) -> None:
|
|
215
|
+
"""Begin a new authentication session and notify listeners."""
|
|
216
|
+
async with self._lock:
|
|
217
|
+
if self._state == AuthenticationState.AUTHENTICATING:
|
|
218
|
+
raise RuntimeError("Authentication already in progress")
|
|
219
|
+
self._context = context
|
|
220
|
+
self._state = AuthenticationState.AUTHENTICATING
|
|
221
|
+
flow = cast(AuthenticationFlow[Any], self)
|
|
222
|
+
for listener in self._listeners:
|
|
223
|
+
listener.authentication_started(flow)
|
|
224
|
+
if context is not None:
|
|
225
|
+
for listener in self._listeners:
|
|
226
|
+
listener.authentication_updated(flow, context)
|
|
227
|
+
|
|
228
|
+
def _update_context(self, context: ContextT) -> None:
|
|
229
|
+
"""Update the current context and notify listeners."""
|
|
230
|
+
self._context = context
|
|
231
|
+
flow = cast(AuthenticationFlow[Any], self)
|
|
232
|
+
for listener in self._listeners:
|
|
233
|
+
listener.authentication_updated(flow, context)
|
|
234
|
+
|
|
235
|
+
def _complete(self, result: Any) -> None:
|
|
236
|
+
"""Mark flow as completed and notify listeners."""
|
|
237
|
+
self._state = AuthenticationState.COMPLETED
|
|
238
|
+
flow = cast(AuthenticationFlow[Any], self)
|
|
239
|
+
for listener in self._listeners:
|
|
240
|
+
listener.authentication_completed(flow, result)
|
|
241
|
+
|
|
242
|
+
def _fail(self, error: Exception) -> None:
|
|
243
|
+
"""Mark flow as failed and notify listeners."""
|
|
244
|
+
self._state = AuthenticationState.FAILED
|
|
245
|
+
flow = cast(AuthenticationFlow[Any], self)
|
|
246
|
+
for listener in self._listeners:
|
|
247
|
+
listener.authentication_failed(flow, error)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Awaitable, Callable
|
|
16
|
+
from typing import Generic, TypeVar
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CoalescedResult(Generic[T]):
|
|
22
|
+
"""Coalesce concurrent async operations so only one in-flight task runs.
|
|
23
|
+
|
|
24
|
+
- Caches the last successful result.
|
|
25
|
+
- Errors are propagated to all awaiting callers and are not cached.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, *, ttl: float | None = None, time_provider: Callable[[], float] | None = None) -> None:
|
|
29
|
+
self._lock = asyncio.Lock()
|
|
30
|
+
self._active = False
|
|
31
|
+
self._value: T | None = None
|
|
32
|
+
self._fetched_at: float | None = None
|
|
33
|
+
self._ttl = ttl
|
|
34
|
+
self._time_provider = time_provider or time.time
|
|
35
|
+
self._waiters: list[asyncio.Future[T]] = []
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_active(self) -> bool:
|
|
39
|
+
return self._active
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def value(self) -> T | None:
|
|
43
|
+
return self._value
|
|
44
|
+
|
|
45
|
+
async def perform(
|
|
46
|
+
self,
|
|
47
|
+
operation: Callable[[], Awaitable[T]],
|
|
48
|
+
*,
|
|
49
|
+
reset: bool = False,
|
|
50
|
+
) -> T:
|
|
51
|
+
"""Perform the operation or await the in-flight result.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
operation: Async callable that produces the result.
|
|
55
|
+
reset: If True, clears the cached value before proceeding.
|
|
56
|
+
"""
|
|
57
|
+
future: asyncio.Future[T] | None = None
|
|
58
|
+
async with self._lock:
|
|
59
|
+
if reset:
|
|
60
|
+
self._value = None
|
|
61
|
+
self._fetched_at = None
|
|
62
|
+
if self._active:
|
|
63
|
+
loop = asyncio.get_running_loop()
|
|
64
|
+
future = loop.create_future()
|
|
65
|
+
self._waiters.append(future)
|
|
66
|
+
else:
|
|
67
|
+
if self._is_cache_valid():
|
|
68
|
+
return self._value # type: ignore[return-value]
|
|
69
|
+
self._active = True
|
|
70
|
+
|
|
71
|
+
if future is not None:
|
|
72
|
+
return await future
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
result = await operation()
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
await self._finish(error=exc)
|
|
78
|
+
raise
|
|
79
|
+
else:
|
|
80
|
+
await self._finish(result=result)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
async def _finish(self, result: T | None = None, error: Exception | None = None) -> None:
|
|
84
|
+
async with self._lock:
|
|
85
|
+
if error is None:
|
|
86
|
+
if self._ttl is not None and self._ttl <= 0:
|
|
87
|
+
self._value = None
|
|
88
|
+
self._fetched_at = None
|
|
89
|
+
else:
|
|
90
|
+
self._value = result
|
|
91
|
+
self._fetched_at = self._time_provider()
|
|
92
|
+
self._active = False
|
|
93
|
+
waiters = self._waiters
|
|
94
|
+
self._waiters = []
|
|
95
|
+
|
|
96
|
+
for waiter in waiters:
|
|
97
|
+
if waiter.done():
|
|
98
|
+
continue
|
|
99
|
+
if error is None:
|
|
100
|
+
waiter.set_result(result) # type: ignore[arg-type]
|
|
101
|
+
else:
|
|
102
|
+
waiter.set_exception(error)
|
|
103
|
+
|
|
104
|
+
def _is_cache_valid(self) -> bool:
|
|
105
|
+
if self._value is None:
|
|
106
|
+
return False
|
|
107
|
+
if self._ttl is None:
|
|
108
|
+
return True
|
|
109
|
+
if self._ttl <= 0:
|
|
110
|
+
return False
|
|
111
|
+
if self._fetched_at is None:
|
|
112
|
+
return False
|
|
113
|
+
return (self._time_provider() - self._fetched_at) < self._ttl
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
"""Serialization mixin for frozen dataclasses.
|
|
12
|
+
|
|
13
|
+
Provides a lightweight ``Codable`` protocol (inspired by Swift's ``Codable``)
|
|
14
|
+
that any :func:`~dataclasses.dataclass` can adopt to gain ``to_dict()`` /
|
|
15
|
+
``from_dict()`` round-trip serialization without third-party dependencies.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import dataclasses
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Codable:
|
|
27
|
+
"""Mixin for frozen dataclasses that need dictionary (de)serialization.
|
|
28
|
+
|
|
29
|
+
Provides :meth:`to_dict` for serialization using :func:`dataclasses.asdict`
|
|
30
|
+
(with automatic :class:`~enum.Enum` → value conversion) and a
|
|
31
|
+
:meth:`from_dict` classmethod that subclasses override to reconstruct
|
|
32
|
+
nested types.
|
|
33
|
+
|
|
34
|
+
Usage::
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class MyModel(Codable):
|
|
38
|
+
name: str
|
|
39
|
+
kind: SomeEnum
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "MyModel":
|
|
43
|
+
return cls(
|
|
44
|
+
name=data["name"],
|
|
45
|
+
kind=SomeEnum(data["kind"]),
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict[str, Any]:
|
|
50
|
+
"""Serialize to a plain dict suitable for JSON encoding.
|
|
51
|
+
|
|
52
|
+
Nested dataclasses are recursively converted to dicts.
|
|
53
|
+
:class:`~enum.Enum` members are reduced to their ``.value``.
|
|
54
|
+
"""
|
|
55
|
+
return dataclasses.asdict(self, dict_factory=_enum_aware_dict_factory)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, data: Mapping[str, Any]) -> Codable:
|
|
59
|
+
"""Reconstruct an instance from a plain dict.
|
|
60
|
+
|
|
61
|
+
Subclasses **must** override this to handle nested dataclass and enum
|
|
62
|
+
fields that require type-aware reconstruction.
|
|
63
|
+
"""
|
|
64
|
+
raise NotImplementedError(f"{cls.__name__} must implement from_dict()")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _enum_aware_dict_factory(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
68
|
+
"""Dict factory for :func:`dataclasses.asdict` that converts enum values."""
|
|
69
|
+
return {
|
|
70
|
+
key: value.value if isinstance(value, Enum) else value
|
|
71
|
+
for key, value in pairs
|
|
72
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from .time_coordinator import get_time_coordinator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class Expires(Protocol):
|
|
20
|
+
"""Protocol for objects that can expire based on issued time."""
|
|
21
|
+
@property
|
|
22
|
+
def expires_in(self) -> float:
|
|
23
|
+
"""Return the lifetime in seconds."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def issued_at(self) -> float | None:
|
|
28
|
+
"""Return the issue time in seconds since epoch, if known."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def expires_at(self) -> float | None:
|
|
33
|
+
"""Return the calculated expiration time, if possible."""
|
|
34
|
+
if self.issued_at is None:
|
|
35
|
+
return None
|
|
36
|
+
return float(self.issued_at) + float(self.expires_in)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_expired(self) -> bool:
|
|
40
|
+
"""Return True when the object is expired."""
|
|
41
|
+
expires_at = self.expires_at
|
|
42
|
+
if expires_at is None:
|
|
43
|
+
return False
|
|
44
|
+
return get_time_coordinator().now() >= expires_at
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def is_valid(self) -> bool:
|
|
48
|
+
"""Return True when the object is not expired."""
|
|
49
|
+
return not self.is_expired
|