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.