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:
parent
e8e5808aa9
commit
01be601dbd
30 changed files with 820 additions and 718 deletions
|
@ -136,7 +136,7 @@ import org.keycloak.storage.UserStorageProvider;
|
|||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.federated.UserFederatedStorageProvider;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.validation.ClientValidationUtil;
|
||||
import org.keycloak.validation.ValidationUtil;
|
||||
|
||||
public class RepresentationToModel {
|
||||
|
||||
|
@ -1266,8 +1266,8 @@ public class RepresentationToModel {
|
|||
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());
|
||||
ValidationUtil.validateClient(session, app, false, r -> {
|
||||
throw new RuntimeException("Invalid client " + app.getClientId() + ": " + r.getAllErrorsAsString());
|
||||
});
|
||||
}
|
||||
return appMap;
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -14,26 +14,31 @@
|
|||
* 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;
|
||||
|
||||
public interface ClientValidationContext {
|
||||
|
||||
enum Event {
|
||||
CREATE,
|
||||
UPDATE
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class ClientValidationContext extends DefaultValidationContext<ClientModel> {
|
||||
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();
|
||||
|
||||
ClientModel getClient();
|
||||
|
||||
String getError();
|
||||
|
||||
ClientValidationContext invalid(String error);
|
||||
public OIDCContext(Event event, KeycloakSession session, ClientModel objectToValidate, OIDCClientRepresentation oidcClient) {
|
||||
super(event, session, objectToValidate);
|
||||
this.oidcClient = oidcClient;
|
||||
}
|
||||
|
||||
public OIDCClientRepresentation getOIDCClient() {
|
||||
return oidcClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,12 @@
|
|||
*/
|
||||
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
|
||||
default void close() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -46,6 +46,7 @@ public class EntityDescriptorClientRegistrationProvider extends AbstractClientRe
|
|||
ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
|
||||
EntityDescriptorClientRegistrationContext context = new EntityDescriptorClientRegistrationContext(session, client, this);
|
||||
client = create(context);
|
||||
validateClient(client, true);
|
||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
||||
return Response.created(uri).entity(client).build();
|
||||
}
|
||||
|
|
|
@ -19,8 +19,6 @@ package org.keycloak.services.clientregistration;
|
|||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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>
|
||||
|
@ -52,8 +50,4 @@ public abstract class AbstractClientRegistrationContext implements ClientRegistr
|
|||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateClient(ValidationMessages validationMessages) {
|
||||
return ClientValidator.validate(client, validationMessages);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,18 +19,22 @@ package org.keycloak.services.clientregistration;
|
|||
|
||||
import org.keycloak.events.EventBuilder;
|
||||
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.RepresentationToModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
|
||||
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
import org.keycloak.validation.ClientValidationUtil;
|
||||
import org.keycloak.validation.ValidationUtil;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
|
@ -54,16 +58,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
|
||||
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 {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ClientModel clientModel = ClientManager.createClient(session, realm, client, true);
|
||||
|
@ -82,11 +76,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
|
||||
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);
|
||||
|
||||
|
@ -133,24 +122,9 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
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.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()) {
|
||||
|
@ -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
|
||||
public void setAuth(ClientRegistrationAuth auth) {
|
||||
this.auth = auth;
|
||||
|
|
|
@ -19,7 +19,6 @@ package org.keycloak.services.clientregistration;
|
|||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -32,6 +31,4 @@ public interface ClientRegistrationContext {
|
|||
|
||||
ClientRegistrationProvider getProvider();
|
||||
|
||||
boolean validateClient(ValidationMessages validationMessages);
|
||||
|
||||
}
|
||||
|
|
|
@ -19,8 +19,6 @@ package org.keycloak.services.clientregistration;
|
|||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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>
|
||||
|
@ -31,8 +29,4 @@ public class DefaultClientRegistrationContext extends AbstractClientRegistration
|
|||
super(session, client, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateClient(ValidationMessages validationMessages) {
|
||||
return super.validateClient(validationMessages) && PairwiseClientValidator.validate(session, client, validationMessages);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
|
|||
public Response createDefault(ClientRepresentation client) {
|
||||
DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this);
|
||||
client = create(context);
|
||||
validateClient(client, true);
|
||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).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) {
|
||||
DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this);
|
||||
client = update(clientId, context);
|
||||
validateClient(client, false);
|
||||
return Response.ok(client).build();
|
||||
}
|
||||
|
||||
|
|
|
@ -17,18 +17,11 @@
|
|||
|
||||
package org.keycloak.services.clientregistration.oidc;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.utils.SubjectType;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.services.clientregistration.AbstractClientRegistrationContext;
|
||||
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>
|
||||
|
@ -42,21 +35,4 @@ public class OIDCClientRegistrationContext extends AbstractClientRegistrationCon
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,8 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
|||
updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri());
|
||||
updateClientRepWithProtocolMappers(clientModel, client);
|
||||
|
||||
validateClient(clientModel, clientOIDC, true);
|
||||
|
||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
||||
clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
|
||||
clientOIDC.setClientIdIssuedAt(Time.currentTime());
|
||||
|
@ -122,6 +124,8 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
|||
updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri());
|
||||
updateClientRepWithProtocolMappers(clientModel, client);
|
||||
|
||||
validateClient(clientModel, clientOIDC, false);
|
||||
|
||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
||||
clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
|
||||
return Response.ok(clientOIDC).build();
|
||||
|
|
|
@ -22,7 +22,6 @@ import org.jboss.resteasy.spi.BadRequestException;
|
|||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.authorization.admin.AuthorizationService;
|
||||
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;
|
||||
|
@ -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.AdminPermissionManagement;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
import org.keycloak.utils.ProfileHelper;
|
||||
import org.keycloak.validation.ClientValidationUtil;
|
||||
import org.keycloak.utils.ReservedCharValidator;
|
||||
import org.keycloak.validation.ValidationUtil;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
|
@ -87,10 +83,8 @@ import java.util.HashMap;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import org.keycloak.utils.ReservedCharValidator;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -138,16 +132,6 @@ public class ClientResource {
|
|||
public Response update(final ClientRepresentation rep) {
|
||||
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 {
|
||||
session.clientPolicy().triggerOnEvent(new AdminClientUpdateContext(rep, auth.adminAuth(), client));
|
||||
} catch (ClientPolicyException cpe) {
|
||||
|
@ -157,9 +141,12 @@ public class ClientResource {
|
|||
try {
|
||||
updateClientFromRep(rep, client, session);
|
||||
|
||||
ClientValidationUtil.validate(session, client, false, c -> {
|
||||
ValidationUtil.validateClient(session, client, false, r -> {
|
||||
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();
|
||||
|
|
|
@ -39,10 +39,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyException;
|
|||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
import org.keycloak.validation.ClientValidationUtil;
|
||||
import org.keycloak.validation.ValidationUtil;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
|
@ -59,6 +56,9 @@ import javax.ws.rs.core.Response;
|
|||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
|
@ -169,16 +169,6 @@ public class ClientsResource {
|
|||
public Response createClient(final ClientRepresentation rep) {
|
||||
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 {
|
||||
session.clientPolicy().triggerOnEvent(new AdminClientRegisterContext(rep, auth.adminAuth()));
|
||||
} 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();
|
||||
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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -17,84 +17,200 @@
|
|||
package org.keycloak.validation;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.protocol.ProtocolMapperConfigException;
|
||||
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 java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
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 {
|
||||
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
|
||||
@Override
|
||||
public void validate(ClientValidationContext context) {
|
||||
this.context = context;
|
||||
public ValidationResult validate(ValidationContext<ClientModel> context) {
|
||||
validateUrls(context);
|
||||
validatePairwiseInClientModel(context);
|
||||
|
||||
try {
|
||||
validate(context.getClient());
|
||||
} catch (ValidationException e) {
|
||||
context.invalid(e.getMessage());
|
||||
}
|
||||
return context.toResult();
|
||||
}
|
||||
|
||||
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)
|
||||
String authServerUrl = "https://localhost/auth";
|
||||
|
||||
String resolvedRootUrl = ResolveRelative.resolveRootUrl(authServerUrl, authServerUrl, client.getRootUrl());
|
||||
String resolvedBaseUrl = ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, resolvedRootUrl, client.getBaseUrl());
|
||||
String rootUrl = ResolveRelative.resolveRootUrl(authServerUrl, authServerUrl, client.getRootUrl());
|
||||
|
||||
// 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 resolvedBackchannelLogoutUrl =
|
||||
ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, resolvedRootUrl, backchannelLogoutUrl);
|
||||
ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, authServerUrl, backchannelLogoutUrl);
|
||||
|
||||
validateRootUrl(resolvedRootUrl);
|
||||
validateBaseUrl(resolvedBaseUrl);
|
||||
validateBackchannelLogoutUrl(resolvedBackchannelLogoutUrl);
|
||||
checkUri(FieldMessages.ROOT_URL, rootUrl, context, true, true);
|
||||
checkUri(FieldMessages.BASE_URL, baseUrl, context, true, false);
|
||||
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 {
|
||||
if (rootUrl != null && !rootUrl.isEmpty()) {
|
||||
basicHttpUrlCheck("rootUrl", rootUrl);
|
||||
}
|
||||
private void checkUri(FieldMessages field, String url, ValidationContext<ClientModel> context, boolean checkValidUrl, boolean checkFragment) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
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 {
|
||||
URI uri = new URL(url).toURI();
|
||||
if (uri.getScheme() == null || uri.getScheme().isEmpty()) {
|
||||
valid = false;
|
||||
}
|
||||
} catch (MalformedURLException | URISyntaxException e) {
|
||||
URI uri = new URI(url);
|
||||
|
||||
boolean valid = true;
|
||||
if (uri.getScheme() != null && (uri.getScheme().equals("data") || uri.getScheme().equals("javascript"))) {
|
||||
context.addError(field.getFieldId(), field.getScheme(), field.getSchemeKey());
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
throw new ValidationException("Invalid URL in " + field);
|
||||
// KEYCLOAK-3421
|
||||
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) {
|
||||
super(message, null, false, false);
|
||||
for (ProtocolMapperRepresentation foundPairwise : foundPairwiseMappers) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,15 +52,19 @@ import javax.ws.rs.BadRequestException;
|
|||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.junit.Assert.*;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
|
@ -105,98 +109,110 @@ public class ClientTest extends AbstractAdminTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void createClientValidation() {
|
||||
ClientRepresentation rep = new ClientRepresentation();
|
||||
rep.setClientId("my-app");
|
||||
rep.setDescription("my-app description");
|
||||
rep.setEnabled(true);
|
||||
|
||||
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");
|
||||
public void testInvalidUrlClientValidation() {
|
||||
testClientUriValidation("Root URL is not a valid URL",
|
||||
"Base URL is not a valid URL",
|
||||
"Backchannel logout URL is not a valid URL",
|
||||
null,
|
||||
"invalid", "myapp://some-fake-app");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateClientValidation() {
|
||||
ClientRepresentation rep = createClient();
|
||||
public void testIllegalSchemeClientValidation() {
|
||||
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");
|
||||
rep.setDescription("my-app description");
|
||||
// KEYCLOAK-3421
|
||||
@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);
|
||||
}
|
||||
else {
|
||||
rep = createClient();
|
||||
}
|
||||
|
||||
rep.setRootUrl("invalid");
|
||||
updateClientExpectingValidationError(rep, "Invalid URL in rootUrl");
|
||||
|
||||
for (String testUrl : testUrls) {
|
||||
if (expectedRootUrlError != null) {
|
||||
rep.setRootUrl(testUrl);
|
||||
createOrUpdateClientExpectingValidationErrors(rep, create, expectedRootUrlError);
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
Response response = realm.clients().create(rep);
|
||||
|
||||
assertEquals(400, response.getStatus());
|
||||
OAuth2ErrorRepresentation error = response.readEntity(OAuth2ErrorRepresentation.class);
|
||||
assertEquals("invalid_input", error.getError());
|
||||
assertEquals(expectedError, error.getErrorDescription());
|
||||
|
||||
assertNull(response.getLocation());
|
||||
|
||||
response.close();
|
||||
private void createOrUpdateClientExpectingValidationErrors(ClientRepresentation rep, boolean create, String... expectedErrors) {
|
||||
Response response = null;
|
||||
if (create) {
|
||||
response = realm.clients().create(rep);
|
||||
}
|
||||
|
||||
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) {
|
||||
else {
|
||||
try {
|
||||
realm.clients().get(rep.getId()).update(rep);
|
||||
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) {
|
||||
|
@ -378,55 +394,6 @@ public class ClientTest extends AbstractAdminTest {
|
|||
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
|
||||
public void pushRevocation() {
|
||||
testingClient.testApp().clearAdminActions();
|
||||
|
|
|
@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals;
|
|||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
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.arquillian.annotation.AuthServerContainerExclude;
|
||||
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.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
|
@ -83,7 +85,7 @@ public class AuthorizationAPITest extends AbstractAuthzTest {
|
|||
.redirectUris("http://localhost/resource-server-test")
|
||||
.defaultRoles("uma_protection")
|
||||
.directAccessGrants()
|
||||
.pairwise("http://pairwise.com"))
|
||||
.pairwise(TestApplicationResourceUrls.pairwiseSectorIdentifierUri()))
|
||||
.client(ClientBuilder.create().clientId(TEST_CLIENT)
|
||||
.secret("secret")
|
||||
.authorizationServicesEnabled(true)
|
||||
|
@ -95,6 +97,8 @@ public class AuthorizationAPITest extends AbstractAuthzTest {
|
|||
.redirectUris("http://localhost/test-client")
|
||||
.directAccessGrants())
|
||||
.build());
|
||||
|
||||
testingClient.testApp().oidcClientEndpoints().setSectorIdentifierRedirectUris(Collections.singletonList("http://localhost/resource-server-test"));
|
||||
}
|
||||
|
||||
@Before
|
||||
|
|
|
@ -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.AuthServer;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
|
@ -142,7 +143,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
|
|||
.authorizationServicesEnabled(true)
|
||||
.redirectUris("http://localhost/resource-server-test")
|
||||
.defaultRoles("uma_protection")
|
||||
.pairwise("http://pairwise.com")
|
||||
.pairwise(TestApplicationResourceUrls.pairwiseSectorIdentifierUri())
|
||||
.directAccessGrants())
|
||||
.client(ClientBuilder.create().clientId(TEST_CLIENT)
|
||||
.secret("secret")
|
||||
|
@ -153,13 +154,19 @@ public class EntitlementAPITest extends AbstractAuthzTest {
|
|||
.secret("secret")
|
||||
.authorizationServicesEnabled(true)
|
||||
.redirectUris("http://localhost/test-client")
|
||||
.pairwise("http://pairwise.com")
|
||||
.pairwise(TestApplicationResourceUrls.pairwiseSectorIdentifierUri())
|
||||
.directAccessGrants())
|
||||
.client(ClientBuilder.create().clientId(PUBLIC_TEST_CLIENT)
|
||||
.secret("secret")
|
||||
.redirectUris("http://localhost:8180/auth/realms/master/app/auth/*", "https://localhost:8543/auth/realms/master/app/auth/*")
|
||||
.publicClient())
|
||||
.build());
|
||||
|
||||
configureSectorIdentifierRedirectUris();
|
||||
}
|
||||
|
||||
private void configureSectorIdentifierRedirectUris() {
|
||||
testingClient.testApp().oidcClientEndpoints().setSectorIdentifierRedirectUris(Arrays.asList("http://localhost/resource-server-test", "http://localhost/test-client"));
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -1936,6 +1943,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
|
|||
controller.stop(suiteContext.getAuthServerInfo().getQualifier());
|
||||
controller.start(suiteContext.getAuthServerInfo().getQualifier());
|
||||
reconnectAdminClient();
|
||||
configureSectorIdentifierRedirectUris();
|
||||
|
||||
TokenIntrospectionResponse introspectionResponse = authzClient.protection().introspectRequestingPartyToken(response.getToken());
|
||||
|
||||
|
|
|
@ -24,13 +24,14 @@ import org.keycloak.client.registration.ClientRegistration;
|
|||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.client.registration.HttpErrorException;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
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 javax.ws.rs.NotFoundException;
|
||||
|
@ -38,11 +39,14 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
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.core.Is.is;
|
||||
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.assertTrue;
|
||||
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>
|
||||
|
@ -169,45 +174,121 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void registerClientValidation() throws IOException {
|
||||
authCreateClients();
|
||||
ClientRepresentation client = buildClient();
|
||||
client.setRootUrl("invalid");
|
||||
|
||||
try {
|
||||
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());
|
||||
}
|
||||
public void testInvalidUrlClientValidation() {
|
||||
testClientUriValidation("Root URL is not a valid URL",
|
||||
"Base URL is not a valid URL",
|
||||
"Backchannel logout URL is not a valid URL",
|
||||
null,
|
||||
"invalid", "myapp://some-fake-app");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateClientValidation() throws IOException, ClientRegistrationException {
|
||||
registerClientAsAdmin();
|
||||
|
||||
ClientRepresentation client = reg.get(CLIENT_ID);
|
||||
client.setRootUrl("invalid");
|
||||
|
||||
try {
|
||||
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());
|
||||
public void testIllegalSchemeClientValidation() {
|
||||
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)/*"
|
||||
);
|
||||
}
|
||||
|
||||
ClientRepresentation updatedClient = reg.get(CLIENT_ID);
|
||||
assertNull(updatedClient.getRootUrl());
|
||||
// KEYCLOAK-3421
|
||||
@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
|
||||
|
|
|
@ -23,6 +23,15 @@ ldapErrorMissingGroupsPathGroup=Groups path group does not exist - please create
|
|||
|
||||
clientRedirectURIsFragmentError=Redirect URIs must not contain an URI 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.
|
||||
pairwiseClientRedirectURIsMissingHost=Client redirect URIs must contain a valid host component.
|
||||
|
|
Loading…
Reference in a new issue