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 extractResponseDocument(String response);
|
||||
|
||||
protected boolean isDestinationRequired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected KeyLocator getIDPKeyLocator() {
|
||||
List<Key> keys = new LinkedList<>();
|
||||
|
||||
|
@ -271,7 +275,8 @@ public class SAMLEndpoint {
|
|||
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
|
||||
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
||||
// validate destination
|
||||
if (requestAbstractType.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||
if (isDestinationRequired() &&
|
||||
requestAbstractType.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
|
@ -582,7 +587,8 @@ public class SAMLEndpoint {
|
|||
}
|
||||
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
|
||||
// validate destination
|
||||
if (statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||
if (isDestinationRequired()
|
||||
&& statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
||||
|
|
|
@ -141,6 +141,10 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean isDestinationRequired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected Response handleSamlResponse(String samlResponse, String relayState) {
|
||||
event.event(EventType.LOGOUT);
|
||||
|
@ -154,7 +158,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
|
||||
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
||||
// validate destination
|
||||
if (statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||
if (isDestinationRequired() &&
|
||||
statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
||||
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");
|
||||
|
||||
if (requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
|
||||
if (isDestinationRequired() &&
|
||||
requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
|
||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||
event.error(Errors.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) {
|
||||
if (!isDestinationRequired() && req.getDestination() == null) {
|
||||
return true;
|
||||
}
|
||||
// validate destination
|
||||
if (req.getDestination() == null && samlClient.requiresClientSignature()) {
|
||||
event.detail(Details.REASON, "missing_destination_required");
|
||||
|
|
|
@ -68,6 +68,11 @@ public class SamlEcpProfileService extends SamlService {
|
|||
return SamlProtocol.SAML_SOAP_BINDING;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDestinationRequired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
|
||||
// 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.protocol.HttpClientContext;
|
||||
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.HttpClientBuilder;
|
||||
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||
|
@ -32,20 +34,28 @@ import org.apache.http.util.EntityUtils;
|
|||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.keycloak.adapters.saml.SamlDeployment;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||
import org.keycloak.saml.SAMLRequestParser;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
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.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.parsers.saml.SAMLParser;
|
||||
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 javax.ws.rs.core.Response;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
|
@ -60,6 +70,13 @@ import java.util.List;
|
|||
import java.util.UUID;
|
||||
import javax.ws.rs.core.MultivaluedHashMap;
|
||||
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 static org.hamcrest.Matchers.*;
|
||||
|
@ -68,8 +85,11 @@ import static org.junit.Assert.assertTrue;
|
|||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
|
||||
import static org.keycloak.saml.common.constants.GeneralConstants.RELAY_STATE;
|
||||
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||
|
||||
/**
|
||||
|
@ -311,7 +331,167 @@ public class SamlClient {
|
|||
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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
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.dom.saml.v2.protocol.AuthnRequestType;
|
||||
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.w3c.dom.Document;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
|
||||
|
||||
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 signingPrivateKeyPem;
|
||||
private String signingCertificate;
|
||||
private String authorizationHeader;
|
||||
|
||||
private final Document forceLoginRequestDocument;
|
||||
|
||||
|
@ -90,6 +96,14 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
|||
this.signingCertificate = signingCertificate;
|
||||
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
|
||||
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
||||
|
@ -104,9 +118,16 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
|||
|
||||
Document samlDoc = DocumentUtil.getDocument(transformed);
|
||||
String relayState = this.relayState == null ? null : this.relayState.get();
|
||||
return this.signingPrivateKeyPem == null
|
||||
|
||||
HttpUriRequest request = this.signingPrivateKeyPem == null
|
||||
? requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState, samlDoc)
|
||||
: requestBinding.createSamlSignedRequest(authServerSamlUrl, relayState, samlDoc, signingPrivateKeyPem, signingPublicKeyPem, signingCertificate);
|
||||
|
||||
if (authorizationHeader != null) {
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
protected Document createLoginRequestDocument() {
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
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.ResponseType;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.AbstractAuthTest;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
|
@ -17,7 +23,11 @@ import java.security.spec.PKCS8EncodedKeySpec;
|
|||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
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_SCHEME;
|
||||
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_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_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";
|
||||
|
||||
protected final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
|
||||
protected final AtomicReference<String> sessionIndexRef = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
|
||||
|
@ -109,4 +125,20 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
|||
protected URI getSamlBrokerUrl(String realmName) {
|
||||
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 sales2Rep;
|
||||
|
||||
private final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
|
||||
private final AtomicReference<String> sessionIndexRef = new AtomicReference<>();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
return new SamlClientBuilder()
|
||||
.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) {
|
||||
Document doc = IOUtil.loadXML(archive.get(adapterConfigPath).getAsset().openStream());
|
||||
|
||||
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());
|
||||
modifySAMLDocument(doc);
|
||||
|
||||
archive.add(new StringAsset(IOUtil.documentToString(doc)), adapterConfigPath);
|
||||
|
||||
|
@ -260,6 +255,15 @@ public class DeploymentArchiveProcessorUtils {
|
|||
((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() {
|
||||
String scheme = AUTH_SERVER_SSL_REQUIRED ? "https" : "http";
|
||||
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.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.transform.Result;
|
||||
import javax.xml.transform.Source;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
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
|
||||
* there are following conditions accomplished:
|
||||
|
|
Loading…
Reference in a new issue