dara-core 1.13.1__py3-none-any.whl → 1.14.0a1__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/auth/base.py +21 -1
- dara/core/auth/routes.py +65 -1
- dara/core/auth/utils.py +8 -3
- dara/core/internal/routing.py +6 -3
- dara/core/umd/dara.core.umd.js +631 -468
- {dara_core-1.13.1.dist-info → dara_core-1.14.0a1.dist-info}/METADATA +10 -10
- {dara_core-1.13.1.dist-info → dara_core-1.14.0a1.dist-info}/RECORD +10 -10
- {dara_core-1.13.1.dist-info → dara_core-1.14.0a1.dist-info}/LICENSE +0 -0
- {dara_core-1.13.1.dist-info → dara_core-1.14.0a1.dist-info}/WHEEL +0 -0
- {dara_core-1.13.1.dist-info → dara_core-1.14.0a1.dist-info}/entry_points.txt +0 -0
dara/core/auth/base.py
CHANGED
|
@@ -18,7 +18,7 @@ limitations under the License.
|
|
|
18
18
|
import abc
|
|
19
19
|
from typing import Any, ClassVar, Dict, Union
|
|
20
20
|
|
|
21
|
-
from fastapi import Response
|
|
21
|
+
from fastapi import HTTPException, Response
|
|
22
22
|
from pydantic import BaseModel
|
|
23
23
|
from typing_extensions import TypedDict
|
|
24
24
|
|
|
@@ -69,6 +69,14 @@ class BaseAuthConfig(BaseModel, abc.ABC):
|
|
|
69
69
|
Defines components to use for auth routes
|
|
70
70
|
"""
|
|
71
71
|
|
|
72
|
+
supports_token_refresh: ClassVar[bool] = False
|
|
73
|
+
"""
|
|
74
|
+
Whether this auth config supports token refresh.
|
|
75
|
+
|
|
76
|
+
If an auth config supports token refresh, it should override the refresh_token method
|
|
77
|
+
and set this to True.
|
|
78
|
+
"""
|
|
79
|
+
|
|
72
80
|
@abc.abstractmethod
|
|
73
81
|
def get_token(self, body: SessionRequestBody) -> Union[TokenResponse, RedirectResponse]:
|
|
74
82
|
"""
|
|
@@ -91,6 +99,18 @@ class BaseAuthConfig(BaseModel, abc.ABC):
|
|
|
91
99
|
:param token: encoded token
|
|
92
100
|
"""
|
|
93
101
|
|
|
102
|
+
def refresh_token(self, old_token: TokenData, refresh_token: str) -> tuple[str, str]:
|
|
103
|
+
"""
|
|
104
|
+
Create a new session token and refresh token from a refresh token.
|
|
105
|
+
|
|
106
|
+
Note: the new issued session token should include the same session_id as the old token
|
|
107
|
+
|
|
108
|
+
:param old_token: old session token data
|
|
109
|
+
:param refresh_token: encoded refresh token
|
|
110
|
+
:return: new session token, new refresh token
|
|
111
|
+
"""
|
|
112
|
+
raise HTTPException(400, f'Auth config {self.__class__.__name__} does not support token refresh')
|
|
113
|
+
|
|
94
114
|
def revoke_token(self, token: str, response: Response) -> Union[SuccessResponse, RedirectResponse]:
|
|
95
115
|
"""
|
|
96
116
|
Revoke a session token.
|
dara/core/auth/routes.py
CHANGED
|
@@ -16,9 +16,10 @@ limitations under the License.
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from inspect import iscoroutinefunction
|
|
19
|
+
from typing import Union, cast
|
|
19
20
|
|
|
20
21
|
import jwt
|
|
21
|
-
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
22
|
+
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response
|
|
22
23
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
23
24
|
|
|
24
25
|
from dara.core.auth.base import BaseAuthConfig
|
|
@@ -31,6 +32,7 @@ from dara.core.auth.definitions import (
|
|
|
31
32
|
AuthError,
|
|
32
33
|
SessionRequestBody,
|
|
33
34
|
)
|
|
35
|
+
from dara.core.auth.utils import decode_token
|
|
34
36
|
from dara.core.logging import dev_logger
|
|
35
37
|
|
|
36
38
|
auth_router = APIRouter()
|
|
@@ -103,6 +105,68 @@ async def _revoke_session(response: Response, credentials: HTTPAuthorizationCred
|
|
|
103
105
|
raise HTTPException(status_code=400, detail=BAD_REQUEST_ERROR('No auth credentials passed'))
|
|
104
106
|
|
|
105
107
|
|
|
108
|
+
@auth_router.post('/refresh-token')
|
|
109
|
+
async def handle_refresh_token(
|
|
110
|
+
response: Response,
|
|
111
|
+
dara_refresh_token: Union[str, None] = Cookie(default=None),
|
|
112
|
+
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer()),
|
|
113
|
+
):
|
|
114
|
+
"""
|
|
115
|
+
Given a refresh token, issues a new session token and refresh token cookie.
|
|
116
|
+
|
|
117
|
+
:param response: FastAPI response object
|
|
118
|
+
:param dara_refresh_token: refresh token cookie
|
|
119
|
+
:param settings: env settings object
|
|
120
|
+
"""
|
|
121
|
+
if dara_refresh_token is None:
|
|
122
|
+
raise HTTPException(status_code=400, detail=BAD_REQUEST_ERROR('No refresh token provided'))
|
|
123
|
+
|
|
124
|
+
# Check scheme is correct
|
|
125
|
+
if credentials.scheme != 'Bearer':
|
|
126
|
+
raise HTTPException(
|
|
127
|
+
status_code=400,
|
|
128
|
+
detail=BAD_REQUEST_ERROR(
|
|
129
|
+
'Invalid authentication scheme, previous Bearer token must be included in the refresh request'
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
from dara.core.internal.registries import auth_registry
|
|
134
|
+
|
|
135
|
+
auth_config: BaseAuthConfig = auth_registry.get('auth_config')
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
# decode the old token ignoring expiry date
|
|
139
|
+
old_token_data = decode_token(credentials.credentials, options={'verify_exp': False})
|
|
140
|
+
|
|
141
|
+
# Refresh logic up to implementation - passing in old token data so session_id can be preserved
|
|
142
|
+
session_token, refresh_token = auth_config.refresh_token(old_token_data, dara_refresh_token)
|
|
143
|
+
|
|
144
|
+
# Using 'Strict' as it is only used for the refresh-token endpoint so cross-site requests are not expected
|
|
145
|
+
response.set_cookie(
|
|
146
|
+
key='dara_refresh_token', value=refresh_token, secure=True, httponly=True, samesite='strict'
|
|
147
|
+
)
|
|
148
|
+
return {'token': session_token}
|
|
149
|
+
except BaseException as e:
|
|
150
|
+
# Regardless of exception type, clear the refresh token cookie
|
|
151
|
+
response.delete_cookie('dara_refresh_token')
|
|
152
|
+
headers = {'set-cookie': response.headers['set-cookie']}
|
|
153
|
+
|
|
154
|
+
# If an explicit HTTPException was raised, re-raise it with the cookie header
|
|
155
|
+
if isinstance(e, HTTPException):
|
|
156
|
+
dev_logger.error('Auth Error', error=e)
|
|
157
|
+
e.headers = headers
|
|
158
|
+
raise e
|
|
159
|
+
|
|
160
|
+
# Explicitly handle expired signature error
|
|
161
|
+
if isinstance(e, jwt.ExpiredSignatureError):
|
|
162
|
+
dev_logger.error('Expired Token Signature', error=e)
|
|
163
|
+
raise HTTPException(status_code=401, detail=EXPIRED_TOKEN_ERROR, headers=headers)
|
|
164
|
+
|
|
165
|
+
# Otherwise show a generic invalid token error
|
|
166
|
+
dev_logger.error('Invalid Token', error=cast(Exception, e))
|
|
167
|
+
raise HTTPException(status_code=401, detail=INVALID_TOKEN_ERROR, headers=headers)
|
|
168
|
+
|
|
169
|
+
|
|
106
170
|
# Request to retrieve a session token from the backend. The app does this on startup.
|
|
107
171
|
@auth_router.post('/session')
|
|
108
172
|
async def _get_session(body: SessionRequestBody):
|
dara/core/auth/utils.py
CHANGED
|
@@ -33,12 +33,15 @@ from dara.core.internal.settings import get_settings
|
|
|
33
33
|
from dara.core.logging import dev_logger
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def decode_token(token: str) -> TokenData:
|
|
36
|
+
def decode_token(token: str, **kwargs) -> TokenData:
|
|
37
37
|
"""
|
|
38
38
|
Decode a JWT token
|
|
39
|
+
|
|
40
|
+
:param token: the JWT token to decode
|
|
41
|
+
:param kwargs: additional arguments to pass to the jwt.decode function
|
|
39
42
|
"""
|
|
40
43
|
try:
|
|
41
|
-
return TokenData.parse_obj(jwt.decode(token, get_settings().jwt_secret, algorithms=[JWT_ALGO]))
|
|
44
|
+
return TokenData.parse_obj(jwt.decode(token, get_settings().jwt_secret, algorithms=[JWT_ALGO], **kwargs))
|
|
42
45
|
except jwt.ExpiredSignatureError:
|
|
43
46
|
raise AuthError(code=401, detail=EXPIRED_TOKEN_ERROR)
|
|
44
47
|
except jwt.DecodeError:
|
|
@@ -52,11 +55,13 @@ def sign_jwt(
|
|
|
52
55
|
groups: List[str],
|
|
53
56
|
id_token: Optional[str] = None,
|
|
54
57
|
exp: Optional[Union[datetime, int]] = None,
|
|
58
|
+
session_id: Optional[str] = None,
|
|
55
59
|
):
|
|
56
60
|
"""
|
|
57
61
|
Create a new Dara JWT token
|
|
58
62
|
"""
|
|
59
|
-
session_id
|
|
63
|
+
if session_id is None:
|
|
64
|
+
session_id = str(uuid.uuid4())
|
|
60
65
|
|
|
61
66
|
# Default expiry is 1 day unless specified
|
|
62
67
|
if exp is None:
|
dara/core/internal/routing.py
CHANGED
|
@@ -208,9 +208,12 @@ def create_router(config: Configuration):
|
|
|
208
208
|
'application_name': get_settings().project_name,
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
@core_api_router.get('/auth-
|
|
212
|
-
async def
|
|
213
|
-
return
|
|
211
|
+
@core_api_router.get('/auth-config')
|
|
212
|
+
async def get_auth_config(): # pylint: disable=unused-variable
|
|
213
|
+
return {
|
|
214
|
+
'auth_components': config.auth_config.component_config.dict(),
|
|
215
|
+
'supports_token_refresh': config.auth_config.supports_token_refresh,
|
|
216
|
+
}
|
|
214
217
|
|
|
215
218
|
@core_api_router.get('/components', dependencies=[Depends(verify_session)])
|
|
216
219
|
async def get_components(name: Optional[str] = None): # pylint: disable=unused-variable
|