KEYCLOAK-14306 OIDC redirect_uri allows dangerous schemes resulting in potential XSS

(cherry picked from commit e86bec81744707f270230b5da40e02a7aba17830)

Conflicts:
    testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
    testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
    services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java
This commit is contained in:
vmuzikar 2020-07-20 09:08:28 +02:00 committed by Marek Posolda
parent e8e5808aa9
commit 01be601dbd
30 changed files with 820 additions and 718 deletions

View file

@ -136,7 +136,7 @@ import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.validation.ClientValidationUtil; import org.keycloak.validation.ValidationUtil;
public class RepresentationToModel { public class RepresentationToModel {
@ -1266,8 +1266,8 @@ public class RepresentationToModel {
ClientModel app = createClient(session, realm, resourceRep, false, mappedFlows); ClientModel app = createClient(session, realm, resourceRep, false, mappedFlows);
appMap.put(app.getClientId(), app); appMap.put(app.getClientId(), app);
ClientValidationUtil.validate(session, app, false, c -> { ValidationUtil.validateClient(session, app, false, r -> {
throw new RuntimeException("Invalid client " + app.getClientId() + ": " + c.getError()); throw new RuntimeException("Invalid client " + app.getClientId() + ": " + r.getAllErrorsAsString());
}); });
} }
return appMap; return appMap;

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Red Hat, Inc. and/or its affiliates * Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags. * and other contributors as indicated by the @author tags.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,26 +14,31 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.validation; package org.keycloak.validation;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
public interface ClientValidationContext { /**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
enum Event { */
CREATE, public class ClientValidationContext extends DefaultValidationContext<ClientModel> {
UPDATE public ClientValidationContext(Event event, KeycloakSession session, ClientModel objectToValidate) {
super(event, session, objectToValidate);
} }
Event getEvent(); public static class OIDCContext extends ClientValidationContext {
private final OIDCClientRepresentation oidcClient;
KeycloakSession getSession(); public OIDCContext(Event event, KeycloakSession session, ClientModel objectToValidate, OIDCClientRepresentation oidcClient) {
super(event, session, objectToValidate);
ClientModel getClient(); this.oidcClient = oidcClient;
}
String getError();
ClientValidationContext invalid(String error);
public OIDCClientRepresentation getOIDCClient() {
return oidcClient;
}
}
} }

View file

@ -16,11 +16,12 @@
*/ */
package org.keycloak.validation; package org.keycloak.validation;
import org.keycloak.provider.Provider; import org.keycloak.models.ClientModel;
public interface ClientValidationProvider extends Provider { public interface ClientValidationProvider extends Validator<ClientModel> {
void validate(ClientValidationContext context); // for a special case when performing OIDC client registration
ValidationResult validate(ClientValidationContext.OIDCContext validationContext);
@Override @Override
default void close() { default void close() {

View file

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

View file

@ -0,0 +1,77 @@
/*
* 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.validation;
import org.keycloak.models.KeycloakSession;
import java.util.HashSet;
import java.util.Set;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public abstract class DefaultValidationContext<T> implements ValidationContext<T> {
private final Event event;
private final KeycloakSession session;
private final T objectToValidate;
private final Set<ValidationError> errors;
public DefaultValidationContext(Event event, KeycloakSession session, T objectToValidate) {
this.event = event;
this.session = session;
this.objectToValidate = objectToValidate;
this.errors = new HashSet<>();
}
@Override
public Event getEvent() {
return event;
}
@Override
public KeycloakSession getSession() {
return session;
}
@Override
public T getObjectToValidate() {
return objectToValidate;
}
@Override
public ValidationContext<T> addError(String message) {
return addError(null, message, null);
}
@Override
public ValidationContext<T> addError(String fieldId, String message) {
return addError(fieldId, message, null);
}
@Override
public ValidationContext<T> addError(String fieldId, String message, String localizedMessageKey, Object... localizedMessageParams) {
errors.add(new ValidationError(fieldId, message, localizedMessageKey, localizedMessageParams));
return this;
}
@Override
public ValidationResult toResult() {
return new ValidationResult(new HashSet<>(errors));
}
}

View file

@ -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.KeycloakSession;
public interface ValidationContext<T> {
enum Event {
CREATE,
UPDATE
}
Event getEvent();
KeycloakSession getSession();
T getObjectToValidate();
ValidationContext<T> addError(String message);
ValidationContext<T> addError(String fieldId, String message);
ValidationContext<T> addError(String fieldId, String message, String localizedMessageKey, Object... localizedMessageParams);
ValidationResult toResult();
}

View file

@ -0,0 +1,87 @@
/*
* 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.validation;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Objects;
import java.util.Properties;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class ValidationError {
private final String fieldId;
private final String message;
private final String localizedMessageKey;
private final Object[] localizedMessageParameters;
public ValidationError(String fieldId, String message, String localizedMessageKey, Object[] localizedMessageParameters) {
if (message == null) {
throw new IllegalArgumentException("Message must be set");
}
this.fieldId = fieldId;
this.message = message;
this.localizedMessageKey = localizedMessageKey;
this.localizedMessageParameters = localizedMessageParameters;
}
public String getFieldId() {
return fieldId;
}
public String getLocalizedMessageKey() {
return localizedMessageKey;
}
public Object[] getLocalizedMessageParams() {
return localizedMessageParameters;
}
public String getMessage() {
return message;
}
public String getLocalizedMessage(Properties messagesBundle) {
if (getLocalizedMessageKey() != null) {
return MessageFormat.format(messagesBundle.getProperty(getLocalizedMessageKey(), getMessage()), getLocalizedMessageParams());
}
else {
return getMessage();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ValidationError error = (ValidationError) o;
return Objects.equals(fieldId, error.fieldId) &&
message.equals(error.message) &&
Objects.equals(localizedMessageKey, error.localizedMessageKey) &&
Arrays.equals(localizedMessageParameters, error.localizedMessageParameters);
}
@Override
public int hashCode() {
int result = Objects.hash(fieldId, message, localizedMessageKey);
result = 31 * result + Arrays.hashCode(localizedMessageParameters);
return result;
}
}

View file

@ -0,0 +1,69 @@
/*
* 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.validation;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class ValidationResult {
private final boolean valid;
private final Set<ValidationError> errors;
public ValidationResult(Set<ValidationError> errors) {
this.valid = errors.size() == 0;
this.errors = Collections.unmodifiableSet(errors);
}
public boolean isValid() {
return valid;
}
public Set<ValidationError> getErrors() {
return errors;
}
public String getAllErrorsAsString() {
return getAllErrorsAsString(ValidationError::getMessage);
}
public String getAllLocalizedErrorsAsString(Properties messagesBundle) {
return getAllErrorsAsString(x -> x.getLocalizedMessage(messagesBundle));
}
protected String getAllErrorsAsString(Function<ValidationError, String> function) {
return errors.stream().map(function).collect(Collectors.joining("; "));
}
public boolean fieldHasError(String fieldId) {
if (fieldId == null) {
return false;
}
for (ValidationError error : errors) {
if (fieldId.equals(error.getFieldId())) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,56 @@
/*
* 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 org.keycloak.representations.oidc.OIDCClientRepresentation;
import javax.ws.rs.BadRequestException;
public class ValidationUtil {
public static void validateClient(KeycloakSession session, ClientModel client, boolean create, ErrorHandler errorHandler) throws BadRequestException {
validateClient(session, client, null, create, errorHandler);
}
public static void validateClient(KeycloakSession session, ClientModel client, OIDCClientRepresentation oidcClient, boolean create, ErrorHandler errorHandler) throws BadRequestException {
ClientValidationProvider provider = session.getProvider(ClientValidationProvider.class);
if (provider != null) {
ValidationContext.Event event = create ? ValidationContext.Event.CREATE : ValidationContext.Event.UPDATE;
ValidationResult result;
if (oidcClient != null) {
result = provider.validate(new ClientValidationContext.OIDCContext(event, session, client, oidcClient));
}
else {
result = provider.validate(new ClientValidationContext(event, session, client));
}
if (!result.isValid()) {
errorHandler.onError(result);
}
}
}
public interface ErrorHandler {
void onError(ValidationResult context);
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.validation;
import org.keycloak.provider.Provider;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public interface Validator<T> extends Provider {
ValidationResult validate(ValidationContext<T> validationContext);
}

View file

@ -46,6 +46,7 @@ public class EntityDescriptorClientRegistrationProvider extends AbstractClientRe
ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor); ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
EntityDescriptorClientRegistrationContext context = new EntityDescriptorClientRegistrationContext(session, client, this); EntityDescriptorClientRegistrationContext context = new EntityDescriptorClientRegistrationContext(session, client, this);
client = create(context); client = create(context);
validateClient(client, true);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build(); URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
return Response.created(uri).entity(client).build(); return Response.created(uri).entity(client).build();
} }

View file

@ -19,8 +19,6 @@ package org.keycloak.services.clientregistration;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.validation.ClientValidator;
import org.keycloak.services.validation.ValidationMessages;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -52,8 +50,4 @@ public abstract class AbstractClientRegistrationContext implements ClientRegistr
return provider; return provider;
} }
@Override
public boolean validateClient(ValidationMessages validationMessages) {
return ClientValidator.validate(client, validationMessages);
}
} }

View file

@ -19,18 +19,22 @@ package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.*; import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager; import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
import org.keycloak.services.clientregistration.policy.RegistrationAuth; import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.validation.ValidationMessages; import org.keycloak.validation.ValidationUtil;
import org.keycloak.validation.ClientValidationUtil;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -54,16 +58,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
RegistrationAuth registrationAuth = auth.requireCreate(context); RegistrationAuth registrationAuth = auth.requireCreate(context);
ValidationMessages validationMessages = new ValidationMessages();
if (!context.validateClient(validationMessages)) {
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
throw new ErrorResponseException(
errorCode,
validationMessages.getStringMessages(),
Response.Status.BAD_REQUEST
);
}
try { try {
RealmModel realm = session.getContext().getRealm(); RealmModel realm = session.getContext().getRealm();
ClientModel clientModel = ClientManager.createClient(session, realm, client, true); ClientModel clientModel = ClientManager.createClient(session, realm, client, true);
@ -82,11 +76,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
client.setSecret(clientModel.getSecret()); 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); String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel, registrationAuth);
client.setRegistrationAccessToken(registrationAccessToken); client.setRegistrationAccessToken(registrationAccessToken);
@ -133,24 +122,9 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST); throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST);
} }
ValidationMessages validationMessages = new ValidationMessages();
if (!context.validateClient(validationMessages)) {
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
throw new ErrorResponseException(
errorCode,
validationMessages.getStringMessages(),
Response.Status.BAD_REQUEST
);
}
RepresentationToModel.updateClient(rep, client); RepresentationToModel.updateClient(rep, client);
RepresentationToModel.updateClientProtocolMappers(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); rep = ModelToRepresentation.toRepresentation(client, session);
if (auth.isRegistrationAccessToken()) { if (auth.isRegistrationAccessToken()) {
@ -178,6 +152,18 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
} }
} }
public void validateClient(ClientModel clientModel, OIDCClientRepresentation oidcClient, boolean create) {
ValidationUtil.validateClient(session, clientModel, oidcClient, create, r -> {
session.getTransactionManager().setRollbackOnly();
String errorCode = r.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
throw new ErrorResponseException(errorCode, r.getAllErrorsAsString(), Response.Status.BAD_REQUEST);
});
}
public void validateClient(ClientRepresentation clientRep, boolean create) {
validateClient(session.getContext().getRealm().getClientByClientId(clientRep.getClientId()), null, create);
}
@Override @Override
public void setAuth(ClientRegistrationAuth auth) { public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth; this.auth = auth;

View file

@ -19,7 +19,6 @@ package org.keycloak.services.clientregistration;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.validation.ValidationMessages;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -32,6 +31,4 @@ public interface ClientRegistrationContext {
ClientRegistrationProvider getProvider(); ClientRegistrationProvider getProvider();
boolean validateClient(ValidationMessages validationMessages);
} }

View file

@ -19,8 +19,6 @@ package org.keycloak.services.clientregistration;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.validation.PairwiseClientValidator;
import org.keycloak.services.validation.ValidationMessages;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -31,8 +29,4 @@ public class DefaultClientRegistrationContext extends AbstractClientRegistration
super(session, client, provider); super(session, client, provider);
} }
@Override
public boolean validateClient(ValidationMessages validationMessages) {
return super.validateClient(validationMessages) && PairwiseClientValidator.validate(session, client, validationMessages);
}
} }

View file

@ -48,6 +48,7 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
public Response createDefault(ClientRepresentation client) { public Response createDefault(ClientRepresentation client) {
DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this); DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this);
client = create(context); client = create(context);
validateClient(client, true);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build(); URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
return Response.created(uri).entity(client).build(); return Response.created(uri).entity(client).build();
} }
@ -68,6 +69,7 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
public Response updateDefault(@PathParam("clientId") String clientId, ClientRepresentation client) { public Response updateDefault(@PathParam("clientId") String clientId, ClientRepresentation client) {
DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this); DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this);
client = update(clientId, context); client = update(clientId, context);
validateClient(client, false);
return Response.ok(client).build(); return Response.ok(client).build();
} }

View file

@ -17,18 +17,11 @@
package org.keycloak.services.clientregistration.oidc; package org.keycloak.services.clientregistration.oidc;
import java.util.HashSet;
import java.util.Set;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientregistration.AbstractClientRegistrationContext; import org.keycloak.services.clientregistration.AbstractClientRegistrationContext;
import org.keycloak.services.clientregistration.ClientRegistrationProvider; import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import org.keycloak.services.clientregistration.DefaultClientRegistrationContext;
import org.keycloak.services.validation.PairwiseClientValidator;
import org.keycloak.services.validation.ValidationMessages;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -42,21 +35,4 @@ public class OIDCClientRegistrationContext extends AbstractClientRegistrationCon
this.oidcRep = oidcRep; this.oidcRep = oidcRep;
} }
@Override
public boolean validateClient(ValidationMessages validationMessages) {
boolean valid = super.validateClient(validationMessages);
String rootUrl = client.getRootUrl();
Set<String> redirectUris = new HashSet<>();
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
SubjectType subjectType = SubjectType.parse(oidcRep.getSubjectType());
String sectorIdentifierUri = oidcRep.getSectorIdentifierUri();
// If sector_identifier_uri is in oidc config, then always validate it
if (SubjectType.PAIRWISE == subjectType || (sectorIdentifierUri != null && !sectorIdentifierUri.isEmpty())) {
valid = valid && PairwiseClientValidator.validate(session, rootUrl, redirectUris, oidcRep.getSectorIdentifierUri(), validationMessages);
}
return valid;
}
} }

View file

@ -86,6 +86,8 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri()); updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri());
updateClientRepWithProtocolMappers(clientModel, client); updateClientRepWithProtocolMappers(clientModel, client);
validateClient(clientModel, clientOIDC, true);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build(); URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri); clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
clientOIDC.setClientIdIssuedAt(Time.currentTime()); clientOIDC.setClientIdIssuedAt(Time.currentTime());
@ -122,6 +124,8 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri()); updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri());
updateClientRepWithProtocolMappers(clientModel, client); updateClientRepWithProtocolMappers(clientModel, client);
validateClient(clientModel, clientOIDC, false);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build(); URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri); clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
return Response.ok(clientOIDC).build(); return Response.ok(clientOIDC).build();

View file

@ -22,7 +22,6 @@ import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authorization.admin.AuthorizationService; import org.keycloak.authorization.admin.AuthorizationService;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
@ -63,11 +62,8 @@ import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.services.validation.ClientValidator; import org.keycloak.utils.ReservedCharValidator;
import org.keycloak.services.validation.PairwiseClientValidator; import org.keycloak.validation.ValidationUtil;
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.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
@ -87,10 +83,8 @@ import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import org.keycloak.utils.ReservedCharValidator;
/** /**
@ -138,16 +132,6 @@ public class ClientResource {
public Response update(final ClientRepresentation rep) { public Response update(final ClientRepresentation rep) {
auth.clients().requireConfigure(client); auth.clients().requireConfigure(client);
ValidationMessages validationMessages = new ValidationMessages();
if (!ClientValidator.validate(rep, validationMessages) || !PairwiseClientValidator.validate(session, rep, validationMessages)) {
Properties messages = AdminRoot.getMessages(session, realm, auth.adminAuth().getToken().getLocale());
throw new ErrorResponseException(
validationMessages.getStringMessages(),
validationMessages.getStringMessages(messages),
Response.Status.BAD_REQUEST
);
}
try { try {
session.clientPolicy().triggerOnEvent(new AdminClientUpdateContext(rep, auth.adminAuth(), client)); session.clientPolicy().triggerOnEvent(new AdminClientUpdateContext(rep, auth.adminAuth(), client));
} catch (ClientPolicyException cpe) { } catch (ClientPolicyException cpe) {
@ -157,9 +141,12 @@ public class ClientResource {
try { try {
updateClientFromRep(rep, client, session); updateClientFromRep(rep, client, session);
ClientValidationUtil.validate(session, client, false, c -> { ValidationUtil.validateClient(session, client, false, r -> {
session.getTransactionManager().setRollbackOnly(); session.getTransactionManager().setRollbackOnly();
throw new ErrorResponseException(Errors.INVALID_INPUT ,c.getError(), Response.Status.BAD_REQUEST); throw new ErrorResponseException(
Errors.INVALID_INPUT,
r.getAllLocalizedErrorsAsString(AdminRoot.getMessages(session, realm, auth.adminAuth().getToken().getLocale())),
Response.Status.BAD_REQUEST);
}); });
adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success(); adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success();

View file

@ -39,10 +39,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.validation.ClientValidator; import org.keycloak.validation.ValidationUtil;
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.Consumes;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
@ -59,6 +56,9 @@ import javax.ws.rs.core.Response;
import java.util.Objects; import java.util.Objects;
import java.util.Properties; import java.util.Properties;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
@ -169,16 +169,6 @@ public class ClientsResource {
public Response createClient(final ClientRepresentation rep) { public Response createClient(final ClientRepresentation rep) {
auth.clients().requireManage(); auth.clients().requireManage();
ValidationMessages validationMessages = new ValidationMessages();
if (!ClientValidator.validate(rep, validationMessages) || !PairwiseClientValidator.validate(session, rep, validationMessages)) {
Properties messages = AdminRoot.getMessages(session, realm, auth.adminAuth().getToken().getLocale());
throw new ErrorResponseException(
validationMessages.getStringMessages(),
validationMessages.getStringMessages(messages),
Response.Status.BAD_REQUEST
);
}
try { try {
session.clientPolicy().triggerOnEvent(new AdminClientRegisterContext(rep, auth.adminAuth())); session.clientPolicy().triggerOnEvent(new AdminClientRegisterContext(rep, auth.adminAuth()));
} catch (ClientPolicyException cpe) { } catch (ClientPolicyException cpe) {
@ -210,9 +200,12 @@ public class ClientsResource {
} }
} }
ClientValidationUtil.validate(session, clientModel, true, c -> { ValidationUtil.validateClient(session, clientModel, true, r -> {
session.getTransactionManager().setRollbackOnly(); session.getTransactionManager().setRollbackOnly();
throw new ErrorResponseException(Errors.INVALID_INPUT, c.getError(), Response.Status.BAD_REQUEST); throw new ErrorResponseException(
Errors.INVALID_INPUT,
r.getAllLocalizedErrorsAsString(AdminRoot.getMessages(session, realm, auth.adminAuth().getToken().getLocale())),
Response.Status.BAD_REQUEST);
}); });
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build()).build(); return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build()).build();

View file

@ -1,54 +0,0 @@
/*
*
* * Copyright 2016 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.validation;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class ClientValidator {
/**
* Checks if the Client's Redirect URIs doesn't contain any URI fragments (like http://example.org/auth#fragment)
*
* @see <a href="https://issues.jboss.org/browse/KEYCLOAK-3421">KEYCLOAK-3421</a>
* @param client
* @param messages
* @return true if Redirect URIs doesn't contain any URI with fragments
*/
public static boolean validate(ClientRepresentation client, ValidationMessages messages) {
boolean isValid = true;
if (client.getRedirectUris() != null) {
long urisWithFragmentCount = client.getRedirectUris().stream().filter(p -> p.contains("#")).count();
if (urisWithFragmentCount > 0) {
messages.add("redirectUris", "Redirect URIs must not contain an URI fragment", "clientRedirectURIsFragmentError");
isValid = false;
}
}
if (client.getRootUrl() != null && client.getRootUrl().contains("#")) {
messages.add("rootUrl", "Root URL must not contain an URL fragment", "clientRootURLFragmentError");
isValid = false;
}
return isValid;
}
}

View file

@ -1,47 +0,0 @@
package org.keycloak.services.validation;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:martin.hardselius@gmail.com">Martin Hardselius</a>
*/
public class PairwiseClientValidator {
public static boolean validate(KeycloakSession session, ClientRepresentation client, ValidationMessages messages) {
String rootUrl = client.getRootUrl();
Set<String> redirectUris = new HashSet<>();
boolean valid = true;
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
for (ProtocolMapperRepresentation foundPairwise : foundPairwiseMappers) {
String sectorIdentifierUri = PairwiseSubMapperHelper.getSectorIdentifierUri(foundPairwise);
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
valid = valid && validate(session, rootUrl, redirectUris, sectorIdentifierUri, messages);
}
return true;
}
public static boolean validate(KeycloakSession session, String rootUrl, Set<String> redirectUris, String sectorIdentifierUri, ValidationMessages messages) {
try {
PairwiseSubMapperValidator.validate(session, rootUrl, redirectUris, sectorIdentifierUri);
} catch (ProtocolMapperConfigException e) {
messages.add(e.getMessage(), e.getMessageKey());
return false;
}
return true;
}
}

View file

@ -1,100 +0,0 @@
/*
*
* * Copyright 2016 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.validation;
import java.text.MessageFormat;
import java.util.Properties;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class ValidationMessage {
private String fieldId;
private String message;
private String localizedMessageKey;
private Object[] localizedMessageParameters;
public ValidationMessage(String message) {
this.message = message;
}
public ValidationMessage(String message, String localizedMessageKey, Object... localizedMessageParameters) {
this.message = message;
this.localizedMessageKey = localizedMessageKey;
this.localizedMessageParameters = localizedMessageParameters;
}
public String getFieldId() {
return fieldId;
}
public void setFieldId(String fieldId) {
this.fieldId = fieldId;
}
public String getLocalizedMessageKey() {
return localizedMessageKey;
}
public void setLocalizedMessageKey(String localizedMessageKey) {
this.localizedMessageKey = localizedMessageKey;
}
public Object[] getLocalizedMessageParameters() {
return localizedMessageParameters;
}
public void setLocalizedMessageParameters(Object[] localizedMessageParameters) {
this.localizedMessageParameters = localizedMessageParameters;
}
public String getMessage() {
return message;
}
public String getMessage(Properties localizedMessages) {
if (getLocalizedMessageKey() != null) {
return MessageFormat.format(localizedMessages.getProperty(getLocalizedMessageKey(), getMessage()), getLocalizedMessageParameters());
}
else {
return getMessage();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ValidationMessage message1 = (ValidationMessage) o;
if (getFieldId() != null ? !getFieldId().equals(message1.getFieldId()) : message1.getFieldId() != null)
return false;
return getMessage() != null ? getMessage().equals(message1.getMessage()) : message1.getMessage() == null;
}
@Override
public int hashCode() {
int result = getFieldId() != null ? getFieldId().hashCode() : 0;
result = 31 * result + (getMessage() != null ? getMessage().hashCode() : 0);
return result;
}
}

View file

@ -1,89 +0,0 @@
/*
*
* * Copyright 2016 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.validation;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class ValidationMessages {
private Set<ValidationMessage> messages = new LinkedHashSet<>();
public ValidationMessages() {}
public ValidationMessages(String... messages) {
for (String message : messages) {
add(message);
}
}
public void add(String message) {
messages.add(new ValidationMessage(message));
}
public void add(String message, String localizedMessageKey) {
messages.add(new ValidationMessage(message, localizedMessageKey));
}
public void add(String fieldId, String message, String localizedMessageKey) {
ValidationMessage validationMessage = new ValidationMessage(message, localizedMessageKey);
validationMessage.setFieldId(fieldId);
add(validationMessage);
}
public void add(ValidationMessage message) {
messages.add(message);
}
public boolean fieldHasError(String fieldId) {
if (fieldId == null) {
return false;
}
for (ValidationMessage message : messages) {
if (fieldId.equals(message.getFieldId())) {
return true;
}
}
return false;
}
public Set<ValidationMessage> getMessages() {
return Collections.unmodifiableSet(messages);
}
protected String getStringMessages(Function<? super ValidationMessage, ? extends String> function) {
return messages.stream().map(function).collect(Collectors.joining("; "));
}
public String getStringMessages() {
return getStringMessages(ValidationMessage::getMessage);
}
public String getStringMessages(Properties localizedMessages) {
return getStringMessages(x -> x.getMessage(localizedMessages));
}
}

View file

@ -17,84 +17,200 @@
package org.keycloak.validation; package org.keycloak.validation;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
public class DefaultClientValidationProvider implements ClientValidationProvider { public class DefaultClientValidationProvider implements ClientValidationProvider {
private enum FieldMessages {
ROOT_URL("rootUrl",
"Root URL is not a valid URL", "clientRootURLInvalid",
"Root URL must not contain an URL fragment", "clientRootURLFragmentError",
"Root URL uses an illegal scheme", "clientRootURLIllegalSchemeError"),
private ClientValidationContext context; BASE_URL("baseUrl",
"Base URL is not a valid URL", "clientBaseURLInvalid",
null, null,
"Base URL uses an illegal scheme", "clientBaseURLIllegalSchemeError"),
REDIRECT_URIS("redirectUris",
"A redirect URI is not a valid URI", "clientRedirectURIsInvalid",
"Redirect URIs must not contain an URI fragment", "clientRedirectURIsFragmentError",
"A redirect URI uses an illegal scheme", "clientRedirectURIsIllegalSchemeError"),
BACKCHANNEL_LOGOUT_URL("backchannelLogoutUrl",
"Backchannel logout URL is not a valid URL", "backchannelLogoutUrlIsInvalid",
null, null,
"Backchannel logout URL uses an illegal scheme", "backchannelLogoutUrlIllegalSchemeError");
private String fieldId;
private String invalid;
private String invalidKey;
private String fragment;
private String fragmentKey;
private String scheme;
private String schemeKey;
FieldMessages(String fieldId, String invalid, String invalidKey, String fragment, String fragmentKey, String scheme, String schemeKey) {
this.fieldId = fieldId;
this.invalid = invalid;
this.invalidKey = invalidKey;
this.fragment = fragment;
this.fragmentKey = fragmentKey;
this.scheme = scheme;
this.schemeKey = schemeKey;
}
public String getFieldId() {
return fieldId;
}
public String getInvalid() {
return invalid;
}
public String getInvalidKey() {
return invalidKey;
}
public String getFragment() {
return fragment;
}
public String getFragmentKey() {
return fragmentKey;
}
public String getScheme() {
return scheme;
}
public String getSchemeKey() {
return schemeKey;
}
}
// TODO Before adding more validation consider using a library for validation // TODO Before adding more validation consider using a library for validation
@Override @Override
public void validate(ClientValidationContext context) { public ValidationResult validate(ValidationContext<ClientModel> context) {
this.context = context; validateUrls(context);
validatePairwiseInClientModel(context);
try { return context.toResult();
validate(context.getClient());
} catch (ValidationException e) {
context.invalid(e.getMessage());
}
} }
private void validate(ClientModel client) throws ValidationException { @Override
public ValidationResult validate(ClientValidationContext.OIDCContext context) {
validateUrls(context);
validatePairwiseInOIDCClient(context);
return context.toResult();
}
private void validateUrls(ValidationContext<ClientModel> context) {
ClientModel client = context.getObjectToValidate();
// Use a fake URL for validating relative URLs as we may not be validating clients in the context of a request (import at startup) // Use a fake URL for validating relative URLs as we may not be validating clients in the context of a request (import at startup)
String authServerUrl = "https://localhost/auth"; String authServerUrl = "https://localhost/auth";
String resolvedRootUrl = ResolveRelative.resolveRootUrl(authServerUrl, authServerUrl, client.getRootUrl()); String rootUrl = ResolveRelative.resolveRootUrl(authServerUrl, authServerUrl, client.getRootUrl());
String resolvedBaseUrl = ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, resolvedRootUrl, client.getBaseUrl());
// don't need to use actual rootUrl here as it'd interfere with others URL validations
String baseUrl = ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, authServerUrl, client.getBaseUrl());
String backchannelLogoutUrl = OIDCAdvancedConfigWrapper.fromClientModel(client).getBackchannelLogoutUrl(); String backchannelLogoutUrl = OIDCAdvancedConfigWrapper.fromClientModel(client).getBackchannelLogoutUrl();
String resolvedBackchannelLogoutUrl = String resolvedBackchannelLogoutUrl =
ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, resolvedRootUrl, backchannelLogoutUrl); ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, authServerUrl, backchannelLogoutUrl);
validateRootUrl(resolvedRootUrl); checkUri(FieldMessages.ROOT_URL, rootUrl, context, true, true);
validateBaseUrl(resolvedBaseUrl); checkUri(FieldMessages.BASE_URL, baseUrl, context, true, false);
validateBackchannelLogoutUrl(resolvedBackchannelLogoutUrl); checkUri(FieldMessages.BACKCHANNEL_LOGOUT_URL, resolvedBackchannelLogoutUrl, context, true, false);
client.getRedirectUris().stream()
.map(u -> ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, rootUrl, u))
.forEach(u -> checkUri(FieldMessages.REDIRECT_URIS, u, context, false, true));
} }
private void validateRootUrl(String rootUrl) throws ValidationException { private void checkUri(FieldMessages field, String url, ValidationContext<ClientModel> context, boolean checkValidUrl, boolean checkFragment) {
if (rootUrl != null && !rootUrl.isEmpty()) { if (url == null || url.isEmpty()) {
basicHttpUrlCheck("rootUrl", rootUrl); return;
}
} }
private void validateBaseUrl(String baseUrl) throws ValidationException {
if (baseUrl != null && !baseUrl.isEmpty()) {
basicHttpUrlCheck("baseUrl", baseUrl);
}
}
private void validateBackchannelLogoutUrl(String backchannelLogoutUrl) throws ValidationException {
if (backchannelLogoutUrl != null && !backchannelLogoutUrl.isEmpty()) {
basicHttpUrlCheck("backchannelLogoutUrl", backchannelLogoutUrl);
}
}
private void basicHttpUrlCheck(String field, String url) throws ValidationException {
boolean valid = true;
try { try {
URI uri = new URL(url).toURI(); URI uri = new URI(url);
if (uri.getScheme() == null || uri.getScheme().isEmpty()) {
valid = false; boolean valid = true;
} if (uri.getScheme() != null && (uri.getScheme().equals("data") || uri.getScheme().equals("javascript"))) {
} catch (MalformedURLException | URISyntaxException e) { context.addError(field.getFieldId(), field.getScheme(), field.getSchemeKey());
valid = false; valid = false;
} }
if (!valid) { // KEYCLOAK-3421
throw new ValidationException("Invalid URL in " + field); if (checkFragment && uri.getFragment() != null) {
context.addError(field.getFieldId(), field.getFragment(), field.getFragmentKey());
valid = false;
}
// Don't check if URL is valid if there are other problems with it; otherwise it could lead to duplicit errors.
// This cannot be moved higher because it acts on differently based on environment (e.g. sometimes it checks
// scheme, sometimes it doesn't).
if (checkValidUrl && valid) {
uri.toURL(); // throws an exception
}
}
catch (MalformedURLException | IllegalArgumentException | URISyntaxException e) {
context.addError(field.getFieldId(), field.getInvalid(), field.getInvalidKey());
} }
} }
static class ValidationException extends Exception { private void validatePairwiseInClientModel(ValidationContext<ClientModel> context) {
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(toRepresentation(context.getObjectToValidate(), context.getSession()));
public ValidationException(String message) { for (ProtocolMapperRepresentation foundPairwise : foundPairwiseMappers) {
super(message, null, false, false); String sectorIdentifierUri = PairwiseSubMapperHelper.getSectorIdentifierUri(foundPairwise);
validatePairwise(context, sectorIdentifierUri);
}
}
private void validatePairwiseInOIDCClient(ClientValidationContext.OIDCContext context) {
OIDCClientRepresentation oidcRep = context.getOIDCClient();
SubjectType subjectType = SubjectType.parse(oidcRep.getSubjectType());
String sectorIdentifierUri = oidcRep.getSectorIdentifierUri();
// If sector_identifier_uri is in oidc config, then always validate it
if (SubjectType.PAIRWISE == subjectType || (sectorIdentifierUri != null && !sectorIdentifierUri.isEmpty())) {
validatePairwise(context, oidcRep.getSectorIdentifierUri());
}
}
private void validatePairwise(ValidationContext<ClientModel> context, String sectorIdentifierUri) {
ClientModel client = context.getObjectToValidate();
String rootUrl = client.getRootUrl();
Set<String> redirectUris = new HashSet<>();
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
try {
PairwiseSubMapperValidator.validate(context.getSession(), rootUrl, redirectUris, sectorIdentifierUri);
} catch (ProtocolMapperConfigException e) {
context.addError("pairWise", e.getMessage(), e.getMessageKey());
} }
} }

View file

@ -52,15 +52,19 @@ import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@ -105,98 +109,110 @@ public class ClientTest extends AbstractAdminTest {
} }
@Test @Test
public void createClientValidation() { public void testInvalidUrlClientValidation() {
ClientRepresentation rep = new ClientRepresentation(); testClientUriValidation("Root URL is not a valid URL",
rep.setClientId("my-app"); "Base URL is not a valid URL",
rep.setDescription("my-app description"); "Backchannel logout URL is not a valid URL",
rep.setEnabled(true); null,
"invalid", "myapp://some-fake-app");
rep.setRootUrl("invalid");
createClientExpectingValidationError(rep, "Invalid URL in rootUrl");
rep.setRootUrl(null);
rep.setBaseUrl("invalid");
createClientExpectingValidationError(rep, "Invalid URL in baseUrl");
rep.setRootUrl(null);
rep.setBaseUrl("/valid");
createClientExpectingSuccessfulClientCreation(rep);
rep.setRootUrl("");
rep.setBaseUrl("/valid");
createClientExpectingSuccessfulClientCreation(rep);
rep.setBaseUrl(null);
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setBackchannelLogoutUrl("invalid");
createClientExpectingValidationError(rep, "Invalid URL in backchannelLogoutUrl");
} }
@Test @Test
public void updateClientValidation() { public void testIllegalSchemeClientValidation() {
ClientRepresentation rep = createClient(); testClientUriValidation("Root URL uses an illegal scheme",
"Base URL uses an illegal scheme",
"Backchannel logout URL uses an illegal scheme",
"A redirect URI uses an illegal scheme",
"data:text/html;base64,PHNjcmlwdD5jb25maXJtKGRvY3VtZW50LmRvbWFpbik7PC9zY3JpcHQ+",
"javascript:confirm(document.domain)/*"
);
}
rep.setClientId("my-app"); // KEYCLOAK-3421
rep.setDescription("my-app description"); @Test
public void testFragmentProhibitedClientValidation() {
testClientUriValidation("Root URL must not contain an URL fragment",
null,
null,
"Redirect URIs must not contain an URI fragment",
"http://redhat.com/abcd#someFragment"
);
}
private void testClientUriValidation(String expectedRootUrlError, String expectedBaseUrlError, String expectedBackchannelLogoutUrlError, String expectedRedirectUrisError, String... testUrls) {
testClientUriValidation(false, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls);
testClientUriValidation(true, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls);
}
private void testClientUriValidation(boolean create, String expectedRootUrlError, String expectedBaseUrlError, String expectedBackchannelLogoutUrlError, String expectedRedirectUrisError, String... testUrls) {
ClientRepresentation rep;
if (create) {
rep = new ClientRepresentation();
rep.setClientId("my-app2");
rep.setEnabled(true); rep.setEnabled(true);
}
else {
rep = createClient();
}
rep.setRootUrl("invalid"); for (String testUrl : testUrls) {
updateClientExpectingValidationError(rep, "Invalid URL in rootUrl"); if (expectedRootUrlError != null) {
rep.setRootUrl(testUrl);
createOrUpdateClientExpectingValidationErrors(rep, create, expectedRootUrlError);
}
rep.setRootUrl(null); rep.setRootUrl(null);
rep.setBaseUrl("invalid");
updateClientExpectingValidationError(rep, "Invalid URL in baseUrl");
ClientRepresentation stored = realm.clients().get(rep.getId()).toRepresentation();
assertNull(stored.getRootUrl());
assertNull(stored.getBaseUrl());
rep.setRootUrl(null);
rep.setBaseUrl("/valid");
updateClientExpectingSuccessfulClientUpdate(rep, null, "/valid");
rep.setRootUrl("");
rep.setBaseUrl("/valid");
updateClientExpectingSuccessfulClientUpdate(rep, "", "/valid");
if (expectedBaseUrlError != null) {
rep.setBaseUrl(testUrl);
createOrUpdateClientExpectingValidationErrors(rep, create, expectedBaseUrlError);
}
rep.setBaseUrl(null); rep.setBaseUrl(null);
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setBackchannelLogoutUrl("invalid");
updateClientExpectingValidationError(rep, "Invalid URL in backchannelLogoutUrl"); if (expectedBackchannelLogoutUrlError != null) {
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setBackchannelLogoutUrl(testUrl);
createOrUpdateClientExpectingValidationErrors(rep, create, expectedBackchannelLogoutUrlError);
}
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setBackchannelLogoutUrl(null);
if (expectedRedirectUrisError != null) {
rep.setRedirectUris(Collections.singletonList(testUrl));
createOrUpdateClientExpectingValidationErrors(rep, create, expectedRedirectUrisError);
}
rep.setRedirectUris(null);
if (expectedRootUrlError != null) rep.setRootUrl(testUrl);
if (expectedBaseUrlError != null) rep.setBaseUrl(testUrl);
if (expectedRedirectUrisError != null) rep.setRedirectUris(Collections.singletonList(testUrl));
createOrUpdateClientExpectingValidationErrors(rep, create, expectedRootUrlError, expectedBaseUrlError, expectedRedirectUrisError);
rep.setRootUrl(null);
rep.setBaseUrl(null);
rep.setRedirectUris(null);
}
} }
private void createClientExpectingValidationError(ClientRepresentation rep, String expectedError) { private void createOrUpdateClientExpectingValidationErrors(ClientRepresentation rep, boolean create, String... expectedErrors) {
Response response = realm.clients().create(rep); Response response = null;
if (create) {
assertEquals(400, response.getStatus()); response = realm.clients().create(rep);
OAuth2ErrorRepresentation error = response.readEntity(OAuth2ErrorRepresentation.class);
assertEquals("invalid_input", error.getError());
assertEquals(expectedError, error.getErrorDescription());
assertNull(response.getLocation());
response.close();
} }
else {
private void createClientExpectingSuccessfulClientCreation(ClientRepresentation rep) {
Response response = realm.clients().create(rep);
assertEquals(201, response.getStatus());
String id = ApiUtil.getCreatedId(response);
realm.clients().get(id).remove();
response.close();
}
private void updateClientExpectingValidationError(ClientRepresentation rep, String expectedError) {
try { try {
realm.clients().get(rep.getId()).update(rep); realm.clients().get(rep.getId()).update(rep);
fail("Expected exception"); fail("Expected exception");
} catch (BadRequestException e) {
Response response = e.getResponse();
assertEquals(400, response.getStatus());
OAuth2ErrorRepresentation error = response.readEntity(OAuth2ErrorRepresentation.class);
assertEquals("invalid_input", error.getError());
assertEquals(expectedError, error.getErrorDescription());
} }
catch (BadRequestException e) {
response = e.getResponse();
}
}
expectedErrors = Arrays.stream(expectedErrors).filter(Objects::nonNull).toArray(String[]::new);
assertEquals(response.getStatus(), 400);
OAuth2ErrorRepresentation errorRep = response.readEntity(OAuth2ErrorRepresentation.class);
List<String> actualErrors = asList(errorRep.getErrorDescription().split("; "));
assertThat(actualErrors, containsInAnyOrder(expectedErrors));
assertEquals("invalid_input", errorRep.getError());
} }
private void updateClientExpectingSuccessfulClientUpdate(ClientRepresentation rep, String expectedRootUrl, String expectedBaseUrl) { private void updateClientExpectingSuccessfulClientUpdate(ClientRepresentation rep, String expectedRootUrl, String expectedBaseUrl) {
@ -378,55 +394,6 @@ public class ClientTest extends AbstractAdminTest {
assertNull(userRep.getEmail()); assertNull(userRep.getEmail());
} }
// KEYCLOAK-3421
@Test
public void createClientWithFragments() {
ClientRepresentation client = ClientBuilder.create()
.clientId("client-with-fragment")
.rootUrl("http://localhost/base#someFragment")
.redirectUris("http://localhost/auth", "http://localhost/auth#fragment", "http://localhost/auth*", "/relative")
.build();
Response response = realm.clients().create(client);
assertUriFragmentError(response);
}
// KEYCLOAK-3421
@Test
public void updateClientWithFragments() {
ClientRepresentation client = ClientBuilder.create()
.clientId("client-with-fragment")
.redirectUris("http://localhost/auth", "http://localhost/auth*")
.build();
Response response = realm.clients().create(client);
String clientUuid = ApiUtil.getCreatedId(response);
ClientResource clientResource = realm.clients().get(clientUuid);
getCleanup().addClientUuid(clientUuid);
response.close();
client = clientResource.toRepresentation();
client.setRootUrl("http://localhost/base#someFragment");
List<String> redirectUris = client.getRedirectUris();
redirectUris.add("http://localhost/auth#fragment");
redirectUris.add("/relative");
client.setRedirectUris(redirectUris);
try {
clientResource.update(client);
fail("Should fail");
}
catch (BadRequestException e) {
assertUriFragmentError(e.getResponse());
}
}
private void assertUriFragmentError(Response response) {
assertEquals(response.getStatus(), 400);
String error = response.readEntity(OAuth2ErrorRepresentation.class).getError();
assertTrue("Error response doesn't mention Redirect URIs fragments", error.contains("Redirect URIs must not contain an URI fragment"));
assertTrue("Error response doesn't mention Root URL fragments", error.contains("Root URL must not contain an URL fragment"));
}
@Test @Test
public void pushRevocation() { public void pushRevocation() {
testingClient.testApp().clearAdminActions(); testingClient.testApp().clearAdminActions();

View file

@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List; import java.util.List;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -44,6 +45,7 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
@ -83,7 +85,7 @@ public class AuthorizationAPITest extends AbstractAuthzTest {
.redirectUris("http://localhost/resource-server-test") .redirectUris("http://localhost/resource-server-test")
.defaultRoles("uma_protection") .defaultRoles("uma_protection")
.directAccessGrants() .directAccessGrants()
.pairwise("http://pairwise.com")) .pairwise(TestApplicationResourceUrls.pairwiseSectorIdentifierUri()))
.client(ClientBuilder.create().clientId(TEST_CLIENT) .client(ClientBuilder.create().clientId(TEST_CLIENT)
.secret("secret") .secret("secret")
.authorizationServicesEnabled(true) .authorizationServicesEnabled(true)
@ -95,6 +97,8 @@ public class AuthorizationAPITest extends AbstractAuthzTest {
.redirectUris("http://localhost/test-client") .redirectUris("http://localhost/test-client")
.directAccessGrants()) .directAccessGrants())
.build()); .build());
testingClient.testApp().oidcClientEndpoints().setSectorIdentifierRedirectUris(Collections.singletonList("http://localhost/resource-server-test"));
} }
@Before @Before

View file

@ -93,6 +93,7 @@ import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
@ -142,7 +143,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
.authorizationServicesEnabled(true) .authorizationServicesEnabled(true)
.redirectUris("http://localhost/resource-server-test") .redirectUris("http://localhost/resource-server-test")
.defaultRoles("uma_protection") .defaultRoles("uma_protection")
.pairwise("http://pairwise.com") .pairwise(TestApplicationResourceUrls.pairwiseSectorIdentifierUri())
.directAccessGrants()) .directAccessGrants())
.client(ClientBuilder.create().clientId(TEST_CLIENT) .client(ClientBuilder.create().clientId(TEST_CLIENT)
.secret("secret") .secret("secret")
@ -153,13 +154,19 @@ public class EntitlementAPITest extends AbstractAuthzTest {
.secret("secret") .secret("secret")
.authorizationServicesEnabled(true) .authorizationServicesEnabled(true)
.redirectUris("http://localhost/test-client") .redirectUris("http://localhost/test-client")
.pairwise("http://pairwise.com") .pairwise(TestApplicationResourceUrls.pairwiseSectorIdentifierUri())
.directAccessGrants()) .directAccessGrants())
.client(ClientBuilder.create().clientId(PUBLIC_TEST_CLIENT) .client(ClientBuilder.create().clientId(PUBLIC_TEST_CLIENT)
.secret("secret") .secret("secret")
.redirectUris("http://localhost:8180/auth/realms/master/app/auth/*", "https://localhost:8543/auth/realms/master/app/auth/*") .redirectUris("http://localhost:8180/auth/realms/master/app/auth/*", "https://localhost:8543/auth/realms/master/app/auth/*")
.publicClient()) .publicClient())
.build()); .build());
configureSectorIdentifierRedirectUris();
}
private void configureSectorIdentifierRedirectUris() {
testingClient.testApp().oidcClientEndpoints().setSectorIdentifierRedirectUris(Arrays.asList("http://localhost/resource-server-test", "http://localhost/test-client"));
} }
@Before @Before
@ -1936,6 +1943,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
controller.stop(suiteContext.getAuthServerInfo().getQualifier()); controller.stop(suiteContext.getAuthServerInfo().getQualifier());
controller.start(suiteContext.getAuthServerInfo().getQualifier()); controller.start(suiteContext.getAuthServerInfo().getQualifier());
reconnectAdminClient(); reconnectAdminClient();
configureSectorIdentifierRedirectUris();
TokenIntrospectionResponse introspectionResponse = authzClient.protection().introspectRequestingPartyToken(response.getToken()); TokenIntrospectionResponse introspectionResponse = authzClient.protection().introspectRequestingPartyToken(response.getToken());

View file

@ -24,13 +24,14 @@ import org.keycloak.client.registration.ClientRegistration;
import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException; import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.services.clientregistration.ErrorCodes; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
@ -38,11 +39,14 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is; import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -51,7 +55,8 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import static org.keycloak.services.clientregistration.ErrorCodes.INVALID_CLIENT_METADATA;
import static org.keycloak.services.clientregistration.ErrorCodes.INVALID_REDIRECT_URI;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -169,45 +174,121 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
} }
@Test @Test
public void registerClientValidation() throws IOException { public void testInvalidUrlClientValidation() {
authCreateClients(); testClientUriValidation("Root URL is not a valid URL",
ClientRepresentation client = buildClient(); "Base URL is not a valid URL",
client.setRootUrl("invalid"); "Backchannel logout URL is not a valid URL",
null,
try { "invalid", "myapp://some-fake-app");
registerClient(client);
} catch (ClientRegistrationException e) {
HttpErrorException c = (HttpErrorException) e.getCause();
assertEquals(400, c.getStatusLine().getStatusCode());
OAuth2ErrorRepresentation error = JsonSerialization.readValue(c.getErrorResponse(), OAuth2ErrorRepresentation.class);
assertEquals("invalid_client_metadata", error.getError());
assertEquals("Invalid URL in rootUrl", error.getErrorDescription());
}
} }
@Test @Test
public void updateClientValidation() throws IOException, ClientRegistrationException { public void testIllegalSchemeClientValidation() {
registerClientAsAdmin(); testClientUriValidation("Root URL uses an illegal scheme",
"Base URL uses an illegal scheme",
ClientRepresentation client = reg.get(CLIENT_ID); "Backchannel logout URL uses an illegal scheme",
client.setRootUrl("invalid"); "A redirect URI uses an illegal scheme",
"data:text/html;base64,PHNjcmlwdD5jb25maXJtKGRvY3VtZW50LmRvbWFpbik7PC9zY3JpcHQ+",
try { "javascript:confirm(document.domain)/*"
reg.update(client); );
} catch (ClientRegistrationException e) {
HttpErrorException c = (HttpErrorException) e.getCause();
assertEquals(400, c.getStatusLine().getStatusCode());
OAuth2ErrorRepresentation error = JsonSerialization.readValue(c.getErrorResponse(), OAuth2ErrorRepresentation.class);
assertEquals("invalid_client_metadata", error.getError());
assertEquals("Invalid URL in rootUrl", error.getErrorDescription());
} }
ClientRepresentation updatedClient = reg.get(CLIENT_ID); // KEYCLOAK-3421
assertNull(updatedClient.getRootUrl()); @Test
public void testFragmentProhibitedClientValidation() {
testClientUriValidation("Root URL must not contain an URL fragment",
null,
null,
"Redirect URIs must not contain an URI fragment",
"http://redhat.com/abcd#someFragment"
);
}
private void testClientUriValidation(String expectedRootUrlError, String expectedBaseUrlError, String expectedBackchannelLogoutUrlError, String expectedRedirectUrisError, String... testUrls) {
testClientUriValidation(true, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls);
testClientUriValidation(false, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls);
}
private void testClientUriValidation(boolean register, String expectedRootUrlError, String expectedBaseUrlError, String expectedBackchannelLogoutUrlError, String expectedRedirectUrisError, String... testUrls) {
ClientRepresentation rep;
if (register) {
authCreateClients();
rep = buildClient();
}
else {
try {
registerClientAsAdmin();
rep = reg.get(CLIENT_ID);
}
catch (ClientRegistrationException e) {
throw new RuntimeException(e);
}
}
for (String testUrl : testUrls) {
if (expectedRootUrlError != null) {
rep.setRootUrl(testUrl);
registerOrUpdateClientExpectingValidationErrors(rep, register, false, expectedRootUrlError);
}
rep.setRootUrl(null);
if (expectedBaseUrlError != null) {
rep.setBaseUrl(testUrl);
registerOrUpdateClientExpectingValidationErrors(rep, register, false, expectedBaseUrlError);
}
rep.setBaseUrl(null);
if (expectedBackchannelLogoutUrlError != null) {
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setBackchannelLogoutUrl(testUrl);
registerOrUpdateClientExpectingValidationErrors(rep, register, false, expectedBackchannelLogoutUrlError);
}
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setBackchannelLogoutUrl(null);
if (expectedRedirectUrisError != null) {
rep.setRedirectUris(Collections.singletonList(testUrl));
registerOrUpdateClientExpectingValidationErrors(rep, register, true, expectedRedirectUrisError);
}
rep.setRedirectUris(null);
if (expectedRootUrlError != null) rep.setRootUrl(testUrl);
if (expectedBaseUrlError != null) rep.setBaseUrl(testUrl);
if (expectedRedirectUrisError != null) rep.setRedirectUris(Collections.singletonList(testUrl));
registerOrUpdateClientExpectingValidationErrors(rep, register, expectedRedirectUrisError != null, expectedRootUrlError, expectedBaseUrlError, expectedRedirectUrisError);
rep.setRootUrl(null);
rep.setBaseUrl(null);
rep.setRedirectUris(null);
}
}
private void registerOrUpdateClientExpectingValidationErrors(ClientRepresentation rep, boolean register, boolean redirectUris, String... expectedErrors) {
HttpErrorException errorException = null;
try {
if (register) {
registerClient(rep);
}
else {
reg.update(rep);
}
fail("Expected exception");
}
catch (ClientRegistrationException e) {
errorException = (HttpErrorException) e.getCause();
}
expectedErrors = Arrays.stream(expectedErrors).filter(Objects::nonNull).toArray(String[]::new);
assertEquals(errorException.getStatusLine().getStatusCode(), 400);
OAuth2ErrorRepresentation errorRep;
try {
errorRep = JsonSerialization.readValue(errorException.getErrorResponse(), OAuth2ErrorRepresentation.class);
}
catch (IOException e) {
throw new RuntimeException(e);
}
List<String> actualErrors = asList(errorRep.getErrorDescription().split("; "));
assertThat(actualErrors, containsInAnyOrder(expectedErrors));
assertEquals(redirectUris ? INVALID_REDIRECT_URI : INVALID_CLIENT_METADATA, errorRep.getError());
} }
@Test @Test

View file

@ -23,6 +23,15 @@ ldapErrorMissingGroupsPathGroup=Groups path group does not exist - please create
clientRedirectURIsFragmentError=Redirect URIs must not contain an URI fragment clientRedirectURIsFragmentError=Redirect URIs must not contain an URI fragment
clientRootURLFragmentError=Root URL must not contain an URL fragment clientRootURLFragmentError=Root URL must not contain an URL fragment
clientRootURLIllegalSchemeError=Root URL uses an illegal scheme
clientBaseURLIllegalSchemeError=Base URL uses an illegal scheme
backchannelLogoutUrlIllegalSchemeError=Backchannel logout URL uses an illegal scheme
clientRedirectURIsIllegalSchemeError=A redirect URI uses an illegal scheme
clientBaseURLInvalid=Base URL is not a valid URL
clientRootURLInvalid=Root URL is not a valid URL
clientRedirectURIsInvalid=A redirect URI is not a valid URI
backchannelLogoutUrlIsInvalid=Backchannel logout URL is not a valid URL
pairwiseMalformedClientRedirectURI=Client contained an invalid redirect URI. pairwiseMalformedClientRedirectURI=Client contained an invalid redirect URI.
pairwiseClientRedirectURIsMissingHost=Client redirect URIs must contain a valid host component. pairwiseClientRedirectURIsMissingHost=Client redirect URIs must contain a valid host component.