dara-core 1.22.4__py3-none-any.whl → 1.23.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.
- dara/core/_assets/auto_js/dara.core.umd.cjs +175 -6
- dara/core/auth/__init__.py +2 -0
- dara/core/auth/base.py +15 -3
- dara/core/auth/definitions.py +17 -0
- dara/core/auth/oidc/__init__.py +3 -0
- dara/core/auth/oidc/config.py +586 -0
- dara/core/auth/oidc/definitions.py +312 -0
- dara/core/auth/oidc/routes.py +147 -0
- dara/core/auth/oidc/settings.py +60 -0
- dara/core/auth/oidc/utils.py +162 -0
- dara/core/auth/utils.py +5 -4
- dara/core/configuration.py +6 -2
- dara/core/internal/settings.py +2 -27
- dara/core/internal/utils.py +4 -9
- dara/core/internal/websocket.py +3 -4
- {dara_core-1.22.4.dist-info → dara_core-1.23.0.dist-info}/METADATA +10 -10
- {dara_core-1.22.4.dist-info → dara_core-1.23.0.dist-info}/RECORD +20 -14
- {dara_core-1.22.4.dist-info → dara_core-1.23.0.dist-info}/LICENSE +0 -0
- {dara_core-1.22.4.dist-info → dara_core-1.23.0.dist-info}/WHEEL +0 -0
- {dara_core-1.22.4.dist-info → dara_core-1.23.0.dist-info}/entry_points.txt +0 -0
|
@@ -40389,11 +40389,6 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
|
|
|
40389
40389
|
const mergedInits = React$1.useMemo(() => mergeRequestInits(parentOptions, options), [parentOptions, options]);
|
|
40390
40390
|
return /* @__PURE__ */ React.createElement(requestExtrasCtx.Provider, { value: { options: mergedInits } }, children);
|
|
40391
40391
|
}
|
|
40392
|
-
var AuthType = /* @__PURE__ */ ((AuthType2) => {
|
|
40393
|
-
AuthType2["BASIC"] = "BASIC";
|
|
40394
|
-
AuthType2["SSO"] = "SSO";
|
|
40395
|
-
return AuthType2;
|
|
40396
|
-
})(AuthType || {});
|
|
40397
40392
|
function $constructor(name, initializer2, params) {
|
|
40398
40393
|
function init(inst, def) {
|
|
40399
40394
|
var _a;
|
|
@@ -74361,6 +74356,178 @@ Inferred class string: "${iconClasses}."`
|
|
|
74361
74356
|
}, []);
|
|
74362
74357
|
return /* @__PURE__ */ React.createElement(Center, null, /* @__PURE__ */ React.createElement(DefaultFallback$1, null));
|
|
74363
74358
|
}
|
|
74359
|
+
function OIDCAuthLogin() {
|
|
74360
|
+
const navigate = useNavigate();
|
|
74361
|
+
const location2 = useLocation();
|
|
74362
|
+
const token = useSessionToken();
|
|
74363
|
+
const { defaultPath } = useRouterContext();
|
|
74364
|
+
const previousLocation = React$1.useMemo(() => {
|
|
74365
|
+
const queryParams = new URLSearchParams(location2.search);
|
|
74366
|
+
return queryParams.get("referrer") ?? defaultPath;
|
|
74367
|
+
}, [location2, defaultPath]);
|
|
74368
|
+
const verifyToken = React$1.useCallback(async () => {
|
|
74369
|
+
const verified = await verifySessionToken();
|
|
74370
|
+
if (verified) {
|
|
74371
|
+
navigate(decodeURIComponent(previousLocation), { replace: true });
|
|
74372
|
+
} else {
|
|
74373
|
+
navigate("/logout", { replace: true });
|
|
74374
|
+
}
|
|
74375
|
+
}, [previousLocation, navigate]);
|
|
74376
|
+
const getNewToken = React$1.useCallback(async () => {
|
|
74377
|
+
const res = await request("/api/auth/session", {
|
|
74378
|
+
body: JSON.stringify({
|
|
74379
|
+
redirect_to: previousLocation
|
|
74380
|
+
}),
|
|
74381
|
+
method: HTTP_METHOD.POST
|
|
74382
|
+
});
|
|
74383
|
+
const loggedOut = await handleAuthErrors(res, false);
|
|
74384
|
+
if (loggedOut) {
|
|
74385
|
+
return;
|
|
74386
|
+
}
|
|
74387
|
+
if (res.ok) {
|
|
74388
|
+
const resContent = await res.json();
|
|
74389
|
+
window.location.href = resContent.redirect_uri;
|
|
74390
|
+
}
|
|
74391
|
+
}, [previousLocation]);
|
|
74392
|
+
React$1.useEffect(() => {
|
|
74393
|
+
if (token) {
|
|
74394
|
+
verifyToken();
|
|
74395
|
+
} else {
|
|
74396
|
+
getNewToken();
|
|
74397
|
+
}
|
|
74398
|
+
}, [getNewToken, verifyToken, token]);
|
|
74399
|
+
return /* @__PURE__ */ React.createElement(Center, null, /* @__PURE__ */ React.createElement(DefaultFallback$1, null));
|
|
74400
|
+
}
|
|
74401
|
+
function OIDCAuthLogout() {
|
|
74402
|
+
const navigate = useNavigate();
|
|
74403
|
+
React$1.useEffect(() => {
|
|
74404
|
+
revokeSession().then((responseData) => {
|
|
74405
|
+
setSessionToken(null);
|
|
74406
|
+
if (responseData && "redirect_uri" in responseData) {
|
|
74407
|
+
const loginUrl = new URL("/login", window.location.origin);
|
|
74408
|
+
const finalRedirectUrl = new URL(responseData.redirect_uri);
|
|
74409
|
+
finalRedirectUrl.searchParams.append("post_logout_redirect_uri", loginUrl.toString());
|
|
74410
|
+
window.location.href = finalRedirectUrl.toString();
|
|
74411
|
+
return;
|
|
74412
|
+
}
|
|
74413
|
+
navigate("/login");
|
|
74414
|
+
});
|
|
74415
|
+
}, []);
|
|
74416
|
+
return /* @__PURE__ */ React.createElement(Center, null, /* @__PURE__ */ React.createElement(DefaultFallback$1, null));
|
|
74417
|
+
}
|
|
74418
|
+
class InvalidTokenError extends Error {
|
|
74419
|
+
}
|
|
74420
|
+
InvalidTokenError.prototype.name = "InvalidTokenError";
|
|
74421
|
+
function b64DecodeUnicode(str) {
|
|
74422
|
+
return decodeURIComponent(atob(str).replace(/(.)/g, (m, p2) => {
|
|
74423
|
+
let code = p2.charCodeAt(0).toString(16).toUpperCase();
|
|
74424
|
+
if (code.length < 2) {
|
|
74425
|
+
code = "0" + code;
|
|
74426
|
+
}
|
|
74427
|
+
return "%" + code;
|
|
74428
|
+
}));
|
|
74429
|
+
}
|
|
74430
|
+
function base64UrlDecode(str) {
|
|
74431
|
+
let output = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
74432
|
+
switch (output.length % 4) {
|
|
74433
|
+
case 0:
|
|
74434
|
+
break;
|
|
74435
|
+
case 2:
|
|
74436
|
+
output += "==";
|
|
74437
|
+
break;
|
|
74438
|
+
case 3:
|
|
74439
|
+
output += "=";
|
|
74440
|
+
break;
|
|
74441
|
+
default:
|
|
74442
|
+
throw new Error("base64 string is not of the correct length");
|
|
74443
|
+
}
|
|
74444
|
+
try {
|
|
74445
|
+
return b64DecodeUnicode(output);
|
|
74446
|
+
} catch (err2) {
|
|
74447
|
+
return atob(output);
|
|
74448
|
+
}
|
|
74449
|
+
}
|
|
74450
|
+
function jwtDecode(token, options) {
|
|
74451
|
+
if (typeof token !== "string") {
|
|
74452
|
+
throw new InvalidTokenError("Invalid token specified: must be a string");
|
|
74453
|
+
}
|
|
74454
|
+
options || (options = {});
|
|
74455
|
+
const pos = options.header === true ? 0 : 1;
|
|
74456
|
+
const part = token.split(".")[pos];
|
|
74457
|
+
if (typeof part !== "string") {
|
|
74458
|
+
throw new InvalidTokenError(`Invalid token specified: missing part #${pos + 1}`);
|
|
74459
|
+
}
|
|
74460
|
+
let decoded;
|
|
74461
|
+
try {
|
|
74462
|
+
decoded = base64UrlDecode(part);
|
|
74463
|
+
} catch (e2) {
|
|
74464
|
+
throw new InvalidTokenError(`Invalid token specified: invalid base64 for part #${pos + 1} (${e2.message})`);
|
|
74465
|
+
}
|
|
74466
|
+
try {
|
|
74467
|
+
return JSON.parse(decoded);
|
|
74468
|
+
} catch (e2) {
|
|
74469
|
+
throw new InvalidTokenError(`Invalid token specified: invalid json for part #${pos + 1} (${e2.message})`);
|
|
74470
|
+
}
|
|
74471
|
+
}
|
|
74472
|
+
function decodeStateRedirect(state) {
|
|
74473
|
+
if (!state) {
|
|
74474
|
+
return null;
|
|
74475
|
+
}
|
|
74476
|
+
try {
|
|
74477
|
+
const payload = jwtDecode(state);
|
|
74478
|
+
return payload.redirect_to ?? null;
|
|
74479
|
+
} catch {
|
|
74480
|
+
try {
|
|
74481
|
+
return decodeURIComponent(state);
|
|
74482
|
+
} catch {
|
|
74483
|
+
return null;
|
|
74484
|
+
}
|
|
74485
|
+
}
|
|
74486
|
+
}
|
|
74487
|
+
async function getSSOCallbackToken(search, defaultPath) {
|
|
74488
|
+
try {
|
|
74489
|
+
const params = new URLSearchParams(search);
|
|
74490
|
+
const state = params.get("state");
|
|
74491
|
+
const res = await request("/api/auth/sso-callback", {
|
|
74492
|
+
body: JSON.stringify({
|
|
74493
|
+
auth_code: params.get("code"),
|
|
74494
|
+
state
|
|
74495
|
+
}),
|
|
74496
|
+
method: HTTP_METHOD.POST
|
|
74497
|
+
});
|
|
74498
|
+
const shouldLogOut = await handleAuthErrors(res);
|
|
74499
|
+
if (shouldLogOut) {
|
|
74500
|
+
return null;
|
|
74501
|
+
}
|
|
74502
|
+
if (res.ok) {
|
|
74503
|
+
const { token } = await res.json();
|
|
74504
|
+
return {
|
|
74505
|
+
token,
|
|
74506
|
+
redirectTo: decodeStateRedirect(state) ?? defaultPath
|
|
74507
|
+
};
|
|
74508
|
+
}
|
|
74509
|
+
throw new Error(`${res.status}: ${res.statusText}`);
|
|
74510
|
+
} catch {
|
|
74511
|
+
return null;
|
|
74512
|
+
}
|
|
74513
|
+
}
|
|
74514
|
+
function OIDCAuthSSOCallback() {
|
|
74515
|
+
const { search } = useLocation();
|
|
74516
|
+
const navigate = useNavigate();
|
|
74517
|
+
const routerContext = useRouterContext();
|
|
74518
|
+
React$1.useEffect(() => {
|
|
74519
|
+
getSSOCallbackToken(search, routerContext.defaultPath).then((result) => {
|
|
74520
|
+
if (result) {
|
|
74521
|
+
setSessionToken(result.token);
|
|
74522
|
+
navigate(result.redirectTo);
|
|
74523
|
+
}
|
|
74524
|
+
}).catch((err2) => {
|
|
74525
|
+
console.error("Failed to run SSO callback", err2);
|
|
74526
|
+
navigate("/logout");
|
|
74527
|
+
});
|
|
74528
|
+
}, []);
|
|
74529
|
+
return /* @__PURE__ */ React.createElement(Center, null, /* @__PURE__ */ React.createElement(DefaultFallback$1, null));
|
|
74530
|
+
}
|
|
74364
74531
|
const CenteredDivWithGap$1 = styled(Center)`
|
|
74365
74532
|
gap: 1rem;
|
|
74366
74533
|
margin: 10px;
|
|
@@ -98864,7 +99031,6 @@ body,
|
|
|
98864
99031
|
return /* @__PURE__ */ React.createElement(DynamicComponent$1, { component });
|
|
98865
99032
|
}
|
|
98866
99033
|
exports.ActionImpl = ActionImpl;
|
|
98867
|
-
exports.AuthType = AuthType;
|
|
98868
99034
|
exports.AuthenticatedRoot = AuthenticatedRoot;
|
|
98869
99035
|
exports.BasicAuthLogin = BasicAuthLogin;
|
|
98870
99036
|
exports.BasicAuthLogout = BasicAuthLogout;
|
|
@@ -98897,6 +99063,9 @@ body,
|
|
|
98897
99063
|
exports.NavigateTo = NavigateTo;
|
|
98898
99064
|
exports.Notifications = index$1;
|
|
98899
99065
|
exports.Notify = Notify;
|
|
99066
|
+
exports.OIDCAuthLogin = OIDCAuthLogin;
|
|
99067
|
+
exports.OIDCAuthLogout = OIDCAuthLogout;
|
|
99068
|
+
exports.OIDCAuthSSOCallback = OIDCAuthSSOCallback;
|
|
98900
99069
|
exports.Outlet = Outlet;
|
|
98901
99070
|
exports.PartialRequestExtrasProvider = PartialRequestExtrasProvider;
|
|
98902
99071
|
exports.PoweredByCausalens = PoweredByCausalens;
|
dara/core/auth/__init__.py
CHANGED
|
@@ -17,6 +17,7 @@ limitations under the License.
|
|
|
17
17
|
|
|
18
18
|
from .base import BaseAuthConfig
|
|
19
19
|
from .basic import BasicAuthConfig, DefaultAuthConfig, MultiBasicAuthConfig
|
|
20
|
+
from .oidc import OIDCAuthConfig
|
|
20
21
|
from .routes import auth_router
|
|
21
22
|
|
|
22
23
|
__all__ = [
|
|
@@ -24,5 +25,6 @@ __all__ = [
|
|
|
24
25
|
'MultiBasicAuthConfig',
|
|
25
26
|
'BasicAuthConfig',
|
|
26
27
|
'DefaultAuthConfig',
|
|
28
|
+
'OIDCAuthConfig',
|
|
27
29
|
'auth_router',
|
|
28
30
|
]
|
dara/core/auth/base.py
CHANGED
|
@@ -16,7 +16,7 @@ limitations under the License.
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import abc
|
|
19
|
-
from typing import
|
|
19
|
+
from typing import ClassVar
|
|
20
20
|
|
|
21
21
|
from fastapi import HTTPException, Response
|
|
22
22
|
from pydantic import model_serializer
|
|
@@ -30,6 +30,7 @@ from dara.core.auth.definitions import (
|
|
|
30
30
|
TokenResponse,
|
|
31
31
|
)
|
|
32
32
|
from dara.core.base_definitions import DaraBaseModel as BaseModel
|
|
33
|
+
from dara.core.definitions import ApiRoute
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class AuthComponent(TypedDict):
|
|
@@ -71,6 +72,17 @@ class BaseAuthConfig(BaseModel, abc.ABC):
|
|
|
71
72
|
Defines components to use for auth routes
|
|
72
73
|
"""
|
|
73
74
|
|
|
75
|
+
required_routes: ClassVar[list[ApiRoute]] = []
|
|
76
|
+
"""
|
|
77
|
+
List of routes the auth config depends on.
|
|
78
|
+
Will be added to the app if this auth config is used.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
async def startup_hook(self) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Called when the server is starting up, can be used to set up e.g. JWKS clients for OIDC auth
|
|
84
|
+
"""
|
|
85
|
+
|
|
74
86
|
@abc.abstractmethod
|
|
75
87
|
def get_token(self, body: SessionRequestBody) -> TokenResponse | RedirectResponse:
|
|
76
88
|
"""
|
|
@@ -82,7 +94,7 @@ class BaseAuthConfig(BaseModel, abc.ABC):
|
|
|
82
94
|
"""
|
|
83
95
|
|
|
84
96
|
@abc.abstractmethod
|
|
85
|
-
def verify_token(self, token: str) ->
|
|
97
|
+
def verify_token(self, token: str) -> TokenData:
|
|
86
98
|
"""
|
|
87
99
|
Verify a session token.
|
|
88
100
|
|
|
@@ -93,7 +105,7 @@ class BaseAuthConfig(BaseModel, abc.ABC):
|
|
|
93
105
|
:param token: encoded token
|
|
94
106
|
"""
|
|
95
107
|
|
|
96
|
-
def refresh_token(self, old_token: TokenData, refresh_token: str) -> tuple[str, str]:
|
|
108
|
+
async def refresh_token(self, old_token: TokenData, refresh_token: str) -> tuple[str, str]:
|
|
97
109
|
"""
|
|
98
110
|
Create a new session token and refresh token from a refresh token.
|
|
99
111
|
|
dara/core/auth/definitions.py
CHANGED
|
@@ -18,6 +18,7 @@ limitations under the License.
|
|
|
18
18
|
from contextvars import ContextVar
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
|
|
21
|
+
from pydantic import ConfigDict
|
|
21
22
|
from typing_extensions import TypedDict
|
|
22
23
|
|
|
23
24
|
from dara.core.base_definitions import DaraBaseModel as BaseModel
|
|
@@ -60,6 +61,18 @@ class UserData(BaseModel):
|
|
|
60
61
|
identity_email: str | None = None
|
|
61
62
|
groups: list[str] | None = []
|
|
62
63
|
|
|
64
|
+
# allow extra for more flexibility in custom oidc configs
|
|
65
|
+
model_config = ConfigDict(extra='allow')
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_token_data(cls, token_data: TokenData):
|
|
69
|
+
return cls(
|
|
70
|
+
identity_id=token_data.identity_id,
|
|
71
|
+
identity_name=token_data.identity_name,
|
|
72
|
+
identity_email=token_data.identity_email,
|
|
73
|
+
groups=token_data.groups,
|
|
74
|
+
)
|
|
75
|
+
|
|
63
76
|
|
|
64
77
|
class TokenResponse(TypedDict):
|
|
65
78
|
token: str
|
|
@@ -76,6 +89,8 @@ class SuccessResponse(TypedDict):
|
|
|
76
89
|
class SessionRequestBody(BaseModel):
|
|
77
90
|
username: str | None = None
|
|
78
91
|
password: str | None = None
|
|
92
|
+
redirect_to: str | None = None
|
|
93
|
+
"""Optional URL to redirect to after successful authentication (used in OIDC flows)"""
|
|
79
94
|
|
|
80
95
|
|
|
81
96
|
class AuthError(Exception):
|
|
@@ -116,4 +131,6 @@ JWT_ALGO = 'HS256'
|
|
|
116
131
|
# Context
|
|
117
132
|
SESSION_ID: ContextVar[str | None] = ContextVar('session_id', default=None)
|
|
118
133
|
USER: ContextVar[UserData | None] = ContextVar('user', default=None)
|
|
134
|
+
|
|
119
135
|
ID_TOKEN: ContextVar[str | None] = ContextVar('id_token', default=None)
|
|
136
|
+
"""Current ID token, set when using OIDC auth"""
|