diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java index 17dafc737f..c3241c2e66 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java +++ b/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java @@ -21,7 +21,9 @@ import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.ConditionsType; +import org.keycloak.dom.saml.v2.assertion.OneTimeUseType; import org.keycloak.dom.saml.v2.assertion.SubjectConfirmationDataType; +import org.keycloak.dom.saml.v2.protocol.ExtensionsType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.saml.common.PicketLinkLogger; import org.keycloak.saml.common.PicketLinkLoggerFactory; @@ -41,7 +43,6 @@ import org.w3c.dom.Document; import java.net.URI; import java.util.LinkedList; import java.util.List; -import org.keycloak.dom.saml.v2.protocol.ExtensionsType; import static org.keycloak.saml.common.util.StringUtil.isNotNull; @@ -68,7 +69,7 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui protected String requestIssuer; protected String sessionIndex; protected final List extensions = new LinkedList<>(); - + protected boolean includeOneTimeUseCondition; public SAML2LoginResponseBuilder sessionIndex(String sessionIndex) { this.sessionIndex = sessionIndex; @@ -110,12 +111,12 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui } public SAML2LoginResponseBuilder requestID(String requestID) { - this.requestID =requestID; + this.requestID = requestID; return this; } public SAML2LoginResponseBuilder requestIssuer(String requestIssuer) { - this.requestIssuer =requestIssuer; + this.requestIssuer = requestIssuer; return this; } @@ -140,6 +141,11 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui return this; } + public SAML2LoginResponseBuilder includeOneTimeUseCondition(boolean includeOneTimeUseCondition) { + this.includeOneTimeUseCondition = includeOneTimeUseCondition; + return this; + } + @Override public SAML2LoginResponseBuilder addExtension(NodeGenerator extension) { this.extensions.add(extension); @@ -217,7 +223,11 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui assertion.addStatement(authnStatement); } - if (! this.extensions.isEmpty()) { + if (includeOneTimeUseCondition) { + assertion.getConditions().addCondition(new OneTimeUseType()); + } + + if (!this.extensions.isEmpty()) { ExtensionsType extensionsType = new ExtensionsType(); for (NodeGenerator extension : this.extensions) { extensionsType.addExtension(extension); diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java index 7ece1c7fb1..867aceb2fd 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java @@ -31,6 +31,7 @@ import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType; import org.keycloak.dom.saml.v2.assertion.ConditionsType; import org.keycloak.dom.saml.v2.assertion.EncryptedElementType; import org.keycloak.dom.saml.v2.assertion.NameIDType; +import org.keycloak.dom.saml.v2.assertion.OneTimeUseType; import org.keycloak.dom.saml.v2.assertion.StatementAbstractType; import org.keycloak.dom.saml.v2.assertion.SubjectType; import org.keycloak.dom.saml.v2.assertion.URIType; @@ -99,7 +100,8 @@ public class SAMLAssertionWriter extends BaseWriter { } if (conditions.getNotOnOrAfter() != null) { - StaxUtil.writeAttribute(writer, JBossSAMLConstants.NOT_ON_OR_AFTER.get(), conditions.getNotOnOrAfter().toString()); + StaxUtil.writeAttribute(writer, JBossSAMLConstants.NOT_ON_OR_AFTER.get(), + conditions.getNotOnOrAfter().toString()); } List typeOfConditions = conditions.getConditions(); @@ -121,6 +123,11 @@ public class SAMLAssertionWriter extends BaseWriter { StaxUtil.writeEndElement(writer); } + if (typeCondition instanceof OneTimeUseType) { + StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.ONE_TIME_USE.get(), + ASSERTION_NSURI.get()); + StaxUtil.writeEndElement(writer); + } } } @@ -237,8 +244,8 @@ public class SAMLAssertionWriter extends BaseWriter { if (uriTypes != null) { for (URIType uriType : uriTypes) { if (uriType instanceof AuthnContextDeclType) { - StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, - JBossSAMLConstants.AUTHN_CONTEXT_DECLARATION.get(), ASSERTION_NSURI.get()); + StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.AUTHN_CONTEXT_DECLARATION.get(), + ASSERTION_NSURI.get()); StaxUtil.writeCharacters(writer, uriType.getValue().toASCIIString()); StaxUtil.writeEndElement(writer); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java index fd6c2d0279..38d253d9fe 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java @@ -111,6 +111,7 @@ public class SamlClient extends ClientConfigResolver { return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); } + public void setForceNameIDFormat(boolean val) { client.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val)); } @@ -139,16 +140,18 @@ public class SamlClient extends ClientConfigResolver { client.setAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING, Boolean.toString(val)); } + public boolean requiresAssertionSignature() { - return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE)); } public void setRequiresAssertionSignature(boolean val) { - client.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE , Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, Boolean.toString(val)); } + public boolean requiresEncryption() { - return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ENCRYPT)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ENCRYPT)); } @@ -162,7 +165,7 @@ public class SamlClient extends ClientConfigResolver { } public void setRequiresClientSignature(boolean val) { - client.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE , Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, Boolean.toString(val)); } @@ -192,6 +195,7 @@ public class SamlClient extends ClientConfigResolver { client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, val); } + public String getClientEncryptingPrivateKey() { return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE); } @@ -203,19 +207,29 @@ public class SamlClient extends ClientConfigResolver { /** * Always returns non-{@code null} result. + * * @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set. */ public XmlKeyInfoKeyNameTransformer getXmlSigKeyInfoKeyNameTransformer() { return XmlKeyInfoKeyNameTransformer.from( - client.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER), - DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER); + client.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER), + DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER); } public void setXmlSigKeyInfoKeyNameTransformer(XmlKeyInfoKeyNameTransformer xmlSigKeyInfoKeyNameTransformer) { client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, - xmlSigKeyInfoKeyNameTransformer == null - ? null - : xmlSigKeyInfoKeyNameTransformer.name()); + xmlSigKeyInfoKeyNameTransformer == null + ? null + : xmlSigKeyInfoKeyNameTransformer.name()); } + public boolean includeOneTimeUseCondition() { + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION)); + } + + public void setIncludeOneTimeUseCondition(boolean val) { + client.setAttribute(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, Boolean.toString(val)); + } + + } diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java index d41a414b01..ef700268d0 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java @@ -29,6 +29,7 @@ public interface SamlConfigAttributes { String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm"; String SAML_NAME_ID_FORMAT_ATTRIBUTE = "saml_name_id_format"; String SAML_AUTHNSTATEMENT = "saml.authnstatement"; + String SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition"; String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format"; String SAML_SERVER_SIGNATURE = "saml.server.signature"; String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext"; diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 6759fb6bf8..d1750d2047 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -25,7 +25,6 @@ import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.jboss.logging.Logger; - import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; @@ -56,12 +55,12 @@ import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer; +import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; import org.keycloak.services.ErrorPage; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.RealmsResource; - import org.w3c.dom.Document; import javax.ws.rs.core.HttpHeaders; @@ -81,8 +80,6 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; -import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; - /** * @author Bill Burke * @version $Revision: 1 $ @@ -382,6 +379,8 @@ public class SamlProtocol implements LoginProtocol { builder.disableAuthnStatement(true); } + builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition()); + List> attributeStatementMappers = new LinkedList<>(); List> loginResponseMappers = new LinkedList<>(); ProtocolMapperProcessor roleListMapper = null; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java new file mode 100644 index 0000000000..cec6e1a6bb --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017 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.testsuite.saml; + +import com.google.common.collect.Collections2; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType; +import org.keycloak.dom.saml.v2.assertion.ConditionsType; +import org.keycloak.dom.saml.v2.assertion.OneTimeUseType; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.protocol.saml.SamlConfigAttributes; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; +import org.keycloak.testsuite.util.SamlClient; +import org.w3c.dom.Document; + +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.util.SamlClient.login; + +/** + * KEYCLOAK-4360 + * @author mrpardijs + */ +public class IncludeOneTimeUseConditionTest extends AbstractSamlTest +{ + @Test + public void testOneTimeUseConditionIsAdded() throws Exception + { + testOneTimeUseConditionIncluded(Boolean.TRUE); + } + + @Test + public void testOneTimeUseConditionIsNotAdded() throws Exception + { + testOneTimeUseConditionIncluded(Boolean.FALSE); + } + + private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws ProcessingException, ConfigurationException, ParsingException + { + ClientsResource clients = adminClient.realm(REALM_NAME).clients(); + List foundClients = clients.findByClientId(SAML_CLIENT_ID_SALES_POST); + assertThat(foundClients, hasSize(1)); + ClientResource clientRes = clients.get(foundClients.get(0).getId()); + ClientRepresentation client = clientRes.toRepresentation(); + client.getAttributes().put(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString()); + clientRes.update(client); + + AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); + loginRep.setProtocolBinding(SamlClient.Binding.POST.getBindingUri()); + + Document samlRequest = SAML2Request.convert(loginRep); + SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, SamlClient.Binding.POST, + SamlClient.Binding.POST); + + assertThat(res.getSamlObject(), notNullValue()); + assertThat(res.getSamlObject(), instanceOf(ResponseType.class)); + + ResponseType rt = (ResponseType) res.getSamlObject(); + assertThat(rt.getAssertions(), not(empty())); + final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions(); + assertThat(conditionsType, notNullValue()); + assertThat(conditionsType.getConditions(), not(empty())); + + final List conditions = conditionsType.getConditions(); + + final Collection oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType); + + final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty(); + assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded)); + } + + +} diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java index b8cbf4dfe0..a1d225a960 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java @@ -1,5 +1,9 @@ package org.keycloak.testsuite.console.page.clients.settings; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.testsuite.console.page.clients.CreateClientForm; import org.keycloak.testsuite.console.page.fragment.OnOffSwitch; @@ -9,10 +13,6 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.ui.Select; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import static org.keycloak.testsuite.util.WaitUtils.pause; import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement; @@ -68,10 +68,8 @@ public class ClientSettingsForm extends CreateClientForm { private List deleteWebOriginIcons; public enum OidcAccessType { - BEARER_ONLY("bearer-only"), - PUBLIC("public"), - CONFIDENTIAL("confidential"); - + BEARER_ONLY("bearer-only"), PUBLIC("public"), CONFIDENTIAL("confidential"); + private final String name; private OidcAccessType(String name) { @@ -82,7 +80,7 @@ public class ClientSettingsForm extends CreateClientForm { return name; } } - + public void setBaseUrl(String baseUrl) { setInputValue(baseUrlInput, baseUrl); } @@ -213,28 +211,31 @@ public class ClientSettingsForm extends CreateClientForm { public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { serviceAccountsEnabledSwitch.setOn(serviceAccountsEnabled); } - + public class SAMLClientSettingsForm extends Form { public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature"; public static final String SAML_AUTHNSTATEMENT = "saml.authnstatement"; - public static final String SAML_CLIENT_SIGNATURE = "saml.client.signature"; - public static final String SAML_ENCRYPT = "saml.encrypt"; - public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding"; - public static final String SAML_MULTIVALUED_ROLES = "saml.multivalued.roles"; - public static final String SAML_SERVER_SIGNATURE = "saml.server.signature"; - public static final String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext"; - public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm"; - public static final String SAML_ASSERTION_CONSUMER_URL_POST = "saml_assertion_consumer_url_post"; - public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT = "saml_assertion_consumer_url_redirect"; - public static final String SAML_FORCE_NAME_ID_FORMAT = "saml_force_name_id_format"; - public static final String SAML_NAME_ID_FORMAT = "saml_name_id_format"; - public static final String SAML_SIGNATURE_CANONICALIZATION_METHOD = "saml_signature_canonicalization_method"; - public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST = "saml_single_logout_service_url_post"; - public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT = "saml_single_logout_service_url_redirect"; - + public static final String SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition"; + public static final String SAML_CLIENT_SIGNATURE = "saml.client.signature"; + public static final String SAML_ENCRYPT = "saml.encrypt"; + public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding"; + public static final String SAML_MULTIVALUED_ROLES = "saml.multivalued.roles"; + public static final String SAML_SERVER_SIGNATURE = "saml.server.signature"; + public static final String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext"; + public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm"; + public static final String SAML_ASSERTION_CONSUMER_URL_POST = "saml_assertion_consumer_url_post"; + public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT = "saml_assertion_consumer_url_redirect"; + public static final String SAML_FORCE_NAME_ID_FORMAT = "saml_force_name_id_format"; + public static final String SAML_NAME_ID_FORMAT = "saml_name_id_format"; + public static final String SAML_SIGNATURE_CANONICALIZATION_METHOD = "saml_signature_canonicalization_method"; + public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST = "saml_single_logout_service_url_post"; + public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT = "saml_single_logout_service_url_redirect"; + @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlAuthnStatement']]") private OnOffSwitch samlAuthnStatement; + @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlOneTimeUseCondition']]") + private OnOffSwitch samlOneTimeUseCondition; @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlServerSignature']]") private OnOffSwitch samlServerSignature; @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlServerSignatureEnableKeyInfoExtension']]") @@ -257,10 +258,10 @@ public class ClientSettingsForm extends CreateClientForm { private OnOffSwitch samlForceNameIdFormat; @FindBy(id = "samlNameIdFormat") private Select samlNameIdFormat; - + @FindBy(xpath = "//fieldset[contains(@data-ng-show, 'saml')]//i") private WebElement fineGrainCollapsor; - + @FindBy(id = "consumerServicePost") private WebElement consumerServicePostInput; @FindBy(id = "consumerServiceRedirect") @@ -269,12 +270,13 @@ public class ClientSettingsForm extends CreateClientForm { private WebElement logoutPostBindingInput; @FindBy(id = "logoutRedirectBinding") private WebElement logoutRedirectBindingInput; - + public void setValues(ClientRepresentation client) { waitUntilElement(fineGrainCollapsor).is().visible(); - + Map attributes = client.getAttributes(); samlAuthnStatement.setOn("true".equals(attributes.get(SAML_AUTHNSTATEMENT))); + samlOneTimeUseCondition.setOn("true".equals(attributes.get(SAML_ONETIMEUSE_CONDITION))); samlServerSignature.setOn("true".equals(attributes.get(SAML_SERVER_SIGNATURE))); samlAssertionSignature.setOn("true".equals(attributes.get(SAML_ASSERTION_SIGNATURE))); if (samlServerSignature.isOn() || samlAssertionSignature.isOn()) { @@ -288,10 +290,10 @@ public class ClientSettingsForm extends CreateClientForm { frontchannelLogout.setOn(client.isFrontchannelLogout()); samlForceNameIdFormat.setOn("true".equals(attributes.get(SAML_FORCE_NAME_ID_FORMAT))); samlNameIdFormat.selectByVisibleText(attributes.get(SAML_NAME_ID_FORMAT)); - + fineGrainCollapsor.click(); waitUntilElement(consumerServicePostInput).is().present(); - + setInputValue(consumerServicePostInput, attributes.get(SAML_ASSERTION_CONSUMER_URL_POST)); setInputValue(consumerServiceRedirectInput, attributes.get(SAML_ASSERTION_CONSUMER_URL_REDIRECT)); setInputValue(logoutPostBindingInput, attributes.get(SAML_SINGLE_LOGOUT_SERVICE_URL_POST)); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java index 887e708b86..559ba28823 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java @@ -24,6 +24,7 @@ import static org.keycloak.testsuite.console.page.clients.settings.ClientSetting import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_FORCE_NAME_ID_FORMAT; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_FORCE_POST_BINDING; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_NAME_ID_FORMAT; +import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ONETIMEUSE_CONDITION; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SERVER_SIGNATURE; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SIGNATURE_ALGORITHM; import static org.keycloak.testsuite.util.AttributesAssert.assertEqualsBooleanAttributes; @@ -38,7 +39,7 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; public abstract class AbstractClientTest extends AbstractConsoleTest { public final String TEST_CLIENT_ID = "test-client"; - public final List TEST_REDIRECT_URIs = Arrays.asList(new String[]{"http://example.test/app/"}); + public final List TEST_REDIRECT_URIs = Arrays.asList(new String[] { "http://example.test/app/" }); @Page protected Clients clientsPage; @@ -65,12 +66,12 @@ public abstract class AbstractClientTest extends AbstractConsoleTest { client.setClientId(clientId); client.setEnabled(true); client.setProtocol(protocol); - + client.setDirectAccessGrantsEnabled(true); client.setFullScopeAllowed(true); client.setPublicClient(true); client.setStandardFlowEnabled(true); - + if (protocol.equals(SAML)) { client.setAttributes(getSAMLAttributes()); } @@ -86,6 +87,7 @@ public abstract class AbstractClientTest extends AbstractConsoleTest { attributes.put(SAML_SIGNATURE_ALGORITHM, "RSA_SHA256"); attributes.put(SAML_FORCE_NAME_ID_FORMAT, "false"); attributes.put(SAML_NAME_ID_FORMAT, "username"); + attributes.put(SAML_ONETIMEUSE_CONDITION, "true"); return attributes; } @@ -128,7 +130,8 @@ public abstract class AbstractClientTest extends AbstractConsoleTest { public ProtocolMapperRepresentation findClientMapperByName(String clientId, String mapperName) { ProtocolMapperRepresentation found = null; - for (ProtocolMapperRepresentation mapper : testRealmResource().clients().get(clientId).getProtocolMappers().getMappers()) { + for (ProtocolMapperRepresentation mapper : testRealmResource().clients().get(clientId).getProtocolMappers() + .getMappers()) { if (mapperName.equals(mapper.getName())) { found = mapper; } diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 05f7f0411a..6bf9fef794 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -226,6 +226,8 @@ service-accounts-enabled=Service Accounts Enabled service-accounts-enabled.tooltip=Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of 'Client Credentials Grant' for this client. include-authnstatement=Include AuthnStatement include-authnstatement.tooltip=Should a statement specifying the method and timestamp be included in login responses? +include-onetimeuse-condition=Include OneTimeUse Condition +include-onetimeuse-condition.tooltip=Should a OneTimeUse Condition be included in login responses? sign-documents=Sign Documents sign-documents.tooltip=Should SAML documents be signed by the realm? sign-documents-redirect-enable-key-info-ext=Optimize REDIRECT signing key lookup diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index f1ebe4f6d5..acb7f8651e 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -863,6 +863,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $scope.realm = realm; $scope.samlAuthnStatement = false; + $scope.samlOneTimeUseCondition = false; $scope.samlMultiValuedRoles = false; $scope.samlServerSignature = false; $scope.samlServerSignatureEnableKeyInfoExtension = false; @@ -959,6 +960,13 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $scope.samlAuthnStatement = false; } } + if ($scope.client.attributes["saml.onetimeuse.condition"]) { + if ($scope.client.attributes["saml.onetimeuse.condition"] == "true") { + $scope.samlOneTimeUseCondition = true; + } else { + $scope.samlOneTimeUseCondition = false; + } + } if ($scope.client.attributes["saml_force_name_id_format"]) { if ($scope.client.attributes["saml_force_name_id_format"] == "true") { $scope.samlForceNameIdFormat = true; @@ -1177,6 +1185,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $scope.clientEdit.attributes["saml.authnstatement"] = "false"; } + if ($scope.samlOneTimeUseCondition == true) { + $scope.clientEdit.attributes["saml.onetimeuse.condition"] = "true"; + } else { + $scope.clientEdit.attributes["saml.onetimeuse.condition"] = "false"; + + } if ($scope.samlForceNameIdFormat == true) { $scope.clientEdit.attributes["saml_force_name_id_format"] = "true"; } else { @@ -1380,7 +1394,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien return ($scope.client.useTemplateScope && $scope.template && template.fullScopeAllowed) || (!$scope.template && $scope.client.fullScopeAllowed); } - + $scope.changeFlag = function() { Client.update({ realm : realm.realm, @@ -1587,7 +1601,7 @@ module.controller('ClientClusteringCtrl', function($scope, client, Client, Clien }; }); -module.controller('ClientClusteringNodeCtrl', function($scope, client, Client, ClientClusterNode, realm, +module.controller('ClientClusteringNodeCtrl', function($scope, client, Client, ClientClusterNode, realm, $location, $routeParams, Notifications, Dialog) { $scope.client = client; $scope.realm = realm; @@ -1852,7 +1866,7 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv changed: false, mapperTypes: serverInfo.protocolMapperTypes[protocol] } - + $scope.model.mapperType = $scope.model.mapperTypes[0]; $scope.$watch(function() { @@ -2130,7 +2144,7 @@ module.controller('ClientTemplateProtocolMapperCreateCtrl', function($scope, rea changed: false, mapperTypes: serverInfo.protocolMapperTypes[protocol] } - + $scope.model.mapperType = $scope.model.mapperTypes[0]; $scope.$watch(function() { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index 4ceb7cb9a7..3eb084f44d 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -124,6 +124,13 @@ {{:: 'include-authnstatement.tooltip' | translate}} +
+ +
+ +
+ {{:: 'include-onetimeuse-condition.tooltip' | translate}} +