[KEYCLOAK-16676] - Client attributes should not be stored if null or empty
This commit is contained in:
parent
7780badb2a
commit
0f30b3118a
7 changed files with 132 additions and 4 deletions
|
@ -293,13 +293,26 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAttribute(String name, String value) {
|
public void setAttribute(String name, String value) {
|
||||||
|
boolean valueUndefined = value == null || "".equals(value.trim());
|
||||||
|
|
||||||
for (ClientAttributeEntity attr : entity.getAttributes()) {
|
for (ClientAttributeEntity attr : entity.getAttributes()) {
|
||||||
if (attr.getName().equals(name)) {
|
if (attr.getName().equals(name)) {
|
||||||
attr.setValue(value);
|
// clean up, so that attributes previously set with either a empty or null value are removed
|
||||||
|
// we should remove this in future versions so that new clients never store empty/null attributes
|
||||||
|
if (valueUndefined) {
|
||||||
|
removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
attr.setValue(value);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do not create attributes if empty or null
|
||||||
|
if (valueUndefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ClientAttributeEntity attr = new ClientAttributeEntity();
|
ClientAttributeEntity attr = new ClientAttributeEntity();
|
||||||
attr.setName(name);
|
attr.setName(name);
|
||||||
attr.setValue(value);
|
attr.setValue(value);
|
||||||
|
|
|
@ -245,6 +245,13 @@ public abstract class MapClientAdapter extends AbstractClientModel<MapClientEnti
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAttribute(String name, String value) {
|
public void setAttribute(String name, String value) {
|
||||||
|
boolean valueUndefined = value == null || "".equals(value.trim());
|
||||||
|
|
||||||
|
if (valueUndefined) {
|
||||||
|
removeAttribute(name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
entity.setAttribute(name, value);
|
entity.setAttribute(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@ package org.keycloak.testsuite.rest;
|
||||||
|
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.common.util.HtmlUtils;
|
import org.keycloak.common.util.HtmlUtils;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.representations.LogoutToken;
|
||||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||||
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||||
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
||||||
|
@ -55,6 +56,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
|
|
||||||
private final BlockingQueue<LogoutAction> adminLogoutActions;
|
private final BlockingQueue<LogoutAction> adminLogoutActions;
|
||||||
|
private final BlockingQueue<LogoutToken> backChannelLogoutTokens;
|
||||||
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
|
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
|
||||||
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
|
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
|
||||||
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
|
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
|
||||||
|
@ -63,10 +65,13 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
HttpRequest request;
|
HttpRequest request;
|
||||||
|
|
||||||
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
|
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
|
||||||
|
BlockingQueue<LogoutToken> backChannelLogoutTokens,
|
||||||
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
||||||
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction, TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) {
|
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
||||||
|
TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.adminLogoutActions = adminLogoutActions;
|
this.adminLogoutActions = adminLogoutActions;
|
||||||
|
this.backChannelLogoutTokens = backChannelLogoutTokens;
|
||||||
this.adminPushNotBeforeActions = adminPushNotBeforeActions;
|
this.adminPushNotBeforeActions = adminPushNotBeforeActions;
|
||||||
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
|
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
|
||||||
this.oidcClientData = oidcClientData;
|
this.oidcClientData = oidcClientData;
|
||||||
|
@ -79,6 +84,13 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
adminLogoutActions.add(new JWSInput(data).readJsonContent(LogoutAction.class));
|
adminLogoutActions.add(new JWSInput(data).readJsonContent(LogoutAction.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@Path("/admin/backchannelLogout")
|
||||||
|
public void backchannelLogout() throws JWSInputException {
|
||||||
|
backChannelLogoutTokens.add(new JWSInput(request.getDecodedFormParameters().getFirst(OAuth2Constants.LOGOUT_TOKEN)).readJsonContent(LogoutToken.class));
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.TEXT_PLAIN_UTF_8)
|
@Consumes(MediaType.TEXT_PLAIN_UTF_8)
|
||||||
@Path("/admin/k_push_not_before")
|
@Path("/admin/k_push_not_before")
|
||||||
|
@ -100,6 +112,13 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
return adminLogoutActions.poll(10, TimeUnit.SECONDS);
|
return adminLogoutActions.poll(10, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/poll-backchannel-logout")
|
||||||
|
public LogoutToken getBackChannelLogoutAction() throws InterruptedException {
|
||||||
|
return backChannelLogoutTokens.poll(20, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/poll-admin-not-before")
|
@Path("/poll-admin-not-before")
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.crypto.KeyType;
|
||||||
import org.keycloak.crypto.KeyUse;
|
import org.keycloak.crypto.KeyUse;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.representations.LogoutToken;
|
||||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||||
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||||
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
||||||
|
@ -41,6 +42,7 @@ import java.util.concurrent.LinkedBlockingDeque;
|
||||||
public class TestApplicationResourceProviderFactory implements RealmResourceProviderFactory {
|
public class TestApplicationResourceProviderFactory implements RealmResourceProviderFactory {
|
||||||
|
|
||||||
private BlockingQueue<LogoutAction> adminLogoutActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<LogoutAction> adminLogoutActions = new LinkedBlockingDeque<>();
|
||||||
|
private BlockingQueue<LogoutToken> backChannelLogoutTokens = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
||||||
|
|
||||||
|
@ -48,7 +50,8 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RealmResourceProvider create(KeycloakSession session) {
|
public RealmResourceProvider create(KeycloakSession session) {
|
||||||
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions, oidcClientData);
|
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions,
|
||||||
|
backChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData);
|
||||||
|
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.client.resources;
|
package org.keycloak.testsuite.client.resources;
|
||||||
|
|
||||||
|
import org.keycloak.representations.LogoutToken;
|
||||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||||
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||||
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
||||||
|
@ -39,6 +40,11 @@ public interface TestApplicationResource {
|
||||||
@Path("/poll-admin-logout")
|
@Path("/poll-admin-logout")
|
||||||
LogoutAction getAdminLogoutAction();
|
LogoutAction getAdminLogoutAction();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/poll-backchannel-logout")
|
||||||
|
LogoutToken getBackChannelLogoutToken();
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/poll-admin-not-before")
|
@Path("/poll-admin-not-before")
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.keycloak.events.admin.ResourceType;
|
||||||
import org.keycloak.models.AccountRoles;
|
import org.keycloak.models.AccountRoles;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||||
import org.keycloak.representations.adapters.action.GlobalRequestResult;
|
import org.keycloak.representations.adapters.action.GlobalRequestResult;
|
||||||
|
@ -385,6 +386,13 @@ public class ClientTest extends AbstractAdminTest {
|
||||||
|
|
||||||
storedClient = realm.clients().get(client.getId()).toRepresentation();
|
storedClient = realm.clients().get(client.getId()).toRepresentation();
|
||||||
assertClient(client, storedClient);
|
assertClient(client, storedClient);
|
||||||
|
|
||||||
|
storedClient.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, "");
|
||||||
|
|
||||||
|
realm.clients().get(storedClient.getId()).update(storedClient);
|
||||||
|
storedClient = realm.clients().get(client.getId()).toRepresentation();
|
||||||
|
|
||||||
|
assertFalse(storedClient.getAttributes().containsKey(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -23,11 +23,15 @@ 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.ClientsResource;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.jose.jws.JWSHeader;
|
import org.keycloak.jose.jws.JWSHeader;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
|
@ -328,6 +332,74 @@ public class LogoutTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successfulKLogoutAfterEmptyBackChannelUrl() throws Exception {
|
||||||
|
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
|
||||||
|
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
|
||||||
|
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, "");
|
||||||
|
|
||||||
|
clients.get(rep.getId()).update(rep);
|
||||||
|
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
oauth.clientSessionState("client-session");
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
String idTokenString = tokenResponse.getIdToken();
|
||||||
|
String logoutUrl = oauth.getLogoutUrl()
|
||||||
|
.idTokenHint(idTokenString)
|
||||||
|
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
|
||||||
|
CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
|
||||||
|
assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
|
||||||
|
assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(oauth.APP_AUTH_ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(testingClient.testApp().getAdminLogoutAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void backChannelPreferenceOverKLogout() throws Exception {
|
||||||
|
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
|
||||||
|
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
|
||||||
|
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, oauth.APP_ROOT + "/admin/backchannelLogout");
|
||||||
|
|
||||||
|
ClientResource clientResource = clients.get(rep.getId());
|
||||||
|
clientResource.update(rep);
|
||||||
|
|
||||||
|
try {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
oauth.clientSessionState("client-session");
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
String idTokenString = tokenResponse.getIdToken();
|
||||||
|
String logoutUrl = oauth.getLogoutUrl()
|
||||||
|
.idTokenHint(idTokenString)
|
||||||
|
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
|
||||||
|
CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
|
||||||
|
assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
|
||||||
|
assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(oauth.APP_AUTH_ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(testingClient.testApp().getBackChannelLogoutToken());
|
||||||
|
} finally {
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, "");
|
||||||
|
clientResource.update(rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
|
private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
|
||||||
oauth.doLogin("test-user@localhost", "password");
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue