Page MenuHomeSoftware Heritage

D3849.id13601.diff
No OneTemporary

D3849.id13601.diff

diff --git a/swh/web/auth/backends.py b/swh/web/auth/backends.py
--- a/swh/web/auth/backends.py
+++ b/swh/web/auth/backends.py
@@ -4,6 +4,7 @@
# See top-level LICENSE file for more information
from datetime import datetime, timedelta
+import hashlib
from typing import Any, Dict, Optional
from django.core.cache import cache
@@ -11,7 +12,7 @@
from django.utils import timezone
from rest_framework.authentication import BaseAuthentication
-from rest_framework.exceptions import AuthenticationFailed
+from rest_framework.exceptions import AuthenticationFailed, ValidationError
import sentry_sdk
@@ -130,7 +131,7 @@
return None
try:
- auth_type, token = auth_header.split(" ", 1)
+ auth_type, refresh_token = auth_header.split(" ", 1)
except ValueError:
raise AuthenticationFailed("Invalid HTTP authorization header format")
@@ -139,10 +140,39 @@
(f"Invalid or unsupported HTTP authorization" f" type ({auth_type}).")
)
try:
- # attempt to decode token
- decoded_token = _oidc_client.decode_token(token)
+
+ # compute a cache key from the token that does not exceed
+ # memcached key size limit
+ hasher = hashlib.sha1()
+ hasher.update(refresh_token.encode("ascii"))
+ cache_key = f"api_token_{hasher.hexdigest()}"
+
+ # check if an access token is cached
+ access_token = cache.get(cache_key)
+
+ # attempt to decode access token
+ try:
+ decoded_token = _oidc_client.decode_token(access_token)
+ except Exception:
+ # access token is None or it has expired
+ decoded_token = None
+
+ if access_token is None or decoded_token is None:
+ # get a new access token from authentication provider
+ access_token = _oidc_client.refresh_token(refresh_token)["access_token"]
+ # decode access token
+ decoded_token = _oidc_client.decode_token(access_token)
+ # compute access token expiration
+ exp = datetime.fromtimestamp(decoded_token["exp"])
+ ttl = int(exp.timestamp() - timezone.now().timestamp())
+ # save access token in cache while it is valid
+ cache.set(cache_key, access_token, timeout=max(0, ttl))
+
# create Django user
user = _oidc_user_from_decoded_token(decoded_token)
+ except UnicodeEncodeError as e:
+ sentry_sdk.capture_exception(e)
+ raise ValidationError("Invalid bearer token")
except Exception as e:
sentry_sdk.capture_exception(e)
raise AuthenticationFailed(str(e))
diff --git a/swh/web/tests/auth/keycloak_mock.py b/swh/web/tests/auth/keycloak_mock.py
--- a/swh/web/tests/auth/keycloak_mock.py
+++ b/swh/web/tests/auth/keycloak_mock.py
@@ -62,16 +62,19 @@
),
}
self.authorization_code = Mock()
+ self.refresh_token = Mock()
self.userinfo = Mock()
self.logout = Mock()
if auth_success:
self.authorization_code.return_value = copy(oidc_profile)
+ self.refresh_token.return_value = copy(oidc_profile)
self.userinfo.return_value = copy(userinfo)
else:
self.authorization_url = Mock()
exception = Exception("Authentication failed")
self.authorization_code.side_effect = exception
self.authorization_url.side_effect = exception
+ self.refresh_token.side_effect = exception
self.userinfo.side_effect = exception
self.logout.side_effect = exception
diff --git a/swh/web/tests/auth/test_api_auth.py b/swh/web/tests/auth/test_api_auth.py
--- a/swh/web/tests/auth/test_api_auth.py
+++ b/swh/web/tests/auth/test_api_auth.py
@@ -46,10 +46,10 @@
"""
url = reverse("api-1-stat-counters")
- access_token = sample_data.oidc_profile["access_token"]
+ refresh_token = sample_data.oidc_profile["refresh_token"]
mock_keycloak(mocker)
- api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {access_token}")
+ api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token}")
response = api_client.get(url)
request = response.wsgi_request
@@ -68,11 +68,11 @@
def test_drf_oidc_bearer_token_auth_failure(mocker, api_client):
url = reverse("api-1-stat-counters")
- access_token = sample_data.oidc_profile["access_token"]
+ refresh_token = sample_data.oidc_profile["refresh_token"]
# check for failed authentication but with expected token format
mock_keycloak(mocker, auth_success=False)
- api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {access_token}")
+ api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token}")
response = api_client.get(url)
request = response.wsgi_request
@@ -81,23 +81,22 @@
assert isinstance(request.user, AnonymousUser)
# check for failed authentication when token format is invalid
- mock_keycloak(mocker)
- api_client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-format")
+ api_client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-format-ééàà")
response = api_client.get(url)
request = response.wsgi_request
- assert response.status_code == 403
+ assert response.status_code == 400
assert isinstance(request.user, AnonymousUser)
def test_drf_oidc_auth_invalid_or_missing_authorization_type(api_client):
url = reverse("api-1-stat-counters")
- access_token = sample_data.oidc_profile["access_token"]
+ refresh_token = sample_data.oidc_profile["refresh_token"]
# missing authorization type
- api_client.credentials(HTTP_AUTHORIZATION=f"{access_token}")
+ api_client.credentials(HTTP_AUTHORIZATION=f"{refresh_token}")
response = api_client.get(url)
request = response.wsgi_request
diff --git a/swh/web/tests/auth/test_backends.py b/swh/web/tests/auth/test_backends.py
--- a/swh/web/tests/auth/test_backends.py
+++ b/swh/web/tests/auth/test_backends.py
@@ -121,10 +121,12 @@
kc_oidc_mock = mock_keycloak(mocker)
+ refresh_token = sample_data.oidc_profile["refresh_token"]
access_token = sample_data.oidc_profile["access_token"]
+
decoded_token = kc_oidc_mock.decode_token(access_token)
- request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"Bearer {access_token}")
+ request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"Bearer {refresh_token}")
user, _ = drf_auth_backend.authenticate(request)
_check_authenticated_user(user, decoded_token, kc_oidc_mock)
@@ -144,16 +146,14 @@
# simulate a failed authentication with a bearer token in expected format
mock_keycloak(mocker, auth_success=False)
- access_token = sample_data.oidc_profile["access_token"]
+ refresh_token = sample_data.oidc_profile["refresh_token"]
- request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"Bearer {access_token}")
+ request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"Bearer {refresh_token}")
with pytest.raises(AuthenticationFailed):
drf_auth_backend.authenticate(request)
# simulate a failed authentication with an invalid bearer token format
- mock_keycloak(mocker)
-
request = api_request_factory.get(
url, HTTP_AUTHORIZATION="Bearer invalid-token-format"
)
@@ -171,7 +171,7 @@
url = reverse("api-1-stat-counters")
drf_auth_backend = OIDCBearerTokenAuthentication()
- access_token = sample_data.oidc_profile["access_token"]
+ refresh_token = sample_data.oidc_profile["refresh_token"]
# Invalid authorization type
request = api_request_factory.get(url, HTTP_AUTHORIZATION="Foo token")
@@ -180,7 +180,7 @@
drf_auth_backend.authenticate(request)
# Missing authorization type
- request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"{access_token}")
+ request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"{refresh_token}")
with pytest.raises(AuthenticationFailed):
drf_auth_backend.authenticate(request)
@@ -196,9 +196,9 @@
mock_keycloak(mocker, user_permissions=[permission])
drf_auth_backend = OIDCBearerTokenAuthentication()
- access_token = sample_data.oidc_profile["access_token"]
+ refresh_token = sample_data.oidc_profile["refresh_token"]
url = reverse("api-1-stat-counters")
- request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"Bearer {access_token}")
+ request = api_request_factory.get(url, HTTP_AUTHORIZATION=f"Bearer {refresh_token}")
user, _ = drf_auth_backend.authenticate(request)
assert user.has_perm(permission)

File Metadata

Mime Type
text/plain
Expires
Wed, Sep 17, 4:55 PM (4 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3225670

Event Timeline