KEYCLOAK-4360: Add OneTimeUse condition to SAMLResponse

Add OneTimeUse Condition to SAMLResponse when configured in client settings
This commit is contained in:
Mark Pardijs 2017-03-07 16:13:50 +01:00
parent 16d5ca3378
commit c78c0b73d3
11 changed files with 217 additions and 60 deletions

View file

@ -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.AudienceRestrictionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.ConditionsType; 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.assertion.SubjectConfirmationDataType;
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.saml.common.PicketLinkLogger; import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory; import org.keycloak.saml.common.PicketLinkLoggerFactory;
@ -41,7 +43,6 @@ import org.w3c.dom.Document;
import java.net.URI; import java.net.URI;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import static org.keycloak.saml.common.util.StringUtil.isNotNull; import static org.keycloak.saml.common.util.StringUtil.isNotNull;
@ -68,7 +69,7 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui
protected String requestIssuer; protected String requestIssuer;
protected String sessionIndex; protected String sessionIndex;
protected final List<NodeGenerator> extensions = new LinkedList<>(); protected final List<NodeGenerator> extensions = new LinkedList<>();
protected boolean includeOneTimeUseCondition;
public SAML2LoginResponseBuilder sessionIndex(String sessionIndex) { public SAML2LoginResponseBuilder sessionIndex(String sessionIndex) {
this.sessionIndex = sessionIndex; this.sessionIndex = sessionIndex;
@ -110,12 +111,12 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui
} }
public SAML2LoginResponseBuilder requestID(String requestID) { public SAML2LoginResponseBuilder requestID(String requestID) {
this.requestID =requestID; this.requestID = requestID;
return this; return this;
} }
public SAML2LoginResponseBuilder requestIssuer(String requestIssuer) { public SAML2LoginResponseBuilder requestIssuer(String requestIssuer) {
this.requestIssuer =requestIssuer; this.requestIssuer = requestIssuer;
return this; return this;
} }
@ -140,6 +141,11 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui
return this; return this;
} }
public SAML2LoginResponseBuilder includeOneTimeUseCondition(boolean includeOneTimeUseCondition) {
this.includeOneTimeUseCondition = includeOneTimeUseCondition;
return this;
}
@Override @Override
public SAML2LoginResponseBuilder addExtension(NodeGenerator extension) { public SAML2LoginResponseBuilder addExtension(NodeGenerator extension) {
this.extensions.add(extension); this.extensions.add(extension);
@ -217,7 +223,11 @@ public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBui
assertion.addStatement(authnStatement); assertion.addStatement(authnStatement);
} }
if (! this.extensions.isEmpty()) { if (includeOneTimeUseCondition) {
assertion.getConditions().addCondition(new OneTimeUseType());
}
if (!this.extensions.isEmpty()) {
ExtensionsType extensionsType = new ExtensionsType(); ExtensionsType extensionsType = new ExtensionsType();
for (NodeGenerator extension : this.extensions) { for (NodeGenerator extension : this.extensions) {
extensionsType.addExtension(extension); extensionsType.addExtension(extension);

View file

@ -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.ConditionsType;
import org.keycloak.dom.saml.v2.assertion.EncryptedElementType; import org.keycloak.dom.saml.v2.assertion.EncryptedElementType;
import org.keycloak.dom.saml.v2.assertion.NameIDType; 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.StatementAbstractType;
import org.keycloak.dom.saml.v2.assertion.SubjectType; import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.assertion.URIType; import org.keycloak.dom.saml.v2.assertion.URIType;
@ -99,7 +100,8 @@ public class SAMLAssertionWriter extends BaseWriter {
} }
if (conditions.getNotOnOrAfter() != null) { 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<ConditionAbstractType> typeOfConditions = conditions.getConditions(); List<ConditionAbstractType> typeOfConditions = conditions.getConditions();
@ -121,6 +123,11 @@ public class SAMLAssertionWriter extends BaseWriter {
StaxUtil.writeEndElement(writer); 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) { if (uriTypes != null) {
for (URIType uriType : uriTypes) { for (URIType uriType : uriTypes) {
if (uriType instanceof AuthnContextDeclType) { if (uriType instanceof AuthnContextDeclType) {
StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.AUTHN_CONTEXT_DECLARATION.get(),
JBossSAMLConstants.AUTHN_CONTEXT_DECLARATION.get(), ASSERTION_NSURI.get()); ASSERTION_NSURI.get());
StaxUtil.writeCharacters(writer, uriType.getValue().toASCIIString()); StaxUtil.writeCharacters(writer, uriType.getValue().toASCIIString());
StaxUtil.writeEndElement(writer); StaxUtil.writeEndElement(writer);
} }

View file

@ -111,6 +111,7 @@ public class SamlClient extends ClientConfigResolver {
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE));
} }
public void setForceNameIDFormat(boolean val) { public void setForceNameIDFormat(boolean val) {
client.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(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)); client.setAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING, Boolean.toString(val));
} }
public boolean requiresAssertionSignature() { public boolean requiresAssertionSignature() {
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE)); return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE));
} }
public void setRequiresAssertionSignature(boolean val) { 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() { 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) { 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); client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, val);
} }
public String getClientEncryptingPrivateKey() { public String getClientEncryptingPrivateKey() {
return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE); return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE);
} }
@ -203,19 +207,29 @@ public class SamlClient extends ClientConfigResolver {
/** /**
* Always returns non-{@code null} result. * Always returns non-{@code null} result.
*
* @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set. * @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set.
*/ */
public XmlKeyInfoKeyNameTransformer getXmlSigKeyInfoKeyNameTransformer() { public XmlKeyInfoKeyNameTransformer getXmlSigKeyInfoKeyNameTransformer() {
return XmlKeyInfoKeyNameTransformer.from( return XmlKeyInfoKeyNameTransformer.from(
client.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER), client.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER); DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
} }
public void setXmlSigKeyInfoKeyNameTransformer(XmlKeyInfoKeyNameTransformer xmlSigKeyInfoKeyNameTransformer) { public void setXmlSigKeyInfoKeyNameTransformer(XmlKeyInfoKeyNameTransformer xmlSigKeyInfoKeyNameTransformer) {
client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER,
xmlSigKeyInfoKeyNameTransformer == null xmlSigKeyInfoKeyNameTransformer == null
? null ? null
: xmlSigKeyInfoKeyNameTransformer.name()); : 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));
}
} }

View file

@ -29,6 +29,7 @@ public interface SamlConfigAttributes {
String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm"; String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
String SAML_NAME_ID_FORMAT_ATTRIBUTE = "saml_name_id_format"; String SAML_NAME_ID_FORMAT_ATTRIBUTE = "saml_name_id_format";
String SAML_AUTHNSTATEMENT = "saml.authnstatement"; 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_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format";
String SAML_SERVER_SIGNATURE = "saml.server.signature"; String SAML_SERVER_SIGNATURE = "saml.server.signature";
String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext"; String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";

View file

@ -25,7 +25,6 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; 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.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer; import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
@ -81,8 +80,6 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -382,6 +379,8 @@ public class SamlProtocol implements LoginProtocol {
builder.disableAuthnStatement(true); builder.disableAuthnStatement(true);
} }
builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());
List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>(); List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>();
List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>(); List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper = null; ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper = null;

View file

@ -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<ClientRepresentation> 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<ConditionAbstractType> conditions = conditionsType.getConditions();
final Collection<ConditionAbstractType> oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType);
final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty();
assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded));
}
}

View file

@ -1,5 +1,9 @@
package org.keycloak.testsuite.console.page.clients.settings; 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.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.console.page.clients.CreateClientForm; import org.keycloak.testsuite.console.page.clients.CreateClientForm;
import org.keycloak.testsuite.console.page.fragment.OnOffSwitch; 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.FindBy;
import org.openqa.selenium.support.ui.Select; 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.pause;
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement; import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
@ -68,10 +68,8 @@ public class ClientSettingsForm extends CreateClientForm {
private List<WebElement> deleteWebOriginIcons; private List<WebElement> deleteWebOriginIcons;
public enum OidcAccessType { public enum OidcAccessType {
BEARER_ONLY("bearer-only"), BEARER_ONLY("bearer-only"), PUBLIC("public"), CONFIDENTIAL("confidential");
PUBLIC("public"),
CONFIDENTIAL("confidential");
private final String name; private final String name;
private OidcAccessType(String name) { private OidcAccessType(String name) {
@ -82,7 +80,7 @@ public class ClientSettingsForm extends CreateClientForm {
return name; return name;
} }
} }
public void setBaseUrl(String baseUrl) { public void setBaseUrl(String baseUrl) {
setInputValue(baseUrlInput, baseUrl); setInputValue(baseUrlInput, baseUrl);
} }
@ -213,28 +211,31 @@ public class ClientSettingsForm extends CreateClientForm {
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
serviceAccountsEnabledSwitch.setOn(serviceAccountsEnabled); serviceAccountsEnabledSwitch.setOn(serviceAccountsEnabled);
} }
public class SAMLClientSettingsForm extends Form { public class SAMLClientSettingsForm extends Form {
public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature"; public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
public static final String SAML_AUTHNSTATEMENT = "saml.authnstatement"; public static final String SAML_AUTHNSTATEMENT = "saml.authnstatement";
public static final String SAML_CLIENT_SIGNATURE = "saml.client.signature"; public static final String SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition";
public static final String SAML_ENCRYPT = "saml.encrypt"; public static final String SAML_CLIENT_SIGNATURE = "saml.client.signature";
public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding"; public static final String SAML_ENCRYPT = "saml.encrypt";
public static final String SAML_MULTIVALUED_ROLES = "saml.multivalued.roles"; public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature"; public static final String SAML_MULTIVALUED_ROLES = "saml.multivalued.roles";
public static final String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext"; public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm"; public static final String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
public static final String SAML_ASSERTION_CONSUMER_URL_POST = "saml_assertion_consumer_url_post"; public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT = "saml_assertion_consumer_url_redirect"; public static final String SAML_ASSERTION_CONSUMER_URL_POST = "saml_assertion_consumer_url_post";
public static final String SAML_FORCE_NAME_ID_FORMAT = "saml_force_name_id_format"; public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT = "saml_assertion_consumer_url_redirect";
public static final String SAML_NAME_ID_FORMAT = "saml_name_id_format"; public static final String SAML_FORCE_NAME_ID_FORMAT = "saml_force_name_id_format";
public static final String SAML_SIGNATURE_CANONICALIZATION_METHOD = "saml_signature_canonicalization_method"; public static final String SAML_NAME_ID_FORMAT = "saml_name_id_format";
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST = "saml_single_logout_service_url_post"; public static final String SAML_SIGNATURE_CANONICALIZATION_METHOD = "saml_signature_canonicalization_method";
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT = "saml_single_logout_service_url_redirect"; 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']]") @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlAuthnStatement']]")
private OnOffSwitch 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']]") @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlServerSignature']]")
private OnOffSwitch samlServerSignature; private OnOffSwitch samlServerSignature;
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlServerSignatureEnableKeyInfoExtension']]") @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlServerSignatureEnableKeyInfoExtension']]")
@ -257,10 +258,10 @@ public class ClientSettingsForm extends CreateClientForm {
private OnOffSwitch samlForceNameIdFormat; private OnOffSwitch samlForceNameIdFormat;
@FindBy(id = "samlNameIdFormat") @FindBy(id = "samlNameIdFormat")
private Select samlNameIdFormat; private Select samlNameIdFormat;
@FindBy(xpath = "//fieldset[contains(@data-ng-show, 'saml')]//i") @FindBy(xpath = "//fieldset[contains(@data-ng-show, 'saml')]//i")
private WebElement fineGrainCollapsor; private WebElement fineGrainCollapsor;
@FindBy(id = "consumerServicePost") @FindBy(id = "consumerServicePost")
private WebElement consumerServicePostInput; private WebElement consumerServicePostInput;
@FindBy(id = "consumerServiceRedirect") @FindBy(id = "consumerServiceRedirect")
@ -269,12 +270,13 @@ public class ClientSettingsForm extends CreateClientForm {
private WebElement logoutPostBindingInput; private WebElement logoutPostBindingInput;
@FindBy(id = "logoutRedirectBinding") @FindBy(id = "logoutRedirectBinding")
private WebElement logoutRedirectBindingInput; private WebElement logoutRedirectBindingInput;
public void setValues(ClientRepresentation client) { public void setValues(ClientRepresentation client) {
waitUntilElement(fineGrainCollapsor).is().visible(); waitUntilElement(fineGrainCollapsor).is().visible();
Map<String, String> attributes = client.getAttributes(); Map<String, String> attributes = client.getAttributes();
samlAuthnStatement.setOn("true".equals(attributes.get(SAML_AUTHNSTATEMENT))); 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))); samlServerSignature.setOn("true".equals(attributes.get(SAML_SERVER_SIGNATURE)));
samlAssertionSignature.setOn("true".equals(attributes.get(SAML_ASSERTION_SIGNATURE))); samlAssertionSignature.setOn("true".equals(attributes.get(SAML_ASSERTION_SIGNATURE)));
if (samlServerSignature.isOn() || samlAssertionSignature.isOn()) { if (samlServerSignature.isOn() || samlAssertionSignature.isOn()) {
@ -288,10 +290,10 @@ public class ClientSettingsForm extends CreateClientForm {
frontchannelLogout.setOn(client.isFrontchannelLogout()); frontchannelLogout.setOn(client.isFrontchannelLogout());
samlForceNameIdFormat.setOn("true".equals(attributes.get(SAML_FORCE_NAME_ID_FORMAT))); samlForceNameIdFormat.setOn("true".equals(attributes.get(SAML_FORCE_NAME_ID_FORMAT)));
samlNameIdFormat.selectByVisibleText(attributes.get(SAML_NAME_ID_FORMAT)); samlNameIdFormat.selectByVisibleText(attributes.get(SAML_NAME_ID_FORMAT));
fineGrainCollapsor.click(); fineGrainCollapsor.click();
waitUntilElement(consumerServicePostInput).is().present(); waitUntilElement(consumerServicePostInput).is().present();
setInputValue(consumerServicePostInput, attributes.get(SAML_ASSERTION_CONSUMER_URL_POST)); setInputValue(consumerServicePostInput, attributes.get(SAML_ASSERTION_CONSUMER_URL_POST));
setInputValue(consumerServiceRedirectInput, attributes.get(SAML_ASSERTION_CONSUMER_URL_REDIRECT)); setInputValue(consumerServiceRedirectInput, attributes.get(SAML_ASSERTION_CONSUMER_URL_REDIRECT));
setInputValue(logoutPostBindingInput, attributes.get(SAML_SINGLE_LOGOUT_SERVICE_URL_POST)); setInputValue(logoutPostBindingInput, attributes.get(SAML_SINGLE_LOGOUT_SERVICE_URL_POST));

View file

@ -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_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_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_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_SERVER_SIGNATURE;
import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SIGNATURE_ALGORITHM; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SIGNATURE_ALGORITHM;
import static org.keycloak.testsuite.util.AttributesAssert.assertEqualsBooleanAttributes; 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 abstract class AbstractClientTest extends AbstractConsoleTest {
public final String TEST_CLIENT_ID = "test-client"; public final String TEST_CLIENT_ID = "test-client";
public final List<String> TEST_REDIRECT_URIs = Arrays.asList(new String[]{"http://example.test/app/"}); public final List<String> TEST_REDIRECT_URIs = Arrays.asList(new String[] { "http://example.test/app/" });
@Page @Page
protected Clients clientsPage; protected Clients clientsPage;
@ -65,12 +66,12 @@ public abstract class AbstractClientTest extends AbstractConsoleTest {
client.setClientId(clientId); client.setClientId(clientId);
client.setEnabled(true); client.setEnabled(true);
client.setProtocol(protocol); client.setProtocol(protocol);
client.setDirectAccessGrantsEnabled(true); client.setDirectAccessGrantsEnabled(true);
client.setFullScopeAllowed(true); client.setFullScopeAllowed(true);
client.setPublicClient(true); client.setPublicClient(true);
client.setStandardFlowEnabled(true); client.setStandardFlowEnabled(true);
if (protocol.equals(SAML)) { if (protocol.equals(SAML)) {
client.setAttributes(getSAMLAttributes()); client.setAttributes(getSAMLAttributes());
} }
@ -86,6 +87,7 @@ public abstract class AbstractClientTest extends AbstractConsoleTest {
attributes.put(SAML_SIGNATURE_ALGORITHM, "RSA_SHA256"); attributes.put(SAML_SIGNATURE_ALGORITHM, "RSA_SHA256");
attributes.put(SAML_FORCE_NAME_ID_FORMAT, "false"); attributes.put(SAML_FORCE_NAME_ID_FORMAT, "false");
attributes.put(SAML_NAME_ID_FORMAT, "username"); attributes.put(SAML_NAME_ID_FORMAT, "username");
attributes.put(SAML_ONETIMEUSE_CONDITION, "true");
return attributes; return attributes;
} }
@ -128,7 +130,8 @@ public abstract class AbstractClientTest extends AbstractConsoleTest {
public ProtocolMapperRepresentation findClientMapperByName(String clientId, String mapperName) { public ProtocolMapperRepresentation findClientMapperByName(String clientId, String mapperName) {
ProtocolMapperRepresentation found = null; 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())) { if (mapperName.equals(mapper.getName())) {
found = mapper; found = mapper;
} }

View file

@ -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. 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=Include AuthnStatement
include-authnstatement.tooltip=Should a statement specifying the method and timestamp be included in login responses? 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=Sign Documents
sign-documents.tooltip=Should SAML documents be signed by the realm? sign-documents.tooltip=Should SAML documents be signed by the realm?
sign-documents-redirect-enable-key-info-ext=Optimize REDIRECT signing key lookup sign-documents-redirect-enable-key-info-ext=Optimize REDIRECT signing key lookup

View file

@ -863,6 +863,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.realm = realm; $scope.realm = realm;
$scope.samlAuthnStatement = false; $scope.samlAuthnStatement = false;
$scope.samlOneTimeUseCondition = false;
$scope.samlMultiValuedRoles = false; $scope.samlMultiValuedRoles = false;
$scope.samlServerSignature = false; $scope.samlServerSignature = false;
$scope.samlServerSignatureEnableKeyInfoExtension = false; $scope.samlServerSignatureEnableKeyInfoExtension = false;
@ -959,6 +960,13 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.samlAuthnStatement = false; $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"]) {
if ($scope.client.attributes["saml_force_name_id_format"] == "true") { if ($scope.client.attributes["saml_force_name_id_format"] == "true") {
$scope.samlForceNameIdFormat = true; $scope.samlForceNameIdFormat = true;
@ -1177,6 +1185,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.clientEdit.attributes["saml.authnstatement"] = "false"; $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) { if ($scope.samlForceNameIdFormat == true) {
$scope.clientEdit.attributes["saml_force_name_id_format"] = "true"; $scope.clientEdit.attributes["saml_force_name_id_format"] = "true";
} else { } else {
@ -1380,7 +1394,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
return ($scope.client.useTemplateScope && $scope.template && template.fullScopeAllowed) return ($scope.client.useTemplateScope && $scope.template && template.fullScopeAllowed)
|| (!$scope.template && $scope.client.fullScopeAllowed); || (!$scope.template && $scope.client.fullScopeAllowed);
} }
$scope.changeFlag = function() { $scope.changeFlag = function() {
Client.update({ Client.update({
realm : realm.realm, 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) { $location, $routeParams, Notifications, Dialog) {
$scope.client = client; $scope.client = client;
$scope.realm = realm; $scope.realm = realm;
@ -1852,7 +1866,7 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv
changed: false, changed: false,
mapperTypes: serverInfo.protocolMapperTypes[protocol] mapperTypes: serverInfo.protocolMapperTypes[protocol]
} }
$scope.model.mapperType = $scope.model.mapperTypes[0]; $scope.model.mapperType = $scope.model.mapperTypes[0];
$scope.$watch(function() { $scope.$watch(function() {
@ -2130,7 +2144,7 @@ module.controller('ClientTemplateProtocolMapperCreateCtrl', function($scope, rea
changed: false, changed: false,
mapperTypes: serverInfo.protocolMapperTypes[protocol] mapperTypes: serverInfo.protocolMapperTypes[protocol]
} }
$scope.model.mapperType = $scope.model.mapperTypes[0]; $scope.model.mapperType = $scope.model.mapperTypes[0];
$scope.$watch(function() { $scope.$watch(function() {

View file

@ -124,6 +124,13 @@
</div> </div>
<kc-tooltip>{{:: 'include-authnstatement.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'include-authnstatement.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlOneTimeUseCondition">{{:: 'include-onetimeuse-condition' | translate}}</label>
<div class="col-sm-6">
<input ng-model="samlOneTimeUseCondition" ng-click="switchChange()" name="samlOneTimeUseCondition" id="samlOneTimeUseCondition" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'include-onetimeuse-condition.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'"> <div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlServerSignature">{{:: 'sign-documents' | translate}}</label> <label class="col-md-2 control-label" for="samlServerSignature">{{:: 'sign-documents' | translate}}</label>
<div class="col-sm-6"> <div class="col-sm-6">