KEYCLOAK-16006 User should not be required to re-authenticate after revoking consent to an application
This commit is contained in:
parent
37cb1ba310
commit
0058011265
11 changed files with 128 additions and 26 deletions
|
@ -497,18 +497,13 @@ public class TokenManager {
|
|||
}
|
||||
|
||||
|
||||
public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, AuthenticatedClientSessionModel clientSession) {
|
||||
public static void dettachClientSession(AuthenticatedClientSessionModel clientSession) {
|
||||
UserSessionModel userSession = clientSession.getUserSession();
|
||||
if (userSession == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clientSession.detachFromUserSession();
|
||||
|
||||
// TODO: Might need optimization to prevent loading client sessions from cache in getAuthenticatedClientSessions()
|
||||
if (userSession.getAuthenticatedClientSessions().isEmpty()) {
|
||||
sessions.removeUserSession(realm, userSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -228,7 +228,17 @@ public class TokenRevocationEndpoint {
|
|||
.map(userSession -> userSession.getAuthenticatedClientSessionByClient(client.getId()))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList()) // collect to avoid concurrent modification as dettachClientSession removes the user sessions.
|
||||
.forEach(clientSession -> TokenManager.dettachClientSession(session.sessions(), realm, clientSession));
|
||||
.forEach(clientSession -> {
|
||||
UserSessionModel userSession = clientSession.getUserSession();
|
||||
TokenManager.dettachClientSession(clientSession);
|
||||
|
||||
if (userSession != null) {
|
||||
// TODO: Might need optimization to prevent loading client sessions from cache in getAuthenticatedClientSessions()
|
||||
if (userSession.getAuthenticatedClientSessions().isEmpty()) {
|
||||
session.sessions().removeUserSession(realm, userSession);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void revokeAccessToken() {
|
||||
|
|
|
@ -555,7 +555,7 @@ public class AuthenticationManager {
|
|||
.forEach(clientSession -> {
|
||||
backchannelLogoutClientSession(session, realm, clientSession, null, uriInfo, headers);
|
||||
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
|
||||
TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
|
||||
TokenManager.dettachClientSession(clientSession);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.services.managers;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UserConsentManager {
|
||||
|
||||
/**
|
||||
* Revoke consent of given user to given client
|
||||
*
|
||||
* @param session
|
||||
* @param client
|
||||
* @param user
|
||||
* @return true if either consent or offlineToken was revoked
|
||||
*/
|
||||
public static boolean revokeConsentToClient(KeycloakSession session, ClientModel client, UserModel user) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
boolean revokedConsent = session.users().revokeConsentForClient(realm, user.getId(), client.getId());
|
||||
boolean revokedOfflineToken = new UserSessionManager(session).revokeOfflineToken(user, client);
|
||||
|
||||
if (revokedConsent) {
|
||||
// Logout clientSessions for this user and client
|
||||
AuthenticationManager.backchannelLogoutUserFromClient(session, realm, user, client, session.getContext().getUri(), session.getContext().getRequestHeaders());
|
||||
}
|
||||
|
||||
return revokedConsent || revokedOfflineToken;
|
||||
}
|
||||
}
|
|
@ -65,6 +65,7 @@ import org.keycloak.services.managers.AppAuthManager;
|
|||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.AbstractSecuredLocalService;
|
||||
|
@ -465,11 +466,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
|
|||
|
||||
// Revoke grant in UserModel
|
||||
UserModel user = auth.getUser();
|
||||
session.users().revokeConsentForClient(realm, user.getId(), client.getId());
|
||||
new UserSessionManager(session).revokeOfflineToken(user, client);
|
||||
|
||||
// Logout clientSessions for this user and client
|
||||
AuthenticationManager.backchannelLogoutUserFromClient(session, realm, user, client, session.getContext().getUri(), headers);
|
||||
UserConsentManager.revokeConsentToClient(session, client, user);
|
||||
|
||||
event.event(EventType.REVOKE_GRANT).client(auth.getClient()).user(auth.getUser()).detail(Details.REVOKED_CLIENT, client.getClientId()).success();
|
||||
setReferrerOnPage();
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.keycloak.representations.account.ConsentScopeRepresentation;
|
|||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.account.resources.ResourcesService;
|
||||
|
@ -286,8 +287,7 @@ public class AccountRestService {
|
|||
return ErrorResponse.error(msg, Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
session.users().revokeConsentForClient(realm, user.getId(), client.getId());
|
||||
new UserSessionManager(session).revokeOfflineToken(user, client);
|
||||
UserConsentManager.revokeConsentToClient(session, client, user);
|
||||
event.success();
|
||||
|
||||
return Response.noContent().build();
|
||||
|
|
|
@ -65,6 +65,7 @@ import org.keycloak.services.ForbiddenException;
|
|||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.resources.account.AccountFormService;
|
||||
|
@ -477,15 +478,9 @@ public class UserResource {
|
|||
if (client == null) {
|
||||
throw new NotFoundException("Client not found");
|
||||
}
|
||||
boolean revokedConsent = session.users().revokeConsentForClient(realm, user.getId(), client.getId());
|
||||
boolean revokedOfflineToken = new UserSessionManager(session).revokeOfflineToken(user, client);
|
||||
boolean revokedConsent = UserConsentManager.revokeConsentToClient(session, client, user);
|
||||
|
||||
if (revokedConsent) {
|
||||
// Logout clientSessions for this user and client
|
||||
AuthenticationManager.backchannelLogoutUserFromClient(session, realm, user, client, session.getContext().getUri(), headers);
|
||||
}
|
||||
|
||||
if (!revokedConsent && !revokedOfflineToken) {
|
||||
if (!revokedConsent) {
|
||||
throw new NotFoundException("Consent nor offline token not found");
|
||||
}
|
||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
|
||||
|
|
|
@ -149,7 +149,7 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
|
|||
.client("account").detail(Details.REVOKED_CLIENT, "offline-client").assertEvent();
|
||||
|
||||
// Assert refresh doesn't work now (increase time one more time)
|
||||
setAdapterAndServerTimeOffset(9999);
|
||||
setAdapterAndServerTimeOffset(19999);
|
||||
offlineTokenPage.navigateTo();
|
||||
assertCurrentUrlDoesntStartWith(offlineTokenPage);
|
||||
loginPage.assertCurrent();
|
||||
|
|
|
@ -318,9 +318,10 @@ public class ConsentsTest extends AbstractKeycloakTest {
|
|||
Map<String, Object> consent = consents.get(0);
|
||||
Assert.assertEquals("Consent should be given to " + CLIENT_ID, CLIENT_ID, consent.get("clientId"));
|
||||
|
||||
// list sessions
|
||||
// list sessions. Single client should be in user session
|
||||
List<UserSessionRepresentation> sessions = userResource.getUserSessions();
|
||||
Assert.assertEquals("There should be one active session", 1, sessions.size());
|
||||
Assert.assertEquals("There should be one client in user session", 1, sessions.get(0).getClients().size());
|
||||
|
||||
// revoke consent
|
||||
userResource.revokeConsent(CLIENT_ID);
|
||||
|
@ -331,7 +332,8 @@ public class ConsentsTest extends AbstractKeycloakTest {
|
|||
|
||||
// list sessions
|
||||
sessions = userResource.getUserSessions();
|
||||
Assert.assertEquals("There should be no active session", 0, sessions.size());
|
||||
Assert.assertEquals("There should be one active session", 1, sessions.size());
|
||||
Assert.assertEquals("There should be no client in user session", 0, sessions.get(0).getClients().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -461,4 +461,48 @@ public class OAuthGrantTest extends AbstractKeycloakTest {
|
|||
Assert.assertEquals("Email address", displayedScopes.get(1));
|
||||
}
|
||||
|
||||
|
||||
// KEYCLOAK-16006 - tests that after revoke consent from single client, the SSO session is still valid and not automatically logged-out
|
||||
@Test
|
||||
public void oauthGrantUserNotLoggedOutAfterConsentRevoke() throws Exception {
|
||||
// Login
|
||||
oauth.clientId(THIRD_PARTY_APP);
|
||||
oauth.doLoginGrant("test-user@localhost", "password");
|
||||
|
||||
// Confirm consent screen
|
||||
grantPage.assertCurrent();
|
||||
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
|
||||
grantPage.accept();
|
||||
|
||||
Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE));
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin()
|
||||
.client(THIRD_PARTY_APP)
|
||||
.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
|
||||
.assertEvent();
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
// Revoke consent with admin REST API
|
||||
adminClient.realm(REALM_NAME).users().get(loginEvent.getUserId()).revokeConsent(THIRD_PARTY_APP);
|
||||
|
||||
// Make sure that after refresh, consent page is displayed and user doesn't need to re-authenticate. Just accept consent screen again
|
||||
oauth.openLoginForm();
|
||||
|
||||
grantPage.assertCurrent();
|
||||
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
|
||||
grantPage.accept();
|
||||
|
||||
loginEvent = events.expectLogin()
|
||||
.client(THIRD_PARTY_APP)
|
||||
.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
|
||||
.assertEvent();
|
||||
|
||||
//String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||
String sessionId2 = loginEvent.getSessionId();
|
||||
Assert.assertEquals(sessionId, sessionId2);
|
||||
|
||||
// Revert consent
|
||||
adminClient.realm(REALM_NAME).users().get(loginEvent.getUserId()).revokeConsent(THIRD_PARTY_APP);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -113,6 +113,14 @@ public class TokenRevocationTest extends AbstractKeycloakTest {
|
|||
|
||||
isTokenDisabled(tokenResponse1, "test-app");
|
||||
isTokenEnabled(tokenResponse2, "test-app-scope");
|
||||
|
||||
// Revoke second token and assert no sessions for testUser
|
||||
response = oauth.doTokenRevoke(tokenResponse2.getRefreshToken(), "refresh_token", "password");
|
||||
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
|
||||
|
||||
userSessions = testUser.getUserSessions();
|
||||
assertEquals(0, userSessions.size());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue