Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F11023642
D3849.id13601.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
8 KB
Subscribers
None
D3849.id13601.diff
View Options
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
Details
Attached
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
Attached To
D3849: auth/backends: Use offline refresh token for Web API authentication
Event Timeline
Log In to Comment