NullPointer during OIDC logout client disabled (#13424)

closes #12624
This commit is contained in:
Marcelo Daniel Silva Sales 2022-08-08 12:34:09 +02:00 committed by GitHub
parent ec808d28bb
commit e44cea587f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 17 deletions

View file

@ -42,7 +42,7 @@ import org.keycloak.sessions.RootAuthenticationSessionModel;
public class LogoutSessionCodeChecks extends SessionCodeChecks { public class LogoutSessionCodeChecks extends SessionCodeChecks {
public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
String code, String clientId, String tabId) { String code, String clientId, String tabId) {
super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null); super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null);
} }
@ -67,4 +67,9 @@ public class LogoutSessionCodeChecks extends SessionCodeChecks {
} }
return true; return true;
} }
@Override
protected boolean checkClientDisabled(ClientModel client) {
return !client.isEnabled() && getClientCode() != null;
}
} }

View file

@ -18,6 +18,7 @@
package org.keycloak.services.resources; package org.keycloak.services.resources;
import java.net.URI; import java.net.URI;
import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
@ -151,7 +152,8 @@ public class SessionCodeChecks {
// object retrieve // object retrieve
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session); AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
AuthenticationSessionModel authSession = null; AuthenticationSessionModel authSession = null;
if (authSessionId != null) authSession = authSessionManager.getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId); if (authSessionId != null)
authSession = authSessionManager.getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId);
AuthenticationSessionModel authSessionCookie = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId); AuthenticationSessionModel authSessionCookie = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
if (authSession != null && authSessionCookie != null && !authSession.getParentSession().getId().equals(authSessionCookie.getParentSession().getId())) { if (authSession != null && authSessionCookie != null && !authSession.getParentSession().getId().equals(authSessionCookie.getParentSession().getId())) {
@ -222,9 +224,9 @@ public class SessionCodeChecks {
setClientToEvent(client); setClientToEvent(client);
session.getContext().setClient(client); session.getContext().setClient(client);
if (!client.isEnabled()) { if (checkClientDisabled(client)) {
event.error(Errors.CLIENT_DISABLED); event.error(Errors.CLIENT_DISABLED);
response = ErrorPage.error(session,authSession, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED); response = ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
clientCode.removeExpiredClientSession(); clientCode.removeExpiredClientSession();
return false; return false;
} }
@ -236,7 +238,7 @@ public class SessionCodeChecks {
String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
// Check if we transitted between flows (eg. clicking "register" on login screen) // Check if we transitted between flows (eg. clicking "register" on login screen)
if (execution==null && !flowPath.equals(lastFlow)) { if (execution == null && !flowPath.equals(lastFlow)) {
logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", flowPath, lastFlow); logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", flowPath, lastFlow);
// Don't allow moving to different flow if I am on requiredActions already // Don't allow moving to different flow if I am on requiredActions already
@ -378,7 +380,7 @@ public class SessionCodeChecks {
AuthenticationSessionModel authSession = null; AuthenticationSessionModel authSession = null;
Cookie cook = RestartLoginCookie.getRestartCookie(session); Cookie cook = RestartLoginCookie.getRestartCookie(session);
if(cook == null){ if (cook == null) {
event.error(Errors.COOKIE_NOT_FOUND); event.error(Errors.COOKIE_NOT_FOUND);
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.COOKIE_NOT_FOUND); return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.COOKIE_NOT_FOUND);
} }
@ -449,4 +451,8 @@ public class SessionCodeChecks {
protected EventBuilder getEvent() { protected EventBuilder getEvent() {
return event; return event;
} }
protected boolean checkClientDisabled(ClientModel client) {
return !client.isEnabled();
}
} }

View file

@ -142,4 +142,9 @@ public class ClientAttributeUpdater extends ServerResourceUpdater<ClientAttribut
rep.setDirectAccessGrantsEnabled(directAccessGranted); rep.setDirectAccessGrantsEnabled(directAccessGranted);
return this; return this;
} }
public ClientAttributeUpdater setEnabled(Boolean enabled){
rep.setEnabled(enabled);
return this;
}
} }

View file

@ -18,10 +18,12 @@
package org.keycloak.testsuite.oauth; package org.keycloak.testsuite.oauth;
import java.io.Closeable;
import java.util.Collections; import java.util.Collections;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import org.hamcrest.MatcherAssert;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -30,6 +32,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
@ -48,6 +51,7 @@ import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LogoutConfirmPage; import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.testsuite.util.ServerURLs;
@ -206,7 +210,35 @@ public class LegacyLogoutTest extends AbstractTestRealmKeycloakTest {
} }
} }
// Test logout with deprecated "redirect_uri" and without "id_token_hint" and client disabled after login
@Test
public void logoutWithLegacyRedirectUriAndWithoutIdTokenHintClientDisabled() throws Exception {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
try (Closeable testAppClient = ClientAttributeUpdater.forClient(adminClient, "test", oauth.getClientId())
.setEnabled(false).update()) {
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
MatcherAssert.assertThat(false, is(rep.isEnabled()));
String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
// Assert logout confirmation page. Session still exists. Assert default language on logout page (English)
logoutConfirmPage.assertCurrent();
MatcherAssert.assertThat(true, is(isSessionActive(sessionId)));
events.assertEmpty();
logoutConfirmPage.confirmLogout();
// Redirected back to the application with expected state
events.expectLogout(sessionId).removeDetail(Details.REDIRECT_URI).assertEvent();
MatcherAssert.assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(APP_REDIRECT_URI);
}
}
private OAuthClient.AccessTokenResponse loginUser() { private OAuthClient.AccessTokenResponse loginUser() {
oauth.doLogin("test-user@localhost", "password"); oauth.doLogin("test-user@localhost", "password");

View file

@ -93,7 +93,7 @@ import org.openqa.selenium.NoSuchElementException;
/** /**
* Test for OIDC RP-Initiated Logout - https://openid.net/specs/openid-connect-rpinitiated-1_0.html * Test for OIDC RP-Initiated Logout - https://openid.net/specs/openid-connect-rpinitiated-1_0.html
* * <p>
* This is handled on server-side by the LogoutEndpoint.logout method * This is handled on server-side by the LogoutEndpoint.logout method
* *
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -141,6 +141,7 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void logoutRedirect() { public void logoutRedirect() {
OAuthClient.AccessTokenResponse tokenResponse = loginUser(); OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState(); String sessionId = tokenResponse.getSessionState();
@ -292,7 +293,7 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
assertEquals("test-user@localhost", loginPage.getUsername()); assertEquals("test-user@localhost", loginPage.getUsername());
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
//log out //log out
appPage.openAccount(); appPage.openAccount();
accountManagementPage.signOut(); accountManagementPage.signOut();
@ -506,8 +507,7 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
try { try {
logoutConfirmPage.clickBackToApplicationLink(); logoutConfirmPage.clickBackToApplicationLink();
fail(); fail();
} } catch (NoSuchElementException ex) {
catch (NoSuchElementException ex) {
// expected // expected
} }
@ -562,8 +562,7 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
try { try {
errorPage.clickBackToApplication(); errorPage.clickBackToApplication();
fail(); fail();
} } catch (NoSuchElementException ex) {
catch (NoSuchElementException ex) {
// expected // expected
} }
} }
@ -760,8 +759,7 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
try { try {
logoutConfirmPage.clickBackToApplicationLink(); logoutConfirmPage.clickBackToApplicationLink();
fail(); fail();
} } catch (NoSuchElementException ex) {
catch (NoSuchElementException ex) {
// expected // expected
} }
@ -880,8 +878,7 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
try { try {
logoutConfirmPage.clickBackToApplicationLink(); logoutConfirmPage.clickBackToApplicationLink();
fail(); fail();
} } catch (NoSuchElementException ex) {
catch (NoSuchElementException ex) {
// expected // expected
} }
@ -944,7 +941,7 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
rep.setFrontchannelLogout(true); rep.setFrontchannelLogout(true);
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout"); rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout");
clients.get(rep.getId()).update(rep); clients.get(rep.getId()).update(rep);
try { try {
oauth.clientSessionState("client-session"); oauth.clientSessionState("client-session");
oauth.doLogin("test-user@localhost", "password"); oauth.doLogin("test-user@localhost", "password");
@ -1029,7 +1026,49 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
} }
} }
@Test
public void logoutWithIdTokenAndDisabledClientMustWork() throws Exception {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
try (Closeable accountClientUpdater = ClientAttributeUpdater.forClient(adminClient, "test", oauth.getClientId())
.setEnabled(false).update()) {
String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).clientId("test-app").build();
driver.navigate().to(logoutUrl);
MatcherAssert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
events.assertEmpty();
logoutConfirmPage.confirmLogout();
events.expectLogout(tokenResponse.getSessionState()).assertEvent();
MatcherAssert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
}
}
//Login and logout with account client disabled after login
@Test
public void testLogoutWhenAccountClientIsDisabled() throws IOException {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
try (Closeable accountClientUpdater = ClientAttributeUpdater.forClient(adminClient, "test", Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)
.setEnabled(false)
.update()) {
String logoutUrl = oauth.getLogoutUrl().build();
driver.navigate().to(logoutUrl);
events.assertEmpty();
logoutConfirmPage.assertCurrent();
logoutConfirmPage.confirmLogout();
MatcherAssert.assertThat(false, is(isSessionActive(sessionId)));
MatcherAssert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
}
}
// SUPPORT METHODS
private OAuthClient.AccessTokenResponse loginUser() { private OAuthClient.AccessTokenResponse loginUser() {
return loginUser(false); return loginUser(false);
} }