diff --git a/assets/src/bundles/auth/index.js b/assets/src/bundles/auth/index.js --- a/assets/src/bundles/auth/index.js +++ b/assets/src/bundles/auth/index.js @@ -49,8 +49,14 @@ <pre id="swh-bearer-token" class="mt-3">${token}</pre>`; swh.webapp.showModalHtml('Display bearer token', tokenHtml); }) - .catch(() => { - swh.webapp.showModalHtml('Display bearer token', errorMessage('Internal server error.')); + .catch(response => { + response.text().then(responseText => { + let errorMsg = 'Internal server error.'; + if (response.status === 400) { + errorMsg = responseText; + } + swh.webapp.showModalHtml('Display bearer token', errorMessage(errorMsg)); + }); }); } diff --git a/cypress/integration/api-tokens.spec.js b/cypress/integration/api-tokens.spec.js --- a/cypress/integration/api-tokens.spec.js +++ b/cypress/integration/api-tokens.spec.js @@ -59,9 +59,9 @@ .should('contain', 'You are not allowed to generate bearer tokens'); }); - function displayToken(Urls, status, tokenValue = '') { + function displayToken(Urls, status, body = '') { cy.intercept('POST', `${Urls.oidc_get_bearer_token()}/**`, { - body: tokenValue, + body: body, statusCode: status }).as('getTokenRequest'); @@ -83,11 +83,20 @@ .should('contain', tokenValue); }); - it('should report errors when token display failed', function() { + it('should report error when token display failed', function() { initTokensPage(this.Urls, [{id: 1, creation_date: new Date().toISOString()}]); - displayToken(this.Urls, 500); + const errorMessage = 'Internal server error'; + displayToken(this.Urls, 500, errorMessage); cy.get('.modal-body') - .should('contain', 'Internal server error'); + .should('contain', errorMessage); + }); + + it('should report error when token expired', function() { + initTokensPage(this.Urls, [{id: 1, creation_date: new Date().toISOString()}]); + const errorMessage = 'Bearer token has expired'; + displayToken(this.Urls, 400, errorMessage); + cy.get('.modal-body') + .should('contain', errorMessage); }); function revokeToken(Urls, status) { diff --git a/swh/web/auth/views.py b/swh/web/auth/views.py --- a/swh/web/auth/views.py +++ b/swh/web/auth/views.py @@ -14,6 +14,7 @@ from django.http import HttpRequest from django.http.response import ( HttpResponse, + HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect, JsonResponse, @@ -25,6 +26,7 @@ from swh.auth.django.utils import keycloak_oidc_client from swh.auth.django.views import get_oidc_login_data, oidc_login_view from swh.auth.django.views import urlpatterns as auth_urlpatterns +from swh.auth.keycloak import KeycloakError, keycloak_error_message from swh.web.auth.models import OIDCUserOfflineTokens from swh.web.auth.utils import decrypt_data, encrypt_data from swh.web.common.exc import ForbiddenExc @@ -111,9 +113,21 @@ decrypted_token = decrypt_data( _encrypted_token_bytes(token_data.offline_token), secret, salt ) - return HttpResponse(decrypted_token.decode("ascii"), content_type="text/plain") + refresh_token = decrypted_token.decode("ascii") + # check token is still valid + oidc_client = keycloak_oidc_client() + oidc_client.refresh_token(refresh_token) + return HttpResponse(refresh_token, content_type="text/plain") except InvalidToken: return HttpResponse(status=401) + except KeycloakError as ke: + error_msg = keycloak_error_message(ke) + if error_msg in ( + "invalid_grant: Offline session not active", + "invalid_grant: Offline user session not found", + ): + error_msg = "Bearer token has expired, please generate a new one." + return HttpResponseBadRequest(error_msg, content_type="text/plain") @require_http_methods(["POST"]) diff --git a/swh/web/templates/auth/profile.html b/swh/web/templates/auth/profile.html --- a/swh/web/templates/auth/profile.html +++ b/swh/web/templates/auth/profile.html @@ -77,6 +77,9 @@ For instance when using <code>curl</code> proceed as follows: <pre>curl -H "Authorization: Bearer ${TOKEN}" {{ site_base_url }}api/...</pre> </p> + <p> + Please not that a bearer token will automatically expire after 30 days of inactivity. + </p> <div class="mt-3"> <div class="float-right"> <button class="btn btn-default" onclick="swh.auth.applyTokenAction('generate')"> diff --git a/swh/web/tests/auth/test_views.py b/swh/web/tests/auth/test_views.py --- a/swh/web/tests/auth/test_views.py +++ b/swh/web/tests/auth/test_views.py @@ -11,6 +11,7 @@ from django.http import QueryDict +from swh.auth.keycloak import KeycloakError from swh.web.auth.models import OIDCUserOfflineTokens from swh.web.auth.utils import OIDC_SWH_WEB_CLIENT_ID, decrypt_data from swh.web.common.utils import reverse @@ -203,6 +204,39 @@ assert response.content == token +@pytest.mark.django_db +def test_oidc_get_bearer_token_expired_token(client, keycloak_oidc): + """ + User with correct credentials should be allowed to display a token. + """ + + _generate_and_test_bearer_token(client, keycloak_oidc) + + for kc_err_msg in ("Offline session not active", "Offline user session not found"): + + kc_error_dict = { + "error": "invalid_grant", + "error_description": kc_err_msg, + } + + keycloak_oidc.refresh_token.side_effect = KeycloakError( + error_message=json.dumps(kc_error_dict).encode(), response_code=400 + ) + + url = reverse("oidc-get-bearer-token") + + response = check_http_post_response( + client, + url, + status_code=400, + data={"token_id": 1}, + content_type="text/plain", + ) + assert ( + response.content == b"Bearer token has expired, please generate a new one." + ) + + def test_oidc_revoke_bearer_tokens_anonymous_user(client): """ Anonymous user should be refused access with forbidden response.