dara-core 1.23.0a1__py3-none-any.whl → 1.23.1__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.
@@ -27789,8 +27789,10 @@
27789
27789
  `;
27790
27790
  styled.div`
27791
27791
  display: flex;
27792
+ align-items: center;
27792
27793
  gap: 0.5rem;
27793
27794
  width: 100%;
27795
+ line-height: 1;
27794
27796
  `;
27795
27797
  styled.div`
27796
27798
  color: ${(props) => props.theme.colors.grey3};
@@ -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,9 @@ 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
+
63
67
  @classmethod
64
68
  def from_token_data(cls, token_data: TokenData):
65
69
  return cls(
@@ -65,6 +65,7 @@ class OIDCAuthConfig(BaseAuthConfig):
65
65
  - SSO_EXTRA_AUDIENCE - if set, extra audiences to verify against the ID token in addition to `sso_client_id`
66
66
  - SSO_SCOPES - space-separated list of scopes to request from the identity provider, defaults to `openid`
67
67
  - SSO_JWT_ALGO - algorithm to use for verifying IDP-provided JWTs, defaults to `ES256`
68
+ - SSO_USE_USERINFO - if set to `true`, fetch additional claims from the userinfo endpoint when an access token is available
68
69
  """
69
70
 
70
71
  # NOTE: the config follows OIDC specification, but makes a few concessions
@@ -209,29 +210,84 @@ class OIDCAuthConfig(BaseAuthConfig):
209
210
  state = self.generate_state(redirect_to=body.redirect_to)
210
211
  return RedirectResponse(redirect_uri=self.get_authorization_url(state))
211
212
 
212
- def extract_user_data_from_id_token(self, claims: IdTokenClaims) -> UserData:
213
+ async def fetch_userinfo(self, access_token: str) -> dict | None:
213
214
  """
214
- Extract user data from ID token claims.
215
+ Fetch user information from the OIDC userinfo endpoint.
216
+
217
+ Per OpenID Connect Core 1.0 Section 5.3, the userinfo endpoint returns claims
218
+ about the authenticated user. This is useful when the ID token doesn't contain
219
+ all required claims.
220
+
221
+ :param access_token: The access token to authenticate the request
222
+ :return: Dictionary of userinfo claims, or None if the request fails
223
+ """
224
+ userinfo_endpoint = self.discovery.userinfo_endpoint
225
+ if not userinfo_endpoint:
226
+ dev_logger.warning('Userinfo endpoint not available in OIDC discovery')
227
+ return None
228
+
229
+ try:
230
+ response = await self.client.get(
231
+ userinfo_endpoint,
232
+ headers={'Authorization': f'Bearer {access_token}'},
233
+ timeout=10,
234
+ )
235
+ response.raise_for_status()
236
+ return response.json()
237
+ except httpx.HTTPStatusError as e:
238
+ dev_logger.warning(
239
+ f'Failed to fetch userinfo: HTTP {e.response.status_code}',
240
+ )
241
+ return None
242
+ except httpx.RequestError as e:
243
+ dev_logger.warning(f'Failed to fetch userinfo: {e}')
244
+ return None
245
+
246
+ def extract_user_data(self, claims: IdTokenClaims, userinfo: dict | None = None) -> UserData:
247
+ """
248
+ Extract user data from ID token claims and optional userinfo response.
215
249
 
216
250
  Override this method in subclasses to handle provider-specific claim structures.
217
251
  The default implementation uses standard OIDC claims, with support for the
218
- non-standard 'identity' claim.
252
+ non-standard 'identity' claim. When userinfo is provided and SSO_USE_USERINFO
253
+ is enabled, userinfo claims take precedence over ID token claims.
219
254
 
220
255
  :param claims: Decoded ID token claims
256
+ :param userinfo: Optional userinfo response from the userinfo endpoint
221
257
  :return: UserData extracted from the claims
222
258
  """
223
- # Check for non-standard 'identity' claim (Causalens IDP)
224
- # This is a nested object with id, name, email fields
225
- identity_claim = getattr(claims, 'identity', None)
226
- if isinstance(identity_claim, dict):
227
- identity_id = identity_claim.get('id') or claims.sub
228
- identity_name = identity_claim.get('name')
229
- identity_email = identity_claim.get('email') or claims.email
259
+ oidc_settings = get_oidc_settings()
260
+
261
+ # When userinfo is provided and use_userinfo is enabled, prefer userinfo claims
262
+ if userinfo and oidc_settings.use_userinfo:
263
+ # userinfo 'sub' must match id_token 'sub' per OIDC spec
264
+ identity_id = userinfo.get('sub') or claims.sub
265
+ identity_email = userinfo.get('email') or claims.email
266
+ identity_name = (
267
+ userinfo.get('name')
268
+ or userinfo.get('preferred_username')
269
+ or userinfo.get('nickname')
270
+ or (
271
+ f'{userinfo.get("given_name", "")} {userinfo.get("family_name", "")}'.strip()
272
+ if userinfo.get('given_name') or userinfo.get('family_name')
273
+ else None
274
+ )
275
+ )
276
+ groups = userinfo.get('groups') or claims.groups
230
277
  else:
231
- # Standard OIDC: use 'sub' as the identity ID
232
- identity_id = claims.sub
233
- identity_email = claims.email
234
- identity_name = None
278
+ # Check for non-standard 'identity' claim (Causalens IDP)
279
+ # This is a nested object with id, name, email fields
280
+ identity_claim = getattr(claims, 'identity', None)
281
+ if isinstance(identity_claim, dict):
282
+ identity_id = identity_claim.get('id') or claims.sub
283
+ identity_name = identity_claim.get('name')
284
+ identity_email = identity_claim.get('email') or claims.email
285
+ else:
286
+ # Standard OIDC: use 'sub' as the identity ID
287
+ identity_id = claims.sub
288
+ identity_email = claims.email
289
+ identity_name = None
290
+ groups = claims.groups
235
291
 
236
292
  # Fall back to standard claims for name if not set
237
293
  if not identity_name:
@@ -252,7 +308,7 @@ class OIDCAuthConfig(BaseAuthConfig):
252
308
  identity_id=identity_id,
253
309
  identity_name=identity_name,
254
310
  identity_email=identity_email,
255
- groups=claims.groups,
311
+ groups=groups,
256
312
  )
257
313
 
258
314
  def verify_token(self, token: str) -> TokenData:
@@ -291,7 +347,7 @@ class OIDCAuthConfig(BaseAuthConfig):
291
347
  claims = decode_id_token(token)
292
348
 
293
349
  # Extract user data (can be overridden for provider-specific claim structures)
294
- user_data = self.extract_user_data_from_id_token(claims)
350
+ user_data = self.extract_user_data(claims)
295
351
 
296
352
  # Verify user has access based on groups
297
353
  self.verify_user_access(user_data)
@@ -390,6 +446,8 @@ class OIDCAuthConfig(BaseAuthConfig):
390
446
  :return: Tuple of (new_session_token, new_refresh_token)
391
447
  :raises HTTPException: If the refresh fails
392
448
  """
449
+ oidc_settings = get_oidc_settings()
450
+
393
451
  # Request new tokens from the IDP
394
452
  oidc_tokens = await get_token_from_idp(
395
453
  self,
@@ -406,8 +464,13 @@ class OIDCAuthConfig(BaseAuthConfig):
406
464
  # Decode and verify the new ID token
407
465
  claims = decode_id_token(oidc_tokens.id_token)
408
466
 
467
+ # Fetch userinfo if enabled and we have an access token
468
+ userinfo = None
469
+ if oidc_settings.use_userinfo and oidc_tokens.access_token:
470
+ userinfo = await self.fetch_userinfo(oidc_tokens.access_token)
471
+
409
472
  # Extract user data from claims
410
- user_data = self.extract_user_data_from_id_token(claims)
473
+ user_data = self.extract_user_data(claims, userinfo=userinfo)
411
474
 
412
475
  # Verify user still has access
413
476
  self.verify_user_access(user_data)
@@ -100,8 +100,13 @@ async def sso_callback(
100
100
  # Decode and verify the ID token
101
101
  claims = decode_id_token(oidc_tokens.id_token)
102
102
 
103
+ # Fetch userinfo if enabled and we have an access token
104
+ userinfo = None
105
+ if oidc_settings.use_userinfo and oidc_tokens.access_token:
106
+ userinfo = await auth_config.fetch_userinfo(oidc_tokens.access_token)
107
+
103
108
  # Extract user data from claims (handles both standard OIDC and Causalens identity claim)
104
- user_data = auth_config.extract_user_data_from_id_token(claims)
109
+ user_data = auth_config.extract_user_data(claims, userinfo=userinfo)
105
110
 
106
111
  # Verify user has access based on groups
107
112
  auth_config.verify_user_access(user_data)
@@ -24,6 +24,8 @@ class OIDCSettings(BaseSettings):
24
24
  verify_audience: bool = False
25
25
  extra_audience: list[str] | None = None
26
26
  allowed_identity_id: str | None = None
27
+ use_userinfo: bool = False
28
+ """If True, fetch additional claims from the userinfo endpoint when an access token is available."""
27
29
 
28
30
  model_config = SettingsConfigDict(env_file='.env', extra='allow', env_prefix='sso_')
29
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dara-core
3
- Version: 1.23.0a1
3
+ Version: 1.23.1
4
4
  Summary: Dara Framework Core
5
5
  Home-page: https://dara.causalens.com/
6
6
  License: Apache-2.0
@@ -21,10 +21,10 @@ Requires-Dist: cachetools (>=5.0.0)
21
21
  Requires-Dist: certifi (>=2024.7.4)
22
22
  Requires-Dist: click (>=8.1.3,<9.0.0)
23
23
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
24
- Requires-Dist: create-dara-app (==1.23.0-alpha.1)
24
+ Requires-Dist: create-dara-app (==1.23.1)
25
25
  Requires-Dist: croniter (>=6.0.0,<7.0.0)
26
26
  Requires-Dist: cryptography (>=42.0.4)
27
- Requires-Dist: dara-components (==1.23.0-alpha.1) ; extra == "all"
27
+ Requires-Dist: dara-components (==1.23.1) ; extra == "all"
28
28
  Requires-Dist: exceptiongroup (>=1.1.3,<2.0.0)
29
29
  Requires-Dist: fastapi (>=0.115.0,<0.121.0)
30
30
  Requires-Dist: fastapi_vite_dara (==0.4.0)
@@ -55,7 +55,7 @@ Description-Content-Type: text/markdown
55
55
 
56
56
  # Dara Application Framework
57
57
 
58
- <img src="https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/dara_light.svg?raw=true">
58
+ <img src="https://github.com/causalens/dara/blob/v1.23.1/img/dara_light.svg?raw=true">
59
59
 
60
60
  ![Master tests](https://github.com/causalens/dara/actions/workflows/tests.yml/badge.svg?branch=master)
61
61
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
@@ -100,7 +100,7 @@ source .venv/bin/activate
100
100
  dara start
101
101
  ```
102
102
 
103
- ![Dara App](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/components_gallery.png?raw=true)
103
+ ![Dara App](https://github.com/causalens/dara/blob/v1.23.1/img/components_gallery.png?raw=true)
104
104
 
105
105
  Note: `pip` installation uses [PEP 660](https://peps.python.org/pep-0660/) `pyproject.toml`-based editable installs which require `pip >= 21.3` and `setuptools >= 64.0.0`. You can upgrade both with:
106
106
 
@@ -117,9 +117,9 @@ Explore some of our favorite apps - a great way of getting started and getting t
117
117
 
118
118
  | Dara App | Description |
119
119
  | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | ![Large Language Model](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
121
- | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/plot_interactivity.png?raw=true) | Demonstrates how to enable the user to interact with plots, trigger actions based on clicks, mouse movements and other interactions with `Bokeh` or `Plotly` plots |
122
- | ![Graph Editor](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/graph_viewer.png?raw=true) | Demonstrates how to use the `CausalGraphViewer` component to display your graphs or networks, customising the displayed information through colors and tooltips, and updating the page based on user interaction. |
120
+ | ![Large Language Model](https://github.com/causalens/dara/blob/v1.23.1/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
121
+ | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.23.1/img/plot_interactivity.png?raw=true) | Demonstrates how to enable the user to interact with plots, trigger actions based on clicks, mouse movements and other interactions with `Bokeh` or `Plotly` plots |
122
+ | ![Graph Editor](https://github.com/causalens/dara/blob/v1.23.1/img/graph_viewer.png?raw=true) | Demonstrates how to use the `CausalGraphViewer` component to display your graphs or networks, customising the displayed information through colors and tooltips, and updating the page based on user interaction. |
123
123
 
124
124
  Check out our [App Gallery](https://dara.causalens.com/gallery) for more inspiration!
125
125
 
@@ -146,9 +146,9 @@ And the supporting UI packages and tools.
146
146
  - `ui-utils` - miscellaneous utility functions
147
147
  - `ui-widgets` - widget components
148
148
 
149
- More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/CONTRIBUTING.md) file.
149
+ More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.23.1/CONTRIBUTING.md) file.
150
150
 
151
151
  ## License
152
152
 
153
- Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/LICENSE).
153
+ Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.23.1/LICENSE).
154
154
 
@@ -1,7 +1,7 @@
1
1
  dara/core/__init__.py,sha256=yTp-lXT0yy9XqLGYWlmjPgFG5g2eEg2KhKo8KheTHoo,1408
2
2
  dara/core/_assets/__init__.py,sha256=13vMoWHvl1zcFcjNHh8lbTwWOvu4f7krYSco978qxwM,723
3
3
  dara/core/_assets/auto_js/dara.core.css,sha256=yT3PKpi2sKI2-kQIF8xtVbTPQqgpK7-Ua7tfzDPuSsI,4095881
4
- dara/core/_assets/auto_js/dara.core.umd.cjs,sha256=T9MOYSRnuHnraSboQ7NcfodV8vrBkNGISoCZQCuc4Ec,5163725
4
+ dara/core/_assets/auto_js/dara.core.umd.cjs,sha256=hvmFXD-p5KiQFZdoyp6z-Js_DG6r2bFIzXKoGtj5QFQ,5163770
5
5
  dara/core/_assets/auto_js/react-dom.development.js,sha256=vR2Fq5LXMKS5JsTo2CMl6oGCJT8scJV2wXSteTtx8aE,1077040
6
6
  dara/core/_assets/auto_js/react-is.development.js,sha256=2IRgmaphdMq6wx2MbsmVUQ0UwapGETNjgano3XEkGcc,7932
7
7
  dara/core/_assets/auto_js/react-query.development.js,sha256=lI2fTKMvWmjbagGQaVtZYr51_-C_U_so064JwetuDS0,130366
@@ -12,12 +12,12 @@ dara/core/actions.py,sha256=rC5Tu79AFNWMv0CJuchBnoy6pETIFh_1RTSqxrolArI,947
12
12
  dara/core/auth/__init__.py,sha256=HJoYIVPzbpwzN_RUHjGpSJj4o5TmHz9yFyZGiRiObCk,906
13
13
  dara/core/auth/base.py,sha256=NJmUJqA-W8AVKIQbX_0BoHoZqtU1Iz6cJ16RKdcdIyU,3642
14
14
  dara/core/auth/basic.py,sha256=sglIaogCslG2HlDMjFsaaJhOJeXUW-QQLTIYPaUPxAU,4927
15
- dara/core/auth/definitions.py,sha256=OzJshDLuut8MaM_pbfyQFkSlUuMNBWNszA57jRXF0Cg,3848
15
+ dara/core/auth/definitions.py,sha256=ZZMXJbMFY42Q5YaJCMwZT0bivnxq7TaIoug7uYqAGzE,3988
16
16
  dara/core/auth/oidc/__init__.py,sha256=UWdhFvDqLCoILaKVWbmrrJgiMgg9wlVZgCxRvf_HGHM,65
17
- dara/core/auth/oidc/config.py,sha256=5rDxbr3wVZdjIz8USYNeUmKwKfeucHtLgfXwM3_weaE,20835
17
+ dara/core/auth/oidc/config.py,sha256=XdiJSC2-9g8fqM-FkmU0c079VqnrApVv-MKwEBwk50s,23753
18
18
  dara/core/auth/oidc/definitions.py,sha256=KLlUl2Y7n6prX0JSAwPde6J43iMs6uUY07OG7kumRPw,17568
19
- dara/core/auth/oidc/routes.py,sha256=uvNHYUieaXy9hu6MYe_5eZvflX5TmFaIv6EGpCRP-dw,5335
20
- dara/core/auth/oidc/settings.py,sha256=k1CKDTUKSFoCEp1ASI_R5L_lsNkFzjfAymMMIMmI1AE,2097
19
+ dara/core/auth/oidc/routes.py,sha256=wlYp8VZP4C4BFHqJPPYHi3PdLd8SetdG2CtG3B4laBk,5579
20
+ dara/core/auth/oidc/settings.py,sha256=wU6fmkqhTNzMNFUxizFaaavH2cKuXeqRIzMi3TpwLhA,2233
21
21
  dara/core/auth/oidc/utils.py,sha256=IL17tStRNpkAQI3M17nrzGeb4xr47zSLPo4wT7LMwBQ,5145
22
22
  dara/core/auth/routes.py,sha256=dtOxpFotnt4XQ4spW3mbyM7ThYRvfIA_oRK5X5lyYHg,7256
23
23
  dara/core/auth/utils.py,sha256=_aHZ98qMJ0VLE9Zfvj5biPARhe973_eyTx2qxnufzRE,7290
@@ -134,8 +134,8 @@ dara/core/visual/themes/__init__.py,sha256=aM4mgoIYo2neBSw5FRzswsht7PUKjLthiHLmF
134
134
  dara/core/visual/themes/dark.py,sha256=QazCRDqh_SCOyQhdwMkH1wbHf301oL7gCFj91plbLww,2020
135
135
  dara/core/visual/themes/definitions.py,sha256=dtET2YUlwXkO6gJ23MqSb8gIq-LxJ343CWsgueWSifM,2787
136
136
  dara/core/visual/themes/light.py,sha256=dtHb6Q1HOb5r_AvJfe0vZajikVc-GnBEUrGsTcI5MHA,2022
137
- dara_core-1.23.0a1.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
138
- dara_core-1.23.0a1.dist-info/METADATA,sha256=WH9TER9_ycRXWaU40zxhvD5teCWhUbExsBay21YHW_w,7598
139
- dara_core-1.23.0a1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
140
- dara_core-1.23.0a1.dist-info/entry_points.txt,sha256=nAT9o1kJCmTK1saDh29PFGFD6cbxDDDjTj31HDEDwfU,197
141
- dara_core-1.23.0a1.dist-info/RECORD,,
137
+ dara_core-1.23.1.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
138
+ dara_core-1.23.1.dist-info/METADATA,sha256=gVUjEVltqKHlaPr_tu3FypSAhpceR2M1-HJCwP8cT84,7524
139
+ dara_core-1.23.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
140
+ dara_core-1.23.1.dist-info/entry_points.txt,sha256=nAT9o1kJCmTK1saDh29PFGFD6cbxDDDjTj31HDEDwfU,197
141
+ dara_core-1.23.1.dist-info/RECORD,,