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.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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue