dara-core 1.22.3__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.
@@ -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;
@@ -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 Any, ClassVar
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) -> Any | TokenData:
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
 
@@ -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"""
@@ -0,0 +1,3 @@
1
+ from .config import OIDCAuthConfig
2
+
3
+ __all__ = ['OIDCAuthConfig']