KEYCLOAK-16592 Do not require destination with SOAP binding
This commit is contained in:
parent
852593310f
commit
fc29a39e5a
11 changed files with 538 additions and 31 deletions
|
@ -240,6 +240,10 @@ public class SAMLEndpoint {
|
||||||
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
||||||
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
||||||
|
|
||||||
|
protected boolean isDestinationRequired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected KeyLocator getIDPKeyLocator() {
|
protected KeyLocator getIDPKeyLocator() {
|
||||||
List<Key> keys = new LinkedList<>();
|
List<Key> keys = new LinkedList<>();
|
||||||
|
|
||||||
|
@ -271,7 +275,8 @@ public class SAMLEndpoint {
|
||||||
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
|
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
|
||||||
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
||||||
// validate destination
|
// validate destination
|
||||||
if (requestAbstractType.getDestination() == null && containsUnencryptedSignature(holder)) {
|
if (isDestinationRequired() &&
|
||||||
|
requestAbstractType.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||||
event.error(Errors.INVALID_REQUEST);
|
event.error(Errors.INVALID_REQUEST);
|
||||||
|
@ -582,7 +587,8 @@ public class SAMLEndpoint {
|
||||||
}
|
}
|
||||||
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
|
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
|
||||||
// validate destination
|
// validate destination
|
||||||
if (statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
if (isDestinationRequired()
|
||||||
|
&& statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||||
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
||||||
|
|
|
@ -142,6 +142,10 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isDestinationRequired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected Response handleSamlResponse(String samlResponse, String relayState) {
|
protected Response handleSamlResponse(String samlResponse, String relayState) {
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
|
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
|
||||||
|
@ -154,7 +158,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
||||||
// validate destination
|
// validate destination
|
||||||
if (statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
if (isDestinationRequired() &&
|
||||||
|
statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||||
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
|
@ -268,7 +273,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
logger.debug("verified request");
|
logger.debug("verified request");
|
||||||
|
|
||||||
if (requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
|
if (isDestinationRequired() &&
|
||||||
|
requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
|
||||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||||
event.error(Errors.INVALID_REQUEST);
|
event.error(Errors.INVALID_REQUEST);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
|
@ -501,6 +507,9 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateDestination(RequestAbstractType req, SamlClient samlClient, String errorCode) {
|
private boolean validateDestination(RequestAbstractType req, SamlClient samlClient, String errorCode) {
|
||||||
|
if (!isDestinationRequired() && req.getDestination() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// validate destination
|
// validate destination
|
||||||
if (req.getDestination() == null && samlClient.requiresClientSignature()) {
|
if (req.getDestination() == null && samlClient.requiresClientSignature()) {
|
||||||
event.detail(Details.REASON, "missing_destination_required");
|
event.detail(Details.REASON, "missing_destination_required");
|
||||||
|
|
|
@ -68,6 +68,11 @@ public class SamlEcpProfileService extends SamlService {
|
||||||
return SamlProtocol.SAML_SOAP_BINDING;
|
return SamlProtocol.SAML_SOAP_BINDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isDestinationRequired() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
|
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
|
||||||
// force passive authentication when executing this profile
|
// force passive authentication when executing this profile
|
||||||
|
|
|
@ -24,6 +24,8 @@ import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.client.methods.HttpUriRequest;
|
import org.apache.http.client.methods.HttpUriRequest;
|
||||||
import org.apache.http.client.protocol.HttpClientContext;
|
import org.apache.http.client.protocol.HttpClientContext;
|
||||||
import org.apache.http.client.utils.URLEncodedUtils;
|
import org.apache.http.client.utils.URLEncodedUtils;
|
||||||
|
import org.apache.http.entity.ByteArrayEntity;
|
||||||
|
import org.apache.http.entity.ContentType;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
import org.apache.http.impl.client.LaxRedirectStrategy;
|
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||||
|
@ -32,20 +34,28 @@ import org.apache.http.util.EntityUtils;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
import org.keycloak.common.util.KeyUtils;
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
import org.keycloak.saml.SAMLRequestParser;
|
import org.keycloak.saml.SAMLRequestParser;
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
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.exceptions.ProcessingException;
|
||||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||||
|
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
|
||||||
|
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -60,6 +70,13 @@ import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.ws.rs.core.MultivaluedHashMap;
|
import javax.ws.rs.core.MultivaluedHashMap;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.xml.soap.MessageFactory;
|
||||||
|
import javax.xml.soap.SOAPBody;
|
||||||
|
import javax.xml.soap.SOAPEnvelope;
|
||||||
|
import javax.xml.soap.SOAPException;
|
||||||
|
import javax.xml.soap.SOAPHeader;
|
||||||
|
import javax.xml.soap.SOAPHeaderElement;
|
||||||
|
import javax.xml.soap.SOAPMessage;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
|
@ -68,8 +85,11 @@ import static org.junit.Assert.assertTrue;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||||
import org.keycloak.rotation.KeyLocator;
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
|
||||||
import static org.keycloak.saml.common.constants.GeneralConstants.RELAY_STATE;
|
import static org.keycloak.saml.common.constants.GeneralConstants.RELAY_STATE;
|
||||||
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -311,7 +331,167 @@ public class SamlClient {
|
||||||
throw new RuntimeException(ex);
|
throw new RuntimeException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
/**
|
||||||
|
* SOAP binding is currently usable only with http://localhost:8280/ecp-sp/ client, see to-do comment within
|
||||||
|
* {@link #createSamlSignedRequest} for more details. After resolving that to-do it should be usable with any
|
||||||
|
* client.
|
||||||
|
*/
|
||||||
|
SOAP {
|
||||||
|
@Override
|
||||||
|
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException {
|
||||||
|
|
||||||
|
assertThat(response, statusCodeIsHC(200));
|
||||||
|
|
||||||
|
MessageFactory messageFactory = null;
|
||||||
|
try {
|
||||||
|
messageFactory = MessageFactory.newInstance();
|
||||||
|
SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent());
|
||||||
|
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||||
|
Node authnRequestNode = soapBody.getFirstChild();
|
||||||
|
Document document = DocumentUtil.createDocument();
|
||||||
|
document.appendChild(document.importNode(authnRequestNode, true));
|
||||||
|
|
||||||
|
SAMLParser samlParser = SAMLParser.getInstance();
|
||||||
|
JAXPValidationUtil.checkSchemaValidation(document);
|
||||||
|
|
||||||
|
SAML2Object responseType = (SAML2Object) samlParser.parse(document);
|
||||||
|
|
||||||
|
return new SAMLDocumentHolder(responseType, document);
|
||||||
|
} catch (SOAPException | ConfigurationException | ProcessingException | ParsingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String NS_PREFIX_PROFILE_ECP = "ecp";
|
||||||
|
private static final String NS_PREFIX_SAML_PROTOCOL = "samlp";
|
||||||
|
private static final String NS_PREFIX_SAML_ASSERTION = "saml";
|
||||||
|
private static final String NS_PREFIX_PAOS_BINDING = "paos";
|
||||||
|
|
||||||
|
private void createEcpRequestHeader(SOAPEnvelope envelope, SamlDeployment deployment) throws SOAPException {
|
||||||
|
SOAPHeader headers = envelope.getHeader();
|
||||||
|
SOAPHeaderElement ecpRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PROFILE_ECP));
|
||||||
|
|
||||||
|
ecpRequestHeader.setMustUnderstand(true);
|
||||||
|
ecpRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
|
||||||
|
ecpRequestHeader.addAttribute(envelope.createName("ProviderName"), deployment.getEntityID());
|
||||||
|
ecpRequestHeader.addAttribute(envelope.createName("IsPassive"), "0");
|
||||||
|
ecpRequestHeader.addChildElement(envelope.createQName("Issuer", "saml")).setValue(deployment.getEntityID());
|
||||||
|
ecpRequestHeader.addChildElement(envelope.createQName("IDPList", "samlp"))
|
||||||
|
.addChildElement(envelope.createQName("IDPEntry", "samlp"))
|
||||||
|
.addAttribute(envelope.createName("ProviderID"), deployment.getIDP().getEntityID())
|
||||||
|
.addAttribute(envelope.createName("Name"), deployment.getIDP().getEntityID())
|
||||||
|
.addAttribute(envelope.createName("Loc"), deployment.getIDP().getSingleSignOnService().getRequestBindingUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPaosRequestHeader(SOAPEnvelope envelope, SamlDeployment deployment) throws SOAPException {
|
||||||
|
SOAPHeader headers = envelope.getHeader();
|
||||||
|
SOAPHeaderElement paosRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PAOS_BINDING));
|
||||||
|
|
||||||
|
paosRequestHeader.setMustUnderstand(true);
|
||||||
|
paosRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
|
||||||
|
paosRequestHeader.addAttribute(envelope.createName("service"), JBossSAMLURIConstants.ECP_PROFILE.get());
|
||||||
|
paosRequestHeader.addAttribute(envelope.createName("responseConsumerURL"), getResponseConsumerUrl(deployment));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getResponseConsumerUrl(SamlDeployment deployment) {
|
||||||
|
return (deployment.getIDP() == null
|
||||||
|
|| deployment.getIDP().getSingleSignOnService() == null
|
||||||
|
|| deployment.getIDP().getSingleSignOnService().getAssertionConsumerServiceUrl() == null
|
||||||
|
) ? null
|
||||||
|
: deployment.getIDP().getSingleSignOnService().getAssertionConsumerServiceUrl().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) {
|
||||||
|
return createSamlSignedRequest(samlEndpoint, relayState, samlRequest, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
|
||||||
|
return createSamlSignedRequest(samlEndpoint, relayState, samlRequest, realmPrivateKey, realmPublicKey, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey, String certificateStr) {
|
||||||
|
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
|
||||||
|
|
||||||
|
if (realmPrivateKey != null && realmPublicKey != null) {
|
||||||
|
PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(realmPrivateKey);
|
||||||
|
PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(realmPublicKey);
|
||||||
|
X509Certificate cert = org.keycloak.common.util.PemUtils.decodeCertificate(certificateStr);
|
||||||
|
binding
|
||||||
|
.signatureAlgorithm(SignatureAlgorithm.RSA_SHA256)
|
||||||
|
.signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey, cert)
|
||||||
|
.signDocument();
|
||||||
|
|
||||||
|
try {
|
||||||
|
samlRequest = binding.postBinding(samlRequest).getDocument();
|
||||||
|
} catch (ProcessingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageFactory messageFactory = null;
|
||||||
|
try {
|
||||||
|
|
||||||
|
messageFactory = MessageFactory.newInstance();
|
||||||
|
|
||||||
|
SOAPMessage message = messageFactory.createMessage();
|
||||||
|
|
||||||
|
SOAPEnvelope envelope = message.getSOAPPart().getEnvelope();
|
||||||
|
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get());
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get());
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get());
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get());
|
||||||
|
|
||||||
|
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("ecp-sp"); // TODO: Make more general for any client, currently SOAP is usable only with http://localhost:8280/ecp-sp/ client
|
||||||
|
|
||||||
|
createPaosRequestHeader(envelope, deployment);
|
||||||
|
createEcpRequestHeader(envelope, deployment);
|
||||||
|
|
||||||
|
SOAPBody body = envelope.getBody();
|
||||||
|
|
||||||
|
body.addDocument(samlRequest);
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
message.writeTo(outputStream);
|
||||||
|
|
||||||
|
HttpPost post = new HttpPost(samlEndpoint);
|
||||||
|
post.setEntity(new ByteArrayEntity(outputStream.toByteArray(), ContentType.TEXT_XML));
|
||||||
|
return post;
|
||||||
|
} catch (SOAPException | IOException | ParsingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getBindingUri() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpUriRequest createSamlSignedResponse(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpUriRequest createSamlSignedResponse(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey, String certificateStr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractRelayState(CloseableHttpResponse response) throws IOException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException;
|
public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
|
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
|
||||||
|
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
|
||||||
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
import org.keycloak.testsuite.utils.arquillian.DeploymentArchiveProcessorUtils;
|
||||||
|
import org.keycloak.testsuite.utils.io.IOUtil;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class SamlUtils {
|
||||||
|
public static SamlDeployment getSamlDeploymentForClient(String client) throws ParsingException {
|
||||||
|
InputStream is = SamlUtils.class.getResourceAsStream("/adapter-test/keycloak-saml/" + client + "/WEB-INF/keycloak-saml.xml");
|
||||||
|
|
||||||
|
// InputStream -> Document
|
||||||
|
Document doc = IOUtil.loadXML(is);
|
||||||
|
|
||||||
|
// Modify saml deployment the same way as before deploying to real app server
|
||||||
|
DeploymentArchiveProcessorUtils.modifySAMLDocument(doc);
|
||||||
|
|
||||||
|
// Document -> InputStream
|
||||||
|
InputStream isProcessed = IOUtil.documentToInputStream(doc);
|
||||||
|
|
||||||
|
// InputStream -> SamlDeployment
|
||||||
|
ResourceLoader loader = new ResourceLoader() {
|
||||||
|
@Override
|
||||||
|
public InputStream getResourceAsStream(String resource) {
|
||||||
|
return getClass().getResourceAsStream("/adapter-test/keycloak-saml/" + client + resource);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new DeploymentBuilder().build(isProcessed, loader);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,9 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.util.saml;
|
package org.keycloak.testsuite.util.saml;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.admin.Users;
|
||||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
|
@ -34,6 +37,8 @@ import org.apache.http.client.protocol.HttpClientContext;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
|
||||||
|
|
||||||
public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<AuthnRequestType, CreateAuthnRequestStepBuilder> {
|
public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<AuthnRequestType, CreateAuthnRequestStepBuilder> {
|
||||||
|
|
||||||
|
@ -44,6 +49,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
||||||
private String signingPublicKeyPem; // TODO: should not be needed
|
private String signingPublicKeyPem; // TODO: should not be needed
|
||||||
private String signingPrivateKeyPem;
|
private String signingPrivateKeyPem;
|
||||||
private String signingCertificate;
|
private String signingCertificate;
|
||||||
|
private String authorizationHeader;
|
||||||
|
|
||||||
private final Document forceLoginRequestDocument;
|
private final Document forceLoginRequestDocument;
|
||||||
|
|
||||||
|
@ -91,6 +97,14 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CreateAuthnRequestStepBuilder basicAuthentication(UserRepresentation user) {
|
||||||
|
String username = user.getUsername();
|
||||||
|
String password = Users.getPasswordOf(user);
|
||||||
|
String pair = username + ":" + password;
|
||||||
|
this.authorizationHeader = "Basic " + Base64.encodeBytes(pair.getBytes());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
||||||
Document doc = createLoginRequestDocument();
|
Document doc = createLoginRequestDocument();
|
||||||
|
@ -104,9 +118,16 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
||||||
|
|
||||||
Document samlDoc = DocumentUtil.getDocument(transformed);
|
Document samlDoc = DocumentUtil.getDocument(transformed);
|
||||||
String relayState = this.relayState == null ? null : this.relayState.get();
|
String relayState = this.relayState == null ? null : this.relayState.get();
|
||||||
return this.signingPrivateKeyPem == null
|
|
||||||
|
HttpUriRequest request = this.signingPrivateKeyPem == null
|
||||||
? requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState, samlDoc)
|
? requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState, samlDoc)
|
||||||
: requestBinding.createSamlSignedRequest(authServerSamlUrl, relayState, samlDoc, signingPrivateKeyPem, signingPublicKeyPem, signingCertificate);
|
: requestBinding.createSamlSignedRequest(authServerSamlUrl, relayState, samlDoc, signingPrivateKeyPem, signingPublicKeyPem, signingCertificate);
|
||||||
|
|
||||||
|
if (authorizationHeader != null) {
|
||||||
|
request.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Document createLoginRequestDocument() {
|
protected Document createLoginRequestDocument() {
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
package org.keycloak.testsuite.saml;
|
package org.keycloak.testsuite.saml;
|
||||||
|
|
||||||
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
import org.keycloak.protocol.saml.SamlProtocol;
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.testsuite.AbstractAuthTest;
|
import org.keycloak.testsuite.AbstractAuthTest;
|
||||||
import org.keycloak.testsuite.util.SamlClient;
|
import org.keycloak.testsuite.util.SamlClient;
|
||||||
|
@ -17,7 +23,11 @@ import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
|
||||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT;
|
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT;
|
||||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SCHEME;
|
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SCHEME;
|
||||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
|
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
|
||||||
|
@ -36,6 +46,9 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
||||||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post/saml";
|
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post/saml";
|
||||||
public static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8280/sales-post/";
|
public static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8280/sales-post/";
|
||||||
|
|
||||||
|
public static final String SAML_CLIENT_ID_ECP_SP = "http://localhost:8280/ecp-sp/";
|
||||||
|
public static final String SAML_ASSERTION_CONSUMER_URL_ECP_SP = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/ecp-sp/saml";
|
||||||
|
|
||||||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST2 = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post2/saml";
|
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST2 = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post2/saml";
|
||||||
public static final String SAML_CLIENT_ID_SALES_POST2 = "http://localhost:8280/sales-post2/";
|
public static final String SAML_CLIENT_ID_SALES_POST2 = "http://localhost:8280/sales-post2/";
|
||||||
|
|
||||||
|
@ -73,6 +86,9 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
||||||
|
|
||||||
public static final String SAML_BROKER_ALIAS = "saml-broker";
|
public static final String SAML_BROKER_ALIAS = "saml-broker";
|
||||||
|
|
||||||
|
protected final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
|
||||||
|
protected final AtomicReference<String> sessionIndexRef = new AtomicReference<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
|
testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
|
||||||
|
@ -109,4 +125,20 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
||||||
protected URI getSamlBrokerUrl(String realmName) {
|
protected URI getSamlBrokerUrl(String realmName) {
|
||||||
return URI.create(getAuthServerRealmBase(realmName).toString() + "/broker/" + SAML_BROKER_ALIAS + "/endpoint");
|
return URI.create(getAuthServerRealmBase(realmName).toString() + "/broker/" + SAML_BROKER_ALIAS + "/endpoint");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected SAML2Object extractNameIdAndSessionIndexAndTerminate(SAML2Object so) {
|
||||||
|
assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||||
|
ResponseType loginResp1 = (ResponseType) so;
|
||||||
|
final AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion();
|
||||||
|
assertThat(firstAssertion, org.hamcrest.Matchers.notNullValue());
|
||||||
|
assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
|
||||||
|
|
||||||
|
NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID();
|
||||||
|
AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next();
|
||||||
|
|
||||||
|
nameIdRef.set(nameId);
|
||||||
|
sessionIndexRef.set(firstAssertionStatement.getSessionIndex());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,9 +90,6 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
private ClientRepresentation salesRep;
|
private ClientRepresentation salesRep;
|
||||||
private ClientRepresentation sales2Rep;
|
private ClientRepresentation sales2Rep;
|
||||||
|
|
||||||
private final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
|
|
||||||
private final AtomicReference<String> sessionIndexRef = new AtomicReference<>();
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
|
salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
|
||||||
|
@ -116,22 +113,6 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SAML2Object extractNameIdAndSessionIndexAndTerminate(SAML2Object so) {
|
|
||||||
assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
|
||||||
ResponseType loginResp1 = (ResponseType) so;
|
|
||||||
final AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion();
|
|
||||||
assertThat(firstAssertion, org.hamcrest.Matchers.notNullValue());
|
|
||||||
assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
|
|
||||||
|
|
||||||
NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID();
|
|
||||||
AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next();
|
|
||||||
|
|
||||||
nameIdRef.set(nameId);
|
|
||||||
sessionIndexRef.set(firstAssertionStatement.getSessionIndex());
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SamlClientBuilder logIntoUnsignedSalesAppViaIdp() throws IllegalArgumentException, UriBuilderException {
|
private SamlClientBuilder logIntoUnsignedSalesAppViaIdp() throws IllegalArgumentException, UriBuilderException {
|
||||||
return new SamlClientBuilder()
|
return new SamlClientBuilder()
|
||||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build()
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build()
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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 org.junit.Test;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||||
|
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
|
||||||
|
import static org.keycloak.testsuite.util.SamlClient.Binding.SOAP;
|
||||||
|
|
||||||
|
public class SOAPBindingTest extends AbstractSamlTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingAuthnWithSignatureTest() {
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.basicAuthentication(bburkeUser)
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
|
||||||
|
ResponseType rt = (ResponseType)response.getSamlObject();
|
||||||
|
assertThat(rt.getAssertions(), not(empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingAuthnWithSignatureMissingDestinationTest() {
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
|
||||||
|
.transformObject(authnRequestType -> {
|
||||||
|
authnRequestType.setDestination(null);
|
||||||
|
return authnRequestType;
|
||||||
|
})
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.basicAuthentication(bburkeUser)
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
|
||||||
|
ResponseType rt = (ResponseType)response.getSamlObject();
|
||||||
|
assertThat(rt.getAssertions(), not(empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingAuthnWithoutSignatureTest() {
|
||||||
|
getCleanup()
|
||||||
|
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
|
||||||
|
.update()
|
||||||
|
);
|
||||||
|
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
|
||||||
|
.basicAuthentication(bburkeUser)
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
|
||||||
|
ResponseType rt = (ResponseType)response.getSamlObject();
|
||||||
|
assertThat(rt.getAssertions(), not(empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingAuthnWithoutSignatureMissingDestinationTest() {
|
||||||
|
getCleanup()
|
||||||
|
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
|
||||||
|
.update()
|
||||||
|
);
|
||||||
|
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
|
||||||
|
.transformObject(authnRequestType -> {
|
||||||
|
authnRequestType.setDestination(null);
|
||||||
|
return authnRequestType;
|
||||||
|
})
|
||||||
|
.basicAuthentication(bburkeUser)
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
|
||||||
|
ResponseType rt = (ResponseType)response.getSamlObject();
|
||||||
|
assertThat(rt.getAssertions(), not(empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingLogoutWithSignature() {
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, POST)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.build()
|
||||||
|
.login().user(bburkeUser).build()
|
||||||
|
.processSamlResponse(POST)
|
||||||
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
|
.build()
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(POST::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingLogoutWithoutSignature() {
|
||||||
|
getCleanup()
|
||||||
|
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
|
||||||
|
.update()
|
||||||
|
);
|
||||||
|
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, POST)
|
||||||
|
.build()
|
||||||
|
.login().user(bburkeUser).build()
|
||||||
|
.processSamlResponse(POST)
|
||||||
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
|
.build()
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(POST::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingLogoutWithSignatureMissingDestinationTest() {
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, POST)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.build()
|
||||||
|
.login().user(bburkeUser).build()
|
||||||
|
.processSamlResponse(POST)
|
||||||
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
|
.build()
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.transformObject(logoutRequestType -> {
|
||||||
|
logoutRequestType.setDestination(null);
|
||||||
|
return logoutRequestType;
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(POST::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void soapBindingLogoutWithoutSignatureMissingDestinationTest() {
|
||||||
|
getCleanup()
|
||||||
|
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
|
||||||
|
.update()
|
||||||
|
);
|
||||||
|
|
||||||
|
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, POST)
|
||||||
|
.build()
|
||||||
|
.login().user(bburkeUser).build()
|
||||||
|
.processSamlResponse(POST)
|
||||||
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
|
.build()
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.transformObject(logoutRequestType -> {
|
||||||
|
logoutRequestType.setDestination(null);
|
||||||
|
return logoutRequestType;
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.executeAndTransform(POST::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
}
|
||||||
|
}
|
|
@ -241,12 +241,7 @@ public class DeploymentArchiveProcessorUtils {
|
||||||
public static void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
|
public static void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
|
||||||
Document doc = IOUtil.loadXML(archive.get(adapterConfigPath).getAsset().openStream());
|
Document doc = IOUtil.loadXML(archive.get(adapterConfigPath).getAsset().openStream());
|
||||||
|
|
||||||
modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", AUTH_SERVER_REPLACED_URL, getAuthServerContextRoot());
|
modifySAMLDocument(doc);
|
||||||
modifyDocElementAttribute(doc, "SingleLogoutService", "postBindingUrl", AUTH_SERVER_REPLACED_URL, getAuthServerContextRoot());
|
|
||||||
modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", AUTH_SERVER_REPLACED_URL, getAuthServerContextRoot());
|
|
||||||
|
|
||||||
modifyDocElementAttribute(doc, "SingleSignOnService", "assertionConsumerServiceUrl", AUTH_SERVER_REPLACED_URL, getAppServerContextRoot());
|
|
||||||
modifyDocElementAttribute(doc, "SP", "logoutPage", AUTH_SERVER_REPLACED_URL, getAppServerContextRoot());
|
|
||||||
|
|
||||||
archive.add(new StringAsset(IOUtil.documentToString(doc)), adapterConfigPath);
|
archive.add(new StringAsset(IOUtil.documentToString(doc)), adapterConfigPath);
|
||||||
|
|
||||||
|
@ -260,6 +255,15 @@ public class DeploymentArchiveProcessorUtils {
|
||||||
((WebArchive) archive).addAsResource(truststore);
|
((WebArchive) archive).addAsResource(truststore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void modifySAMLDocument(Document doc) {
|
||||||
|
modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", AUTH_SERVER_REPLACED_URL, getAuthServerContextRoot());
|
||||||
|
modifyDocElementAttribute(doc, "SingleLogoutService", "postBindingUrl", AUTH_SERVER_REPLACED_URL, getAuthServerContextRoot());
|
||||||
|
modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", AUTH_SERVER_REPLACED_URL, getAuthServerContextRoot());
|
||||||
|
|
||||||
|
modifyDocElementAttribute(doc, "SingleSignOnService", "assertionConsumerServiceUrl", AUTH_SERVER_REPLACED_URL, getAppServerContextRoot());
|
||||||
|
modifyDocElementAttribute(doc, "SP", "logoutPage", AUTH_SERVER_REPLACED_URL, getAppServerContextRoot());
|
||||||
|
}
|
||||||
|
|
||||||
private static String getAuthServerUrl() {
|
private static String getAuthServerUrl() {
|
||||||
String scheme = AUTH_SERVER_SSL_REQUIRED ? "https" : "http";
|
String scheme = AUTH_SERVER_SSL_REQUIRED ? "https" : "http";
|
||||||
String host = System.getProperty("auth.server.host", "localhost");
|
String host = System.getProperty("auth.server.host", "localhost");
|
||||||
|
|
|
@ -28,12 +28,16 @@ import org.xml.sax.SAXException;
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.transform.Result;
|
||||||
|
import javax.xml.transform.Source;
|
||||||
import javax.xml.transform.Transformer;
|
import javax.xml.transform.Transformer;
|
||||||
import javax.xml.transform.TransformerException;
|
import javax.xml.transform.TransformerException;
|
||||||
import javax.xml.transform.TransformerFactory;
|
import javax.xml.transform.TransformerFactory;
|
||||||
import javax.xml.transform.dom.DOMSource;
|
import javax.xml.transform.dom.DOMSource;
|
||||||
import javax.xml.transform.stream.StreamResult;
|
import javax.xml.transform.stream.StreamResult;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
@ -103,6 +107,19 @@ public class IOUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static InputStream documentToInputStream(Document doc) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
Source xmlSource = new DOMSource(doc);
|
||||||
|
Result outputTarget = new StreamResult(outputStream);
|
||||||
|
TransformerFactory.newInstance().newTransformer().transform(xmlSource, outputTarget);
|
||||||
|
return new ByteArrayInputStream(outputStream.toByteArray());
|
||||||
|
} catch (TransformerException e) {
|
||||||
|
log.error("Can't transform document to InputStream");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifies attribute value according to the given regex (first occurrence) iff
|
* Modifies attribute value according to the given regex (first occurrence) iff
|
||||||
* there are following conditions accomplished:
|
* there are following conditions accomplished:
|
||||||
|
|
Loading…
Reference in a new issue