diff --git a/swh/auth/keycloak.py b/swh/auth/keycloak.py
index 53e8601ee02fe750cb34d7bb4110810d6b3dabb9..72d243ce2535e8591825710ab9d431c33c5358cc 100644
--- a/swh/auth/keycloak.py
+++ b/swh/auth/keycloak.py
@@ -83,6 +83,26 @@ class KeycloakOpenIDConnect:
             **extra_params,
         )
 
+    def login(
+        self, username: str, password: str, **extra_params: str
+    ) -> Dict[str, Any]:
+        """
+        Get OpenID Connect authentication tokens using Direct Access Grant flow.
+
+        Args:
+            username: an existing username in the realm
+            password: password associated to username
+            extra_params: Extra parameters to add in the authorization request
+                payload.
+        """
+        return self._keycloak.token(
+            grant_type="password",
+            scope="openid",
+            username=username,
+            password=password,
+            **extra_params,
+        )
+
     def refresh_token(self, refresh_token: str) -> Dict[str, Any]:
         """
         Request a new access token from Keycloak using a refresh token.
diff --git a/swh/auth/pytest_plugin.py b/swh/auth/pytest_plugin.py
index f585fdfc290fa4fff8a40d794778b79671b7380d..f4dc4950780e7670e789109e13481db522d549a4 100644
--- a/swh/auth/pytest_plugin.py
+++ b/swh/auth/pytest_plugin.py
@@ -124,12 +124,14 @@ class KeycloackOpenIDConnectMock(KeycloakOpenIDConnect):
         # method "Cannot assign to a method affecting mock". Ignore for now.
         self.authorization_code = Mock()  # type: ignore
         self.refresh_token = Mock()  # type: ignore
+        self.login = Mock()  # type: ignore
         self.userinfo = Mock()  # type: ignore
         self.logout = Mock()  # type: ignore
         self.auth_success = auth_success
         if auth_success:
             self.authorization_code.return_value = copy(oidc_profile)
             self.refresh_token.return_value = copy(oidc_profile)
+            self.login.return_value = copy(oidc_profile)
             self.userinfo.return_value = copy(user_info)
         else:
             self.authorization_url = Mock()  # type: ignore
diff --git a/swh/auth/tests/test_keycloak.py b/swh/auth/tests/test_keycloak.py
index 15d5caf2802959f2bbaec1fb0ceeed7f796184e5..5cf774e2433c9665cd6c7e68750749d217c37fc6 100644
--- a/swh/auth/tests/test_keycloak.py
+++ b/swh/auth/tests/test_keycloak.py
@@ -93,3 +93,8 @@ def test_keycloak_decode_token(keycloak_mock):
         expected_decoded_token.pop(dynamic_valued_key)
 
     assert actual_decoded_data2 == expected_decoded_token
+
+
+def test_keycloak_login(keycloak_mock):
+    actual_response = keycloak_mock.login("username", "password")
+    assert actual_response == OIDC_PROFILE