KEYCLOAK-4360: Add OneTimeUse condition to SAMLResponse
Add OneTimeUse Condition to SAMLResponse when configured in client settings
This commit is contained in:
parent
16d5ca3378
commit
c78c0b73d3
11 changed files with 217 additions and 60 deletions
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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,9 +68,7 @@ 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;
|
||||||
|
|
||||||
|
@ -218,23 +216,26 @@ public class ClientSettingsForm extends CreateClientForm {
|
||||||
|
|
||||||
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']]")
|
||||||
|
@ -275,6 +276,7 @@ public class ClientSettingsForm extends CreateClientForm {
|
||||||
|
|
||||||
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()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue