Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement: DO-3034 support seamless handling of refresh tokens #345

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d0312d3
WIP: remove auth context in favor of global store; add token refresh …
krzysztof-causalens Jul 19, 2024
22b021a
Http utils test
krzysztof-causalens Jul 22, 2024
ed078aa
Add tests for refresh token endpoint
krzysztof-causalens Oct 17, 2024
1e8057e
Make refresh sync
krzysztof-causalens Oct 17, 2024
b9afd53
Merge branch 'master' into DO-3034-make-dara-use-refresh-tokens-in-a-…
krzysztof-causalens Oct 17, 2024
048ce04
Lint
krzysztof-causalens Oct 17, 2024
d1e4809
Fix JS tests
krzysztof-causalens Oct 17, 2024
e71e006
Version bump to v1.14.0-alpha.0 [skip ci]
Oct 18, 2024
e6df3a0
Add automatic token refresh, pass previous session_token to refresh_t…
krzysztof-causalens Oct 18, 2024
6ab911a
Version bump to v1.14.0-alpha.1 [skip ci]
Oct 18, 2024
d6d591f
Double notify
krzysztof-causalens Oct 21, 2024
991ba71
Update changelog
krzysztof-causalens Oct 21, 2024
94da85a
Remove unnecessary refresh interval
krzysztof-causalens Oct 21, 2024
f843596
Improve global store documentation
krzysztof-causalens Oct 21, 2024
db3dde9
Notify live WS connections of token data changes to keep auth Context…
krzysztof-causalens Oct 21, 2024
26625df
Lint fix
krzysztof-causalens Oct 21, 2024
29797f7
Version bump to v1.14.0-alpha.2 [skip ci]
Oct 22, 2024
23c3870
Remove backend WS connection update, use client->server message inste…
krzysztof-causalens Oct 23, 2024
9236d62
Add server-side cache to prevent concurrent token refreshes or subseq…
krzysztof-causalens Oct 23, 2024
dd515f6
Version bump to v1.14.0-alpha.3 [skip ci]
Oct 23, 2024
0a74d34
Cleanup
krzysztof-causalens Oct 23, 2024
f8f85b0
Revert changelogs to NEXT
krzysztof-causalens Oct 23, 2024
968d1a7
OOps
krzysztof-causalens Oct 23, 2024
7bac379
Use direct subscription instead
krzysztof-causalens Oct 23, 2024
ae8ebb5
Rename arg
krzysztof-causalens Oct 23, 2024
d5be0bb
Add cancellation for test CI
krzysztof-causalens Oct 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ on:

jobs:
lint-and-tests:
concurrency:
group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"npmClient": "pnpm",
"version": "1.13.1",
"version": "1.14.0-alpha.3",
"packages": [
"packages/*"
],
Expand Down
2 changes: 1 addition & 1 deletion packages/create-dara-app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ license = "Apache-2.0"
name = "create-dara-app"
readme = "README.md"
repository = "https://github.com/causalens/dara"
version = "1.13.1"
version = "1.14.0-alpha.3"
source = []

[tool.poetry.dependencies]
Expand Down
2 changes: 1 addition & 1 deletion packages/dara-components/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Changelog
---

# NEXT
## NEXT

- Fixed an issue where `Plotly`'s figure native `height` and `width` were not obeyed by component.

Expand Down
24 changes: 12 additions & 12 deletions packages/dara-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@darajs/components",
"version": "1.13.1",
"version": "1.14.0-alpha.3",
"description": "Components for the Dara framework",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down Expand Up @@ -29,9 +29,9 @@
"prettier": "@darajs/prettier-config",
"devDependencies": {
"@babel/core": "^7.23.5",
"@darajs/eslint-config": "1.13.1",
"@darajs/prettier-config": "1.13.1",
"@darajs/stylelint-config": "1.13.1",
"@darajs/eslint-config": "1.14.0-alpha.3",
"@darajs/prettier-config": "1.14.0-alpha.3",
"@darajs/stylelint-config": "1.14.0-alpha.3",
"@testing-library/react-hooks": "^3.4.2",
"@types/lodash": "^4.14.155",
"@types/react": "18.2.60",
Expand All @@ -53,14 +53,14 @@
},
"dependencies": {
"@bokeh/bokehjs": "~3.1.1",
"@darajs/core": "1.13.1",
"@darajs/styled-components": "1.13.1",
"@darajs/ui-causal-graph-editor": "1.13.1",
"@darajs/ui-code-editor": "1.13.1",
"@darajs/ui-components": "1.13.1",
"@darajs/ui-hierarchy-viewer": "1.13.1",
"@darajs/ui-icons": "1.13.1",
"@darajs/ui-utils": "1.13.1",
"@darajs/core": "1.14.0-alpha.3",
"@darajs/styled-components": "1.14.0-alpha.3",
"@darajs/ui-causal-graph-editor": "1.14.0-alpha.3",
"@darajs/ui-code-editor": "1.14.0-alpha.3",
"@darajs/ui-components": "1.14.0-alpha.3",
"@darajs/ui-hierarchy-viewer": "1.14.0-alpha.3",
"@darajs/ui-icons": "1.14.0-alpha.3",
"@darajs/ui-utils": "1.14.0-alpha.3",
"@popperjs/core": "2.4.0",
"date-fns": "2.9.0",
"immer": "^10.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/dara-components/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ license = "Apache-2.0"
name = "dara-components"
readme = "README.md"
repository = "https://github.com/causalens/dara"
version = "1.13.1"
version = "1.14.0-alpha.3"
source = []

[[tool.poetry.packages]]
Expand All @@ -36,7 +36,7 @@ include = "dara"
bokeh = ">=3.1.0, <3.2.0"
cai-causal-graph = ">=0.3.6"
certifi = ">=2024.7.4"
dara-core = "1.13.1"
dara-core = "1.14.0-alpha.3"
dill = ">=0.3.0, <0.4.0"
matplotlib = ">=2.0.0"
pandas = ">=1.1.0, <3.0.0"
Expand Down
6 changes: 6 additions & 0 deletions packages/dara-core/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
title: Changelog
---

## NEXT

- Added support for seamless token refresh mechanism. A provided auth config can be configured to support token refresh by:
- implement the `refresh_token` method to sign a new token, reusing the previous session_id for continuity
- adding some mechanism to set a `dara_refresh_token` cookie, e.g. via custom `components_config` and endpoints such as `/sso-callback` for SSO auth

## 1.13.1

- Fixed an issue where data passed to `Table` component within a column of `dtype` `object` did not display correctly for datetime values.
Expand Down
14 changes: 13 additions & 1 deletion packages/dara-core/dara/core/auth/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import abc
from typing import Any, ClassVar, Dict, Union

from fastapi import Response
from fastapi import HTTPException, Response
from pydantic import BaseModel
from typing_extensions import TypedDict

Expand Down Expand Up @@ -91,6 +91,18 @@ def verify_token(self, token: str) -> Union[Any, TokenData]:
:param token: encoded token
"""

def refresh_token(self, old_token: TokenData, refresh_token: str) -> tuple[str, str]:
"""
Create a new session token and refresh token from a refresh token.

Note: the new issued session token should include the same session_id as the old token

:param old_token: old session token data
:param refresh_token: encoded refresh token
:return: new session token, new refresh token
"""
raise HTTPException(400, f'Auth config {self.__class__.__name__} does not support token refresh')

def revoke_token(self, token: str, response: Response) -> Union[SuccessResponse, RedirectResponse]:
"""
Revoke a session token.
Expand Down
77 changes: 76 additions & 1 deletion packages/dara-core/dara/core/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,18 @@
"""

from inspect import iscoroutinefunction
from typing import Union, cast

import jwt
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi import (
APIRouter,
BackgroundTasks,
Cookie,
Depends,
HTTPException,
Request,
Response,
)
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

from dara.core.auth.base import BaseAuthConfig
Expand All @@ -31,6 +40,7 @@
AuthError,
SessionRequestBody,
)
from dara.core.auth.utils import cached_refresh_token, decode_token
from dara.core.logging import dev_logger

auth_router = APIRouter()
Expand Down Expand Up @@ -103,6 +113,71 @@ async def _revoke_session(response: Response, credentials: HTTPAuthorizationCred
raise HTTPException(status_code=400, detail=BAD_REQUEST_ERROR('No auth credentials passed'))


@auth_router.post('/refresh-token')
async def handle_refresh_token(
response: Response,
background_tasks: BackgroundTasks,
dara_refresh_token: Union[str, None] = Cookie(default=None),
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer()),
):
"""
Given a refresh token, issues a new session token and refresh token cookie.

:param response: FastAPI response object
:param dara_refresh_token: refresh token cookie
:param settings: env settings object
"""
if dara_refresh_token is None:
raise HTTPException(status_code=400, detail=BAD_REQUEST_ERROR('No refresh token provided'))

# Check scheme is correct
if credentials.scheme != 'Bearer':
raise HTTPException(
status_code=400,
detail=BAD_REQUEST_ERROR(
'Invalid authentication scheme, previous Bearer token must be included in the refresh request'
),
)

from dara.core.internal.registries import auth_registry

auth_config: BaseAuthConfig = auth_registry.get('auth_config')

try:
# decode the old token ignoring expiry date
old_token_data = decode_token(credentials.credentials, options={'verify_exp': False})

# Refresh logic up to implementation - passing in old token data so session_id can be preserved
session_token, refresh_token = await cached_refresh_token(
auth_config.refresh_token, old_token_data, dara_refresh_token
)

# Using 'Strict' as it is only used for the refresh-token endpoint so cross-site requests are not expected
response.set_cookie(
key='dara_refresh_token', value=refresh_token, secure=True, httponly=True, samesite='strict'
)
return {'token': session_token}
except BaseException as e:
# Regardless of exception type, clear the refresh token cookie
response.delete_cookie('dara_refresh_token')
headers = {'set-cookie': response.headers['set-cookie']}

# If an explicit HTTPException was raised, re-raise it with the cookie header
if isinstance(e, HTTPException):
dev_logger.error('Auth Error', error=e)
e.headers = headers
raise e

# Explicitly handle expired signature error
if isinstance(e, jwt.ExpiredSignatureError):
dev_logger.error('Expired Token Signature', error=e)
raise HTTPException(status_code=401, detail=EXPIRED_TOKEN_ERROR, headers=headers)

# Otherwise show a generic invalid token error
dev_logger.error('Invalid Token', error=cast(Exception, e))
raise HTTPException(status_code=401, detail=INVALID_TOKEN_ERROR, headers=headers)


# Request to retrieve a session token from the backend. The app does this on startup.
@auth_router.post('/session')
async def _get_session(body: SessionRequestBody):
Expand Down
Loading
Loading