Skip to content

Commit

Permalink
KEYCLOAK-12190 Add validation for client root and base URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
stianst committed Feb 7, 2020
1 parent 27f6f7b commit 7545749
Show file tree
Hide file tree
Showing 19 changed files with 506 additions and 8 deletions.
Expand Up @@ -58,6 +58,7 @@ public interface Errors {
String INVALID_FORM = "invalid_form";
String INVALID_CONFIG = "invalid_config";
String EXPIRED_CODE = "expired_code";
String INVALID_INPUT = "invalid_input";

String REGISTRATION_DISABLED = "registration_disabled";
String RESET_CREDENTIAL_DISABLED = "reset_credential_disabled";
Expand Down
Expand Up @@ -133,6 +133,9 @@
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.util.JsonSerialization;
import org.keycloak.validation.ClientValidationContext;
import org.keycloak.validation.ClientValidationProvider;
import org.keycloak.validation.ClientValidationUtil;

public class RepresentationToModel {

Expand Down Expand Up @@ -1235,6 +1238,10 @@ private static Map<String, ClientModel> createClients(KeycloakSession session, R
for (ClientRepresentation resourceRep : rep.getClients()) {
ClientModel app = createClient(session, realm, resourceRep, false, mappedFlows);
appMap.put(app.getClientId(), app);

ClientValidationUtil.validate(session, app, false, c -> {
throw new RuntimeException("Invalid client " + app.getClientId() + ": " + c.getError());
});
}
return appMap;
}
Expand Down
@@ -0,0 +1,39 @@
/*
* Copyright 2019 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.validation;

import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;

public interface ClientValidationContext {

enum Event {
CREATE,
UPDATE
}

Event getEvent();

KeycloakSession getSession();

ClientModel getClient();

String getError();

ClientValidationContext invalid(String error);

}
@@ -0,0 +1,29 @@
/*
* Copyright 2019 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.validation;

import org.keycloak.provider.Provider;

public interface ClientValidationProvider extends Provider {

void validate(ClientValidationContext context);

@Override
default void close() {
}

}
@@ -0,0 +1,37 @@
/*
* Copyright 2019 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.validation;

import org.keycloak.Config;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;

public interface ClientValidationProviderFactory extends ProviderFactory<ClientValidationProvider> {

@Override
default void init(Config.Scope config) {
}

@Override
default void postInit(KeycloakSessionFactory factory) {
}

@Override
default void close() {
}

}
@@ -0,0 +1,45 @@
/*
* Copyright 2019 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.validation;

import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;

public class ClientValidationSPI implements Spi {

@Override
public boolean isInternal() {
return true;
}

@Override
public String getName() {
return "clientValidation";
}

@Override
public Class<? extends Provider> getProviderClass() {
return ClientValidationProvider.class;
}

@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ClientValidationProviderFactory.class;
}

}
@@ -0,0 +1,88 @@
/*
* Copyright 2019 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.validation;

import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;

import javax.ws.rs.BadRequestException;

public class ClientValidationUtil {

public static void validate(KeycloakSession session, ClientModel client, boolean create, ErrorHandler errorHandler) throws BadRequestException {
ClientValidationProvider provider = session.getProvider(ClientValidationProvider.class);
if (provider != null) {
DefaultClientValidationContext context = new DefaultClientValidationContext(create ? ClientValidationContext.Event.CREATE : ClientValidationContext.Event.UPDATE, session, client);
provider.validate(context);

if (!context.isValid()) {
errorHandler.onError(context);
}
}
}

public interface ErrorHandler {

void onError(ClientValidationContext context);

}

private static class DefaultClientValidationContext implements ClientValidationContext {

private Event event;
private KeycloakSession session;
private ClientModel client;

private String error;

public DefaultClientValidationContext(Event event, KeycloakSession session, ClientModel client) {
this.event = event;
this.session = session;
this.client = client;
}

public boolean isValid() {
return error == null;
}

public String getError() {
return error;
}

@Override
public Event getEvent() {
return event;
}

@Override
public KeycloakSession getSession() {
return session;
}

@Override
public ClientModel getClient() {
return client;
}

@Override
public ClientValidationContext invalid(String error) {
this.error = error;
return this;
}
}

}
Expand Up @@ -77,4 +77,5 @@ org.keycloak.crypto.ClientSignatureVerifierSpi
org.keycloak.crypto.HashSpi
org.keycloak.vault.VaultSpi
org.keycloak.crypto.CekManagementSpi
org.keycloak.crypto.ContentEncryptionSpi
org.keycloak.crypto.ContentEncryptionSpi
org.keycloak.validation.ClientValidationSPI
Expand Up @@ -30,6 +30,7 @@
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.validation.ValidationMessages;
import org.keycloak.validation.ClientValidationUtil;

import javax.ws.rs.core.Response;

Expand Down Expand Up @@ -81,6 +82,11 @@ public ClientRepresentation create(ClientRegistrationContext context) {

client.setSecret(clientModel.getSecret());

ClientValidationUtil.validate(session, clientModel, true, c -> {
session.getTransactionManager().setRollbackOnly();
throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, c.getError(), Response.Status.BAD_REQUEST);
});

String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel, registrationAuth);
client.setRegistrationAccessToken(registrationAccessToken);

Expand Down Expand Up @@ -140,6 +146,11 @@ public ClientRepresentation update(String clientId, ClientRegistrationContext co
RepresentationToModel.updateClient(rep, client);
RepresentationToModel.updateClientProtocolMappers(rep, client);

ClientValidationUtil.validate(session, client, false, c -> {
session.getTransactionManager().setRollbackOnly();
throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, c.getError(), Response.Status.BAD_REQUEST);
});

rep = ModelToRepresentation.toRepresentation(client, session);

if (auth.isRegistrationAccessToken()) {
Expand Down
Expand Up @@ -24,6 +24,7 @@
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.events.Errors;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.AuthenticatedClientSessionModel;
Expand Down Expand Up @@ -63,6 +64,7 @@
import org.keycloak.services.validation.PairwiseClientValidator;
import org.keycloak.services.validation.ValidationMessages;
import org.keycloak.utils.ProfileHelper;
import org.keycloak.validation.ClientValidationUtil;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
Expand Down Expand Up @@ -151,10 +153,16 @@ public Response update(final ClientRepresentation rep) {

try {
updateClientFromRep(rep, client, session);

ClientValidationUtil.validate(session, client, false, c -> {
session.getTransactionManager().setRollbackOnly();
throw new ErrorResponseException(Errors.INVALID_INPUT ,c.getError(), Response.Status.BAD_REQUEST);
});

adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success();
return Response.noContent().build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
return ErrorResponse.exists("Client already exists");
}
}

Expand Down
Expand Up @@ -20,6 +20,7 @@
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authorization.admin.AuthorizationService;
import org.keycloak.events.Errors;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientModel;
Expand All @@ -39,6 +40,7 @@
import org.keycloak.services.validation.ClientValidator;
import org.keycloak.services.validation.PairwiseClientValidator;
import org.keycloak.services.validation.ValidationMessages;
import org.keycloak.validation.ClientValidationUtil;

import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
Expand Down Expand Up @@ -203,6 +205,11 @@ public Response createClient(final ClientRepresentation rep) {
}
}

ClientValidationUtil.validate(session, clientModel, true, c -> {
session.getTransactionManager().setRollbackOnly();
throw new ErrorResponseException(Errors.INVALID_INPUT, c.getError(), Response.Status.BAD_REQUEST);
});

return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build()).build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
Expand Down

0 comments on commit 7545749

Please sign in to comment.