|
3 | 3 | import secrets |
4 | 4 | import uuid as uuid_module |
5 | 5 | import warnings |
6 | | -from datetime import datetime, timedelta |
| 6 | +from datetime import datetime, timedelta, timezone |
7 | 7 | from typing import Any, Optional |
8 | 8 |
|
9 | 9 | from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, Response, Security, WebSocket |
|
19 | 19 | # int_from_bytes is deprecated, use int.from_bytes instead |
20 | 20 | with warnings.catch_warnings(): |
21 | 21 | warnings.simplefilter("ignore") |
22 | | - from jose import ExpiredSignatureError, JWTError, jwt |
| 22 | + from jose import ExpiredSignatureError |
23 | 23 |
|
24 | 24 | import pydantic |
25 | 25 | from packaging import version |
|
31 | 31 | from pydantic_settings import BaseSettings |
32 | 32 |
|
33 | 33 | from . import schemas |
| 34 | +from bluesky_authentication import tokens as auth_tokens |
34 | 35 | from .authorization._defaults import _DEFAULT_ANONYMOUS_PROVIDER_NAME |
35 | 36 | from .core import json_or_msgpack |
36 | 37 | from .database import orm |
|
54 | 55 | get_current_username, |
55 | 56 | ) |
56 | 57 |
|
57 | | -ALGORITHM = "HS256" |
58 | 58 | UNIT_SECOND = timedelta(seconds=1) |
59 | 59 |
|
60 | 60 | # Device code flow constants |
|
64 | 64 |
|
65 | 65 | def utcnow(): |
66 | 66 | "UTC now with second resolution" |
67 | | - return datetime.utcnow().replace(microsecond=0) |
| 67 | + return datetime.now(timezone.utc).replace(microsecond=0) |
68 | 68 |
|
69 | 69 |
|
70 | 70 | class Token(BaseModel): |
@@ -123,53 +123,38 @@ async def __call__(self, request: Request) -> Optional[str]: |
123 | 123 |
|
124 | 124 |
|
125 | 125 | def create_access_token(data, secret_key, expires_delta): |
126 | | - to_encode = data.copy() |
127 | | - expire = utcnow() + expires_delta |
128 | | - to_encode.update({"exp": expire, "type": "access"}) |
129 | | - encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=ALGORITHM) |
130 | | - return encoded_jwt |
| 126 | + return auth_tokens.create_access_token( |
| 127 | + data, |
| 128 | + secret_key, |
| 129 | + expires_delta, |
| 130 | + utcnow=utcnow, |
| 131 | + ) |
131 | 132 |
|
132 | 133 |
|
133 | 134 | def create_refresh_token(session_id, secret_key, expires_delta): |
134 | | - expire = utcnow() + expires_delta |
135 | | - to_encode = { |
136 | | - "type": "refresh", |
137 | | - "sid": session_id, |
138 | | - "exp": expire, |
139 | | - } |
140 | | - encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=ALGORITHM) |
141 | | - return encoded_jwt |
142 | | - |
143 | | - |
144 | | -def _decode_token_with_secret_keys(token, secret_keys): |
145 | | - # The first key in settings.secret_keys is used for *encoding*. |
146 | | - # All keys are tried for *decoding* until one works or they all |
147 | | - # fail. They support key rotation. |
148 | | - for secret_key in secret_keys: |
149 | | - try: |
150 | | - payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM]) |
151 | | - return payload |
152 | | - except ExpiredSignatureError: |
153 | | - # Do not let this be caught below with the other JWTError types. |
154 | | - raise |
155 | | - except JWTError: |
156 | | - # Try the next key in the key rotation. |
157 | | - continue |
158 | | - return None |
| 135 | + return auth_tokens.create_refresh_token( |
| 136 | + session_id, |
| 137 | + secret_key, |
| 138 | + expires_delta, |
| 139 | + utcnow=utcnow, |
| 140 | + ) |
159 | 141 |
|
160 | 142 |
|
161 | | -def decode_token(token, secret_keys, proxied_authenticator=None): |
| 143 | +async def decode_token(token, secret_keys, proxied_authenticator=None): |
162 | 144 | credentials_exception = HTTPException( |
163 | 145 | status_code=401, |
164 | 146 | detail="Could not validate credentials", |
165 | 147 | headers={"WWW-Authenticate": "Bearer"}, |
166 | 148 | ) |
167 | | - payload = _decode_token_with_secret_keys(token, secret_keys) |
168 | | - if payload is not None: |
169 | | - return payload |
170 | | - if proxied_authenticator is not None: |
171 | | - return proxied_authenticator.decode_token(token) |
172 | | - raise credentials_exception |
| 149 | + proxied_decoder = ( |
| 150 | + proxied_authenticator.decode_token if proxied_authenticator is not None else None |
| 151 | + ) |
| 152 | + return await auth_tokens.decode_token( |
| 153 | + token, |
| 154 | + secret_keys, |
| 155 | + proxied_decoder=proxied_decoder, |
| 156 | + credentials_exception=credentials_exception, |
| 157 | + ) |
173 | 158 |
|
174 | 159 |
|
175 | 160 | def _extract_scopes(decoded_access_token: dict[str, Any]) -> set[str]: |
@@ -201,7 +186,7 @@ async def get_api_key( |
201 | 186 | return None |
202 | 187 |
|
203 | 188 |
|
204 | | -def get_current_principal( |
| 189 | +async def get_current_principal( |
205 | 190 | request: Request, |
206 | 191 | security_scopes: SecurityScopes, |
207 | 192 | access_token: str = Depends(oauth2_scheme), |
@@ -306,7 +291,7 @@ def get_current_principal( |
306 | 291 | request.state.cookies_to_set.append({"key": API_KEY_COOKIE_NAME, "value": api_key}) |
307 | 292 | elif access_token is not None: |
308 | 293 | try: |
309 | | - payload = decode_token( |
| 294 | + payload = await decode_token( |
310 | 295 | access_token, |
311 | 296 | settings.secret_keys, |
312 | 297 | _get_proxied_authenticator(authenticators), |
@@ -1188,9 +1173,9 @@ def revoke_session( |
1188 | 1173 | return JSONResponse(status_code=200, content={"success": True, "msg": ""}) |
1189 | 1174 |
|
1190 | 1175 |
|
1191 | | -def slide_session(refresh_token, settings, db, api_access_manager): |
| 1176 | +async def slide_session(refresh_token, settings, db, api_access_manager): |
1192 | 1177 | try: |
1193 | | - payload = decode_token(refresh_token, settings.secret_keys) |
| 1178 | + payload = await decode_token(refresh_token, settings.secret_keys) |
1194 | 1179 | except ExpiredSignatureError: |
1195 | 1180 | raise HTTPException(status_code=401, detail="Session has expired. Please re-authenticate.") |
1196 | 1181 | # Find this session in the database. |
|
0 commit comments