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 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 = str(uuid.uuid4())
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:
@@ -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-components')
212
- async def get_auth_components(): # pylint: disable=unused-variable
213
- return config.auth_config.component_config
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