Browse Source

Correct signature handling for SAML2 AuthNRequest

Implements the following bindings for AuthNRequest
- REDIRECT
- POST (future PR)

Has been tested with
- Keycloak
- SSOCircle
- Okta
- SimpleSAMLPhp

Fixes gh-7711
Filip Hanik 5 years ago
parent
commit
a51a202925
21 changed files with 1290 additions and 241 deletions
  1. 148 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java
  2. 62 12
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java
  3. 70 6
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java
  4. 17 3
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java
  5. 172 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java
  6. 86 9
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java
  7. 85 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java
  8. 133 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java
  9. 73 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java
  10. 45 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java
  11. 88 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java
  12. 8 68
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2Utils.java
  13. 5 5
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java
  14. 35 30
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java
  15. 59 11
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java
  16. 55 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementationTests.java
  17. 72 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java
  18. 2 2
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2UtilsTests.java
  19. 50 29
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java
  20. 6 52
      samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlActionTestingSupport.java
  21. 19 14
      samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java

+ 148 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java

@@ -0,0 +1,148 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+import org.springframework.util.Assert;
+
+import java.nio.charset.Charset;
+
+/**
+ * Data holder for {@code AuthNRequest} parameters to be sent using either the
+ * {@link Saml2MessageBinding#POST} or {@link Saml2MessageBinding#REDIRECT} binding.
+ * Data will be encoded and possibly deflated, but will not be escaped for transport,
+ * ie URL encoded, {@link org.springframework.web.util.UriUtils#encode(String, Charset)}
+ * or HTML encoded, {@link org.springframework.web.util.HtmlUtils#htmlEscape(String)}.
+ * https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
+ *
+ * @see Saml2AuthenticationRequestFactory#createPostAuthenticationRequest(Saml2AuthenticationRequestContext)
+ * @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext)
+ * @since 5.3
+ */
+abstract class AbstractSaml2AuthenticationRequest {
+
+	private final String samlRequest;
+	private final String relayState;
+	private final String authenticationRequestUri;
+
+	/**
+	 * Mandatory constructor for the {@link AbstractSaml2AuthenticationRequest}
+	 * @param samlRequest - the SAMLRequest XML data, SAML encoded, cannot be empty or null
+	 * @param relayState - RelayState value that accompanies the request, may be null
+	 * @param authenticationRequestUri - The authenticationRequestUri, a URL, where to send the XML message, cannot be empty or null
+	 */
+	AbstractSaml2AuthenticationRequest(
+			String samlRequest,
+			String relayState,
+			String authenticationRequestUri) {
+		Assert.hasText(samlRequest, "samlRequest cannot be null or empty");
+		Assert.hasText(authenticationRequestUri, "authenticationRequestUri cannot be null or empty");
+		this.authenticationRequestUri = authenticationRequestUri;
+		this.samlRequest = samlRequest;
+		this.relayState = relayState;
+	}
+
+	/**
+	 * Returns the AuthNRequest XML value to be sent. This value is already encoded for transport.
+	 * If {@link #getBinding()} is {@link Saml2MessageBinding#REDIRECT} the value is deflated and SAML encoded.
+	 * If {@link #getBinding()} is {@link Saml2MessageBinding#POST} the value is SAML encoded.
+	 * @return the SAMLRequest parameter value
+	 */
+	public String getSamlRequest() {
+		return this.samlRequest;
+	}
+
+	/**
+	 * Returns the RelayState value, if present in the parameters
+	 * @return the RelayState value, or null if not available
+	 */
+	public String getRelayState() {
+		return this.relayState;
+	}
+
+	/**
+	 * Returns the URI endpoint that this AuthNRequest should be sent to.
+	 * @return the URI endpoint for this message
+	 */
+	public String getAuthenticationRequestUri() {
+		return this.authenticationRequestUri;
+	}
+
+	/**
+	 * Returns the binding this AuthNRequest will be sent and
+	 * encoded with. If {@link Saml2MessageBinding#REDIRECT} is used, the DEFLATE encoding will be automatically applied.
+	 * @return the binding this message will be sent with.
+	 */
+	public abstract Saml2MessageBinding getBinding();
+
+	/**
+	 * A builder for {@link AbstractSaml2AuthenticationRequest} and its subclasses.
+	 */
+	static class Builder<T extends Builder<T>> {
+		String authenticationRequestUri;
+		String samlRequest;
+		String relayState;
+
+		protected Builder() {
+		}
+
+		/**
+		 * Casting the return as the generic subtype, when returning itself
+		 * @return this object
+		 */
+		@SuppressWarnings("unchecked")
+		protected final T _this() {
+			return (T) this;
+		}
+
+
+		/**
+		 * Sets the {@code RelayState} parameter that will accompany this AuthNRequest
+		 *
+		 * @param relayState the relay state value, unencoded. if null or empty, the parameter will be removed from the
+		 * map.
+		 * @return this object
+		 */
+		public T relayState(String relayState) {
+			this.relayState = relayState;
+			return _this();
+		}
+
+		/**
+		 * Sets the {@code SAMLRequest} parameter that will accompany this AuthNRequest
+		 *
+		 * @param samlRequest the SAMLRequest parameter.
+		 * @return this object
+		 */
+		public T samlRequest(String samlRequest) {
+			this.samlRequest = samlRequest;
+			return _this();
+		}
+
+		/**
+		 * Sets the {@code authenticationRequestUri}, a URL that will receive the AuthNRequest message
+		 *
+		 * @param authenticationRequestUri the relay state value, unencoded.
+		 * @return this object
+		 */
+		public T authenticationRequestUri(String authenticationRequestUri) {
+			this.authenticationRequestUri = authenticationRequestUri;
+			return _this();
+		}
+	}
+
+}

+ 62 - 12
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,17 +16,25 @@
 
 package org.springframework.security.saml2.provider.service.authentication;
 
-import org.opensaml.saml.common.xml.SAMLConstants;
-import org.springframework.util.Assert;
-
 import org.joda.time.DateTime;
+import org.opensaml.saml.common.xml.SAMLConstants;
 import org.opensaml.saml.saml2.core.AuthnRequest;
 import org.opensaml.saml.saml2.core.Issuer;
+import org.springframework.security.saml2.credentials.Saml2X509Credential;
+import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest.Builder;
+import org.springframework.util.Assert;
 
 import java.time.Clock;
 import java.time.Instant;
+import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.emptyList;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlDeflate;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlEncode;
+
 /**
  * @since 5.2
  */
@@ -35,11 +43,50 @@ public class OpenSamlAuthenticationRequestFactory implements Saml2Authentication
 	private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
 	private String protocolBinding = SAMLConstants.SAML2_POST_BINDING_URI;
 
+	@Override
+	@Deprecated
+	public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
+		return createAuthenticationRequest(request, request.getCredentials());
+	}
+
 	/**
 	 * {@inheritDoc}
 	 */
 	@Override
-	public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
+	public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) {
+		String xml = createAuthenticationRequest(context, context.getRelyingPartyRegistration().getSigningCredentials());
+		return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context)
+				.samlRequest(samlEncode(xml.getBytes(UTF_8)))
+				.build();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext context) {
+		String xml = createAuthenticationRequest(context, emptyList());
+		List<Saml2X509Credential> signingCredentials = context.getRelyingPartyRegistration().getSigningCredentials();
+		Builder result = Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context);
+
+		String deflatedAndEncoded = samlEncode(samlDeflate(xml));
+		Map<String, String> signedParams = this.saml.signQueryParameters(
+				signingCredentials,
+				deflatedAndEncoded,
+				context.getRelayState()
+		);
+		result.samlRequest(signedParams.get("SAMLRequest"))
+				.relayState(signedParams.get("RelayState"))
+				.sigAlg(signedParams.get("SigAlg"))
+				.signature(signedParams.get("Signature"));
+		return result.build();
+	}
+
+	private String createAuthenticationRequest(Saml2AuthenticationRequestContext request, List<Saml2X509Credential> credentials) {
+		return createAuthenticationRequest(Saml2AuthenticationRequest.withAuthenticationRequestContext(request).build(), credentials);
+	}
+
+	private String createAuthenticationRequest(Saml2AuthenticationRequest context, List<Saml2X509Credential> credentials) {
 		AuthnRequest auth = this.saml.buildSAMLObject(AuthnRequest.class);
 		auth.setID("ARQ" + UUID.randomUUID().toString().substring(1));
 		auth.setIssueInstant(new DateTime(this.clock.millis()));
@@ -47,14 +94,14 @@ public class OpenSamlAuthenticationRequestFactory implements Saml2Authentication
 		auth.setIsPassive(Boolean.FALSE);
 		auth.setProtocolBinding(protocolBinding);
 		Issuer issuer = this.saml.buildSAMLObject(Issuer.class);
-		issuer.setValue(request.getIssuer());
+		issuer.setValue(context.getIssuer());
 		auth.setIssuer(issuer);
-		auth.setDestination(request.getDestination());
-		auth.setAssertionConsumerServiceURL(request.getAssertionConsumerServiceUrl());
+		auth.setDestination(context.getDestination());
+		auth.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl());
 		return this.saml.toXml(
 				auth,
-				request.getCredentials(),
-				request.getIssuer()
+				credentials,
+				context.getIssuer()
 		);
 	}
 
@@ -71,11 +118,14 @@ public class OpenSamlAuthenticationRequestFactory implements Saml2Authentication
 	}
 
 	/**
-	 * Sets the {@code protocolBinding} to use when generating authentication requests
+	 * Sets the {@code protocolBinding} to use when generating authentication requests.
 	 * Acceptable values are {@link SAMLConstants#SAML2_POST_BINDING_URI} and
 	 * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI}
+	 * The IDP will be reading this value in the {@code AuthNRequest} to determine how to
+	 * send the Response/Assertion to the ACS URL, assertion consumer service URL.
 	 *
-	 * @param protocolBinding
+	 * @param protocolBinding either {@link SAMLConstants#SAML2_POST_BINDING_URI} or
+	 * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI}
 	 * @throws IllegalArgumentException if the protocolBinding is not valid
 	 */
 	public void setProtocolBinding(String protocolBinding) {

+ 70 - 6
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,10 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.saml2.provider.service.authentication;
 
-import org.springframework.security.saml2.Saml2Exception;
-import org.springframework.security.saml2.credentials.Saml2X509Credential;
+package org.springframework.security.saml2.provider.service.authentication;
 
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.xml.BasicParserPool;
@@ -41,6 +39,7 @@ import org.opensaml.security.credential.CredentialSupport;
 import org.opensaml.security.credential.UsageType;
 import org.opensaml.security.x509.BasicX509Credential;
 import org.opensaml.xmlsec.SignatureSigningParameters;
+import org.opensaml.xmlsec.crypto.XMLSigningUtil;
 import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver;
 import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver;
 import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver;
@@ -48,21 +47,29 @@ import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyR
 import org.opensaml.xmlsec.signature.support.SignatureConstants;
 import org.opensaml.xmlsec.signature.support.SignatureException;
 import org.opensaml.xmlsec.signature.support.SignatureSupport;
+import org.springframework.security.saml2.Saml2Exception;
+import org.springframework.security.saml2.credentials.Saml2X509Credential;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Utils;
+import org.springframework.util.Assert;
+import org.springframework.web.util.UriUtils;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
 import java.io.ByteArrayInputStream;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import javax.xml.XMLConstants;
-import javax.xml.namespace.QName;
 
 import static java.lang.Boolean.FALSE;
 import static java.lang.Boolean.TRUE;
 import static java.util.Arrays.asList;
 import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getBuilderFactory;
+import static org.springframework.util.StringUtils.hasText;
 
 /**
  * @since 5.2
@@ -191,11 +198,68 @@ final class OpenSamlImplementation {
 		}
 	}
 
+	/**
+	 * Returns query parameter after creating a Query String signature
+	 * All return values are unencoded and will need to be encoded prior to sending
+	 * The methods {@link UriUtils#encode(String, Charset)} and {@link UriUtils#decode(String, Charset)}
+	 * with the {@link StandardCharsets#ISO_8859_1} character set are used for all URL encoding/decoding.
+	 * @param signingCredentials - credentials to be used for signature
+	 * @return a map of unencoded query parameters with the following keys:
+	 * {@code {SAMLRequest, RelayState (may be null)}, SigAlg, Signature}
+	 *
+	 */
+	Map<String, String> signQueryParameters(
+			List<Saml2X509Credential> signingCredentials,
+			String samlRequest,
+			String relayState) {
+		Assert.notNull(samlRequest, "samlRequest cannot be null");
+		String algorithmUri = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
+		StringBuilder queryString = new StringBuilder();
+		queryString
+				.append("SAMLRequest")
+				.append("=")
+				.append(UriUtils.encode(samlRequest, StandardCharsets.ISO_8859_1))
+				.append("&");
+		if (hasText(relayState)) {
+			queryString
+					.append("RelayState")
+					.append("=")
+					.append(UriUtils.encode(relayState, StandardCharsets.ISO_8859_1))
+					.append("&");
+		}
+		queryString
+				.append("SigAlg")
+				.append("=")
+				.append(UriUtils.encode(algorithmUri, StandardCharsets.ISO_8859_1));
+
+		try {
+			byte[] rawSignature = XMLSigningUtil.signWithURI(
+					getSigningCredential(signingCredentials, ""),
+					algorithmUri,
+					queryString.toString().getBytes(StandardCharsets.UTF_8)
+			);
+			String b64Signature = Saml2Utils.samlEncode(rawSignature);
+
+			Map<String, String> result = new LinkedHashMap<>();
+			result.put("SAMLRequest", samlRequest);
+			if (hasText(relayState)) {
+				result.put("RelayState", relayState);
+			}
+			result.put("SigAlg", algorithmUri);
+			result.put("Signature", b64Signature);
+			return result;
+		}
+		catch (SecurityException e) {
+			throw new Saml2Exception(e);
+		}
+	}
+
 	/*
 	 * ==============================================================
 	 * PRIVATE METHODS
 	 * ==============================================================
 	 */
+
 	private XMLObject resolve(byte[] xml) {
 		XMLObject parsed = parse(xml);
 		if (parsed != null) {

+ 17 - 3
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,9 +29,10 @@ import java.util.function.Consumer;
  * from the service provider to the identity provider
  * https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
  *
- * @see {@link Saml2AuthenticationRequestFactory}
  * @since 5.2
+ * @deprecated use {@link Saml2AuthenticationRequestContext}
  */
+@Deprecated
 public final class Saml2AuthenticationRequest {
 	private final String issuer;
 	private final List<Saml2X509Credential> credentials;
@@ -55,7 +56,6 @@ public final class Saml2AuthenticationRequest {
 				this.credentials.add(c);
 			}
 		}
-		Assert.notEmpty(this.credentials, "at least one SIGNING credential must be present");
 	}
 
 
@@ -104,6 +104,20 @@ public final class Saml2AuthenticationRequest {
 		return new Builder();
 	}
 
+	/**
+	 * A builder for {@link Saml2AuthenticationRequest}.
+	 * @param context a context object to copy values from.
+	 * returns a builder object
+	 */
+	public static Builder withAuthenticationRequestContext(Saml2AuthenticationRequestContext context) {
+		return new Builder()
+				.assertionConsumerServiceUrl(context.getAssertionConsumerServiceUrl())
+				.issuer(context.getIssuer())
+				.destination(context.getDestination())
+				.credentials(c -> c.addAll(context.getRelyingPartyRegistration().getCredentials()))
+				;
+	}
+
 	/**
 	 * A builder for {@link Saml2AuthenticationRequest}.
 	 */

+ 172 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java

@@ -0,0 +1,172 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.util.Assert;
+
+/**
+ * Data holder for information required to create an {@code AuthNRequest}
+ * to be sent from the service provider to the identity provider
+ * <a href="https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf">
+ * Assertions and Protocols for SAML 2 (line 2031)</a>
+ *
+ * @see Saml2AuthenticationRequestFactory#createPostAuthenticationRequest(Saml2AuthenticationRequestContext)
+ * @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext)
+ * @since 5.3
+ */
+public final class Saml2AuthenticationRequestContext {
+	private final RelyingPartyRegistration relyingPartyRegistration;
+	private final String issuer;
+	private final String assertionConsumerServiceUrl;
+	private final String relayState;
+
+	private Saml2AuthenticationRequestContext(
+			RelyingPartyRegistration relyingPartyRegistration,
+			String issuer,
+			String assertionConsumerServiceUrl,
+			String relayState) {
+		Assert.hasText(issuer, "issuer cannot be null or empty");
+		Assert.notNull(relyingPartyRegistration, "relyingPartyRegistration cannot be null");
+		Assert.hasText(assertionConsumerServiceUrl, "spAssertionConsumerServiceUrl cannot be null or empty");
+		this.issuer = issuer;
+		this.relyingPartyRegistration = relyingPartyRegistration;
+		this.assertionConsumerServiceUrl = assertionConsumerServiceUrl;
+		this.relayState = relayState;
+	}
+
+	/**
+	 * Returns the {@link RelyingPartyRegistration} configuration for which the AuthNRequest is intended for.
+	 * @return the {@link RelyingPartyRegistration} configuration
+	 */
+	public RelyingPartyRegistration getRelyingPartyRegistration() {
+		return this.relyingPartyRegistration;
+	}
+
+	/**
+	 * Returns the {@code Issuer} value to be used in the {@code AuthNRequest} object.
+	 * This property should be used to populate the {@code AuthNRequest.Issuer} XML element.
+	 * This value typically is a URI, but can be an arbitrary string.
+	 * @return the Issuer value
+	 */
+	public String getIssuer() {
+		return this.issuer;
+	}
+
+	/**
+	 * Returns the desired {@code AssertionConsumerServiceUrl} that this SP wishes to receive the
+	 * assertion on. The IDP may or may not honor this request.
+	 * This property populates the {@code AuthNRequest.AssertionConsumerServiceURL} XML attribute.
+	 * @return the AssertionConsumerServiceURL value
+	 */
+	public String getAssertionConsumerServiceUrl() {
+		return assertionConsumerServiceUrl;
+	}
+
+	/**
+	 * Returns the RelayState value, if present in the parameters
+	 * @return the RelayState value, or null if not available
+	 */
+	public String getRelayState() {
+		return this.relayState;
+	}
+
+	/**
+	 * Returns the {@code Destination}, the WEB Single Sign On URI, for this authentication request.
+	 * This property can also populate the {@code AuthNRequest.Destination} XML attribute.
+	 * @return the Destination value
+	 */
+	public String getDestination() {
+		return this.getRelyingPartyRegistration().getIdpWebSsoUrl();
+	}
+
+	/**
+	 * A builder for {@link Saml2AuthenticationRequestContext}.
+	 * @return a builder object
+	 */
+	public static Builder builder() {
+		return new Builder();
+	}
+
+	/**
+	 * A builder for {@link Saml2AuthenticationRequestContext}.
+	 */
+	public static class Builder {
+		private String issuer;
+		private String assertionConsumerServiceUrl;
+		private String relayState;
+		private RelyingPartyRegistration relyingPartyRegistration;
+
+		private Builder() {
+		}
+
+		/**
+		 * Sets the issuer for the authentication request.
+		 * @param issuer - a required value
+		 * @return this {@code Builder}
+		 */
+		public Builder issuer(String issuer) {
+			this.issuer = issuer;
+			return this;
+		}
+
+		/**
+		 * Sets the {@link RelyingPartyRegistration} used to build the authentication request.
+		 * @param relyingPartyRegistration - a required value
+		 * @return this {@code Builder}
+		 */
+		public Builder relyingPartyRegistration(RelyingPartyRegistration relyingPartyRegistration) {
+			this.relyingPartyRegistration = relyingPartyRegistration;
+			return this;
+		}
+
+		/**
+		 * Sets the {@code assertionConsumerServiceURL} for the authentication request.
+		 * Typically the {@code Service Provider EntityID}
+		 * @param assertionConsumerServiceUrl - a required value
+		 * @return this {@code Builder}
+		 */
+		public Builder assertionConsumerServiceUrl(String assertionConsumerServiceUrl) {
+			this.assertionConsumerServiceUrl = assertionConsumerServiceUrl;
+			return this;
+		}
+
+		/**
+		 * Sets the {@code RelayState} parameter that will accompany this AuthNRequest
+		 * @param relayState the relay state value, unencoded. if null or empty, the parameter will be removed from the map.
+		 * @return this object
+		 */
+		public Builder relayState(String relayState) {
+			this.relayState = relayState;
+			return this;
+		}
+
+		/**
+		 * Creates a {@link Saml2AuthenticationRequestContext} object.
+		 * @return the Saml2AuthenticationRequest object
+		 * @throws {@link IllegalArgumentException} if a required property is not set
+		 */
+		public Saml2AuthenticationRequestContext build() {
+			return new Saml2AuthenticationRequestContext(
+					this.relyingPartyRegistration,
+					this.issuer,
+					this.assertionConsumerServiceUrl,
+					this.relayState
+			);
+		}
+	}
+}

+ 86 - 9
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,25 +17,102 @@
 package org.springframework.security.saml2.provider.service.authentication;
 
 import org.springframework.security.saml2.Saml2Exception;
+import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequest.withAuthenticationRequestContext;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlDeflate;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlEncode;
 
 /**
- * Component that generates an AuthenticationRequest, <code>samlp:AuthnRequestType</code> as defined by
- * https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf
+ * Component that generates AuthenticationRequest, <code>samlp:AuthnRequestType</code> XML, and accompanying
+ * signature data.
+ * as defined by https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf
  * Page 50, Line 2147
  *
  * @since 5.2
  */
 public interface Saml2AuthenticationRequestFactory {
+
 	/**
-	 * Creates an authentication request from the Service Provider, sp,
-	 * to the Identity Provider, idp.
+	 * Creates an authentication request from the Service Provider, sp, to the Identity Provider, idp.
 	 * The authentication result is an XML string that may be signed, encrypted, both or neither.
+	 * This method only returns the {@code SAMLRequest} string for the request, and for a complete
+	 * set of data parameters please use {@link #createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext)}
+	 * or {@link #createPostAuthenticationRequest(Saml2AuthenticationRequestContext)}
 	 *
-	 * @param request - information about the identity provider, the recipient of this authentication request and
-	 *                accompanying data
-	 * @return XML data in the format of a String. This data may be signed, encrypted, both signed and encrypted or
-	 * neither signed and encrypted
+	 * @param request information about the identity provider,
+	 * the recipient of this authentication request and accompanying data
+	 * @return XML data in the format of a String. This data may be signed, encrypted, both signed and encrypted with the
+	 * signature embedded in the XML or neither signed and encrypted
 	 * @throws Saml2Exception when a SAML library exception occurs
+	 * @since 5.2
+	 * @deprecated please use {@link #createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext)}
+	 * or {@link #createPostAuthenticationRequest(Saml2AuthenticationRequestContext)}
+	 * This method will be removed in future versions of Spring Security
 	 */
+	@Deprecated
 	String createAuthenticationRequest(Saml2AuthenticationRequest request);
+
+	/**
+	 * Creates all the necessary AuthNRequest parameters for a REDIRECT binding.
+	 * If the {@link Saml2AuthenticationRequestContext} doesn't contain any {@link Saml2X509CredentialType#SIGNING} credentials
+	 * the result will not contain any signatures.
+	 * The data set will be signed and encoded for REDIRECT binding including the DEFLATE encoding.
+	 * It will contain the following parameters to be sent as part of the query string:
+	 * {@code SAMLRequest, RelayState, SigAlg, Signature}.
+	 * <i>The default implementation, for sake of backwards compatibility, of this method returns the
+	 * SAMLRequest message with an XML signature embedded, that should only be used for the{@link Saml2MessageBinding#POST}
+	 * binding, but works over {@link Saml2MessageBinding#POST} with most providers.</i>
+	 * @param context - information about the identity provider, the recipient of this authentication request and
+	 * accompanying data
+	 * @return a {@link Saml2RedirectAuthenticationRequest} object with applicable http parameters
+	 * necessary to make the AuthNRequest over a POST or REDIRECT binding.
+	 * All parameters will be SAML encoded/deflated, but escaped, ie URI encoded or encoded for Form Data.
+	 * @throws Saml2Exception when a SAML library exception occurs
+	 * @since 5.3
+	 */
+	default Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest(
+			Saml2AuthenticationRequestContext context
+	) {
+		//backwards compatible with 5.2.x settings
+		Saml2AuthenticationRequest.Builder resultBuilder = withAuthenticationRequestContext(context);
+		String samlRequest = createAuthenticationRequest(resultBuilder.build());
+		samlRequest = samlEncode(samlDeflate(samlRequest));
+		return Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context)
+				.samlRequest(samlRequest)
+				.build();
+	}
+
+
+	/**
+	 * Creates all the necessary AuthNRequest parameters for a POST binding.
+	 * If the {@link Saml2AuthenticationRequestContext} doesn't contain any {@link Saml2X509CredentialType#SIGNING} credentials
+	 * the result will not contain any signatures.
+	 * The data set will be signed and encoded for  POST binding and if applicable signed with XML signatures.
+	 * will contain the following parameters to be sent as part of the form data: {@code SAMLRequest, RelayState}.
+	 * <i>The default implementation of this method returns the SAMLRequest message with an XML signature embedded,
+	 * that should only be used for the {@link Saml2MessageBinding#POST} binding.</i>
+	 * @param context - information about the identity provider, the recipient of this authentication request and
+	 * accompanying data
+	 * @return a {@link Saml2PostAuthenticationRequest} object with applicable http parameters
+	 * necessary to make the AuthNRequest over a POST binding.
+	 * All parameters will be SAML encoded but not escaped for Form Data.
+	 * @throws Saml2Exception when a SAML library exception occurs
+	 * @since 5.3
+	 */
+	default Saml2PostAuthenticationRequest createPostAuthenticationRequest(
+			Saml2AuthenticationRequestContext context
+	) {
+		//backwards compatible with 5.2.x settings
+		Saml2AuthenticationRequest.Builder resultBuilder = withAuthenticationRequestContext(context);
+		String samlRequest = createAuthenticationRequest(resultBuilder.build());
+		samlRequest = samlEncode(samlRequest.getBytes(StandardCharsets.UTF_8));
+		return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context)
+				.samlRequest(samlRequest)
+				.build();
+	}
+
 }

+ 85 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+
+import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.POST;
+
+/**
+ * Data holder for information required to send an {@code AuthNRequest} over a POST binding
+ * from the service provider to the identity provider
+ * https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
+ *
+ * @see Saml2AuthenticationRequestFactory
+ * @since 5.3
+ */
+public class Saml2PostAuthenticationRequest extends AbstractSaml2AuthenticationRequest {
+
+	private Saml2PostAuthenticationRequest(
+			String samlRequest,
+			String relayState,
+			String authenticationRequestUri) {
+		super(samlRequest, relayState, authenticationRequestUri);
+	}
+
+	/**
+	 * @return {@link Saml2MessageBinding#POST}
+	 */
+	@Override
+	public Saml2MessageBinding getBinding() {
+		return POST;
+	}
+
+	/**
+	 * Constructs a {@link Builder} from a {@link Saml2AuthenticationRequestContext} object.
+	 * By default the {@link Saml2PostAuthenticationRequest#getAuthenticationRequestUri()} will be set to the
+	 * {@link Saml2AuthenticationRequestContext#getDestination()} value.
+	 * @param context input providing {@code Destination}, {@code RelayState}, and {@code Issuer} objects.
+	 * @return a modifiable builder object
+	 */
+	public static Builder withAuthenticationRequestContext(Saml2AuthenticationRequestContext context) {
+		return new Builder()
+				.authenticationRequestUri(context.getDestination())
+				.relayState(context.getRelayState())
+				;
+	}
+
+	/**
+	 * Builder class for a {@link Saml2PostAuthenticationRequest} object.
+	 */
+	public static class Builder extends AbstractSaml2AuthenticationRequest.Builder<Builder> {
+
+		private Builder() {
+			super();
+		}
+
+		/**
+		 * Constructs an immutable {@link Saml2PostAuthenticationRequest} object.
+		 * @return an immutable {@link Saml2PostAuthenticationRequest} object.
+		 */
+		public Saml2PostAuthenticationRequest build() {
+			return new Saml2PostAuthenticationRequest(
+					this.samlRequest,
+					this.relayState,
+					this.authenticationRequestUri
+			);
+		}
+	}
+
+
+}

+ 133 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java

@@ -0,0 +1,133 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.authentication;
+
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+
+import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.REDIRECT;
+
+/**
+ * Data holder for information required to send an {@code AuthNRequest} over a REDIRECT binding
+ * from the service provider to the identity provider
+ * https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
+ *
+ * @see Saml2AuthenticationRequestFactory
+ * @since 5.3
+ */
+public class Saml2RedirectAuthenticationRequest extends AbstractSaml2AuthenticationRequest {
+
+	private final String sigAlg;
+	private final String signature;
+
+	private Saml2RedirectAuthenticationRequest(
+			String samlRequest,
+			String sigAlg,
+			String signature,
+			String relayState,
+			String authenticationRequestUri) {
+		super(samlRequest, relayState, authenticationRequestUri);
+		this.sigAlg = sigAlg;
+		this.signature = signature;
+	}
+
+	/**
+	 * Returns the SigAlg value for {@link Saml2MessageBinding#REDIRECT} requests
+	 * @return the SigAlg value
+	 */
+	public String getSigAlg() {
+		return this.sigAlg;
+	}
+
+	/**
+	 * Returns the Signature value for {@link Saml2MessageBinding#REDIRECT} requests
+	 * @return the Signature value
+	 */
+	public String getSignature() {
+		return this.signature;
+	}
+
+	/**
+     * @return {@link Saml2MessageBinding#REDIRECT}
+	 */
+	@Override
+	public Saml2MessageBinding getBinding() {
+		return REDIRECT;
+	}
+
+	/**
+	 * Constructs a {@link Saml2RedirectAuthenticationRequest.Builder} from a {@link Saml2AuthenticationRequestContext} object.
+	 * By default the {@link Saml2RedirectAuthenticationRequest#getAuthenticationRequestUri()} will be set to the
+	 * {@link Saml2AuthenticationRequestContext#getDestination()} value.
+	 * @param context input providing {@code Destination}, {@code RelayState}, and {@code Issuer} objects.
+	 * @return a modifiable builder object
+	 */
+	public static Builder withAuthenticationRequestContext(Saml2AuthenticationRequestContext context) {
+		return new Builder()
+				.authenticationRequestUri(context.getDestination())
+				.relayState(context.getRelayState())
+				;
+	}
+
+	/**
+	 * Builder class for a {@link Saml2RedirectAuthenticationRequest} object.
+	 */
+	public static class Builder extends AbstractSaml2AuthenticationRequest.Builder<Builder> {
+		private String sigAlg;
+		private String signature;
+
+		private Builder() {
+			super();
+		}
+
+		/**
+		 * Sets the {@code SigAlg} parameter that will accompany this AuthNRequest
+		 * @param sigAlg the SigAlg parameter value.
+		 * @return this object
+		 */
+		public Builder sigAlg(String sigAlg) {
+			this.sigAlg = sigAlg;
+			return _this();
+		}
+
+		/**
+		 * Sets the {@code Signature} parameter that will accompany this AuthNRequest
+		 * @param signature the Signature parameter value.
+		 * @return this object
+		 */
+		public Builder signature(String signature) {
+			this.signature = signature;
+			return _this();
+		}
+
+		/**
+		 * Constructs an immutable {@link Saml2RedirectAuthenticationRequest} object.
+		 * @return an immutable {@link Saml2RedirectAuthenticationRequest} object.
+		 */
+		public Saml2RedirectAuthenticationRequest build() {
+			return new Saml2RedirectAuthenticationRequest(
+					this.samlRequest,
+					this.sigAlg,
+					this.signature,
+					this.relayState,
+					this.authenticationRequestUri
+			);
+		}
+
+	}
+
+
+}

+ 73 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.authentication;
+
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.security.saml2.Saml2Exception;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterOutputStream;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.zip.Deflater.DEFLATED;
+
+/**
+ * @since 5.3
+ */
+final class Saml2Utils {
+
+
+	private static Base64 BASE64 = new Base64(0, new byte[]{'\n'});
+
+	static String samlEncode(byte[] b) {
+		return BASE64.encodeAsString(b);
+	}
+
+	static byte[] samlDecode(String s) {
+		return BASE64.decode(s);
+	}
+
+	static byte[] samlDeflate(String s) {
+		try {
+			ByteArrayOutputStream b = new ByteArrayOutputStream();
+			DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(DEFLATED, true));
+			deflater.write(s.getBytes(UTF_8));
+			deflater.finish();
+			return b.toByteArray();
+		}
+		catch (IOException e) {
+			throw new Saml2Exception("Unable to deflate string", e);
+		}
+	}
+
+	static String samlInflate(byte[] b) {
+		try {
+			ByteArrayOutputStream out = new ByteArrayOutputStream();
+			InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
+			iout.write(b);
+			iout.finish();
+			return new String(out.toByteArray(), UTF_8);
+		}
+		catch (IOException e) {
+			throw new Saml2Exception("Unable to inflate string", e);
+		}
+	}
+}

+ 45 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.registration;
+
+/**
+ * The type of bindings that messages are exchanged using
+ * Supported bindings are {@code urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST}
+ * and {@code urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect}.
+ * In addition there is support for {@code urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect}
+ * with an XML signature in the message rather than query parameters.
+ * @since 5.3
+ */
+public enum Saml2MessageBinding {
+
+	POST("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
+	REDIRECT("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
+
+	private final String urn;
+
+	Saml2MessageBinding(String s) {
+		this.urn = s;
+	}
+
+	/**
+	 * Returns the URN value from the SAML 2 specification for this binding.
+	 * @return URN value representing this binding
+	 */
+	public String getUrn() {
+		return urn;
+	}
+}

+ 88 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.servlet.filter;
+
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.springframework.security.web.util.UrlUtils.buildFullRequestUrl;
+import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl;
+
+/**
+ * @since 5.3
+ */
+final class Saml2ServletUtils {
+
+	private static final char PATH_DELIMITER = '/';
+
+	static String getServiceProviderEntityId(RelyingPartyRegistration rp, HttpServletRequest request) {
+		return resolveUrlTemplate(
+				rp.getLocalEntityIdTemplate(),
+				getApplicationUri(request),
+				rp.getRemoteIdpEntityId(),
+				rp.getRegistrationId()
+		);
+	}
+
+	static String resolveUrlTemplate(String template, String baseUrl, String entityId, String registrationId) {
+		if (!StringUtils.hasText(template)) {
+			return baseUrl;
+		}
+
+		Map<String, String> uriVariables = new HashMap<>();
+		UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl)
+				.replaceQuery(null)
+				.fragment(null)
+				.build();
+		String scheme = uriComponents.getScheme();
+		uriVariables.put("baseScheme", scheme == null ? "" : scheme);
+		String host = uriComponents.getHost();
+		uriVariables.put("baseHost", host == null ? "" : host);
+		// following logic is based on HierarchicalUriComponents#toUriString()
+		int port = uriComponents.getPort();
+		uriVariables.put("basePort", port == -1 ? "" : ":" + port);
+		String path = uriComponents.getPath();
+		if (StringUtils.hasLength(path)) {
+			if (path.charAt(0) != PATH_DELIMITER) {
+				path = PATH_DELIMITER + path;
+			}
+		}
+		uriVariables.put("basePath", path == null ? "" : path);
+		uriVariables.put("baseUrl", uriComponents.toUriString());
+		uriVariables.put("entityId", StringUtils.hasText(entityId) ? entityId : "");
+		uriVariables.put("registrationId", StringUtils.hasText(registrationId) ? registrationId : "");
+
+		return UriComponentsBuilder.fromUriString(template)
+				.buildAndExpand(uriVariables)
+				.toUriString();
+	}
+
+	static String getApplicationUri(HttpServletRequest request) {
+		UriComponents uriComponents = fromHttpUrl(buildFullRequestUrl(request))
+				.replacePath(request.getContextPath())
+				.replaceQuery(null)
+				.fragment(null)
+				.build();
+		return uriComponents.toUriString();
+	}
+}

+ 8 - 68
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2Utils.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,43 +18,34 @@ package org.springframework.security.saml2.provider.service.servlet.filter;
 
 import org.apache.commons.codec.binary.Base64;
 import org.springframework.security.saml2.Saml2Exception;
-import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
-import org.springframework.util.StringUtils;
-import org.springframework.web.util.UriComponents;
-import org.springframework.web.util.UriComponentsBuilder;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
 import java.util.zip.Inflater;
 import java.util.zip.InflaterOutputStream;
-import javax.servlet.http.HttpServletRequest;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.zip.Deflater.DEFLATED;
-import static org.springframework.security.web.util.UrlUtils.buildFullRequestUrl;
-import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl;
 
 /**
- * @since 5.2
+ * @since 5.3
  */
 final class Saml2Utils {
 
-	private static final char PATH_DELIMITER = '/';
-	private static org.apache.commons.codec.binary.Base64 BASE64 = new Base64(0, new byte[]{'\n'});
 
-	static String encode(byte[] b) {
+	private static Base64 BASE64 = new Base64(0, new byte[]{'\n'});
+
+	static String samlEncode(byte[] b) {
 		return BASE64.encodeAsString(b);
 	}
 
-	static byte[] decode(String s) {
+	static byte[] samlDecode(String s) {
 		return BASE64.decode(s);
 	}
 
-	static byte[] deflate(String s) {
+	static byte[] samlDeflate(String s) {
 		try {
 			ByteArrayOutputStream b = new ByteArrayOutputStream();
 			DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(DEFLATED, true));
@@ -67,7 +58,7 @@ final class Saml2Utils {
 		}
 	}
 
-	static String inflate(byte[] b) {
+	static String samlInflate(byte[] b) {
 		try {
 			ByteArrayOutputStream out = new ByteArrayOutputStream();
 			InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
@@ -79,55 +70,4 @@ final class Saml2Utils {
 			throw new Saml2Exception("Unable to inflate string", e);
 		}
 	}
-
-	static String getServiceProviderEntityId(RelyingPartyRegistration rp, HttpServletRequest request) {
-		return resolveUrlTemplate(
-				rp.getLocalEntityIdTemplate(),
-				getApplicationUri(request),
-				rp.getRemoteIdpEntityId(),
-				rp.getRegistrationId()
-		);
-	}
-
-	static String resolveUrlTemplate(String template, String baseUrl, String entityId, String registrationId) {
-		if (!StringUtils.hasText(template)) {
-			return baseUrl;
-		}
-
-		Map<String, String> uriVariables = new HashMap<>();
-		UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl)
-				.replaceQuery(null)
-				.fragment(null)
-				.build();
-		String scheme = uriComponents.getScheme();
-		uriVariables.put("baseScheme", scheme == null ? "" : scheme);
-		String host = uriComponents.getHost();
-		uriVariables.put("baseHost", host == null ? "" : host);
-		// following logic is based on HierarchicalUriComponents#toUriString()
-		int port = uriComponents.getPort();
-		uriVariables.put("basePort", port == -1 ? "" : ":" + port);
-		String path = uriComponents.getPath();
-		if (StringUtils.hasLength(path)) {
-			if (path.charAt(0) != PATH_DELIMITER) {
-				path = PATH_DELIMITER + path;
-			}
-		}
-		uriVariables.put("basePath", path == null ? "" : path);
-		uriVariables.put("baseUrl", uriComponents.toUriString());
-		uriVariables.put("entityId", StringUtils.hasText(entityId) ? entityId : "");
-		uriVariables.put("registrationId", StringUtils.hasText(registrationId) ? registrationId : "");
-
-		return UriComponentsBuilder.fromUriString(template)
-				.buildAndExpand(uriVariables)
-				.toUriString();
-	}
-
-	static String getApplicationUri(HttpServletRequest request) {
-		UriComponents uriComponents = fromHttpUrl(buildFullRequestUrl(request))
-				.replacePath(request.getContextPath())
-				.replaceQuery(null)
-				.fragment(null)
-				.build();
-		return uriComponents.toUriString();
-	}
 }

+ 5 - 5
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java

@@ -61,8 +61,8 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce
 	 * @param filterProcessesUrl the processing URL, must contain a {registrationId} variable. Required.
 	 */
 	public Saml2WebSsoAuthenticationFilter(
-				RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
-				String filterProcessesUrl) {
+			RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
+			String filterProcessesUrl) {
 		super(filterProcessesUrl);
 		Assert.notNull(relyingPartyRegistrationRepository, "relyingPartyRegistrationRepository cannot be null");
 		Assert.hasText(filterProcessesUrl, "filterProcessesUrl must contain a URL pattern");
@@ -86,7 +86,7 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce
 	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
 			throws AuthenticationException {
 		String saml2Response = request.getParameter("SAMLResponse");
-		byte[] b = Saml2Utils.decode(saml2Response);
+		byte[] b = Saml2Utils.samlDecode(saml2Response);
 
 		String responseXml = inflateIfRequired(request, b);
 		String registrationId = this.matcher.matcher(request).getVariables().get("registrationId");
@@ -97,7 +97,7 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce
 					"Relying Party Registration not found with ID: " + registrationId);
 			throw new Saml2AuthenticationException(saml2Error);
 		}
-		String localSpEntityId = Saml2Utils.getServiceProviderEntityId(rp, request);
+		String localSpEntityId = Saml2ServletUtils.getServiceProviderEntityId(rp, request);
 		final Saml2AuthenticationToken authentication = new Saml2AuthenticationToken(
 				responseXml,
 				request.getRequestURL().toString(),
@@ -110,7 +110,7 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce
 
 	private String inflateIfRequired(HttpServletRequest request, byte[] b) {
 		if (HttpMethod.GET.matches(request.getMethod())) {
-			return Saml2Utils.inflate(b);
+			return Saml2Utils.samlInflate(b);
 		}
 		else {
 			return new String(b, UTF_8);

+ 35 - 30
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,29 +17,28 @@
 package org.springframework.security.saml2.provider.service.servlet.filter;
 
 import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
-import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequest;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext;
 import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
+import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
 import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 import org.springframework.web.util.UriUtils;
 
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
 
 import static java.lang.String.format;
-import static org.springframework.security.saml2.provider.service.servlet.filter.Saml2Utils.deflate;
-import static org.springframework.security.saml2.provider.service.servlet.filter.Saml2Utils.encode;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.springframework.util.StringUtils.hasText;
 
 /**
  * @since 5.2
@@ -47,9 +46,7 @@ import static org.springframework.security.saml2.provider.service.servlet.filter
 public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter {
 
 	private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
-
 	private RequestMatcher redirectMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}");
-
 	private Saml2AuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory();
 
 	public Saml2WebSsoAuthenticationRequestFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
@@ -91,39 +88,47 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
 	}
 
 	private String createSamlRequestRedirectUrl(HttpServletRequest request, RelyingPartyRegistration relyingParty) {
-		Saml2AuthenticationRequest authNRequest = createAuthenticationRequest(relyingParty, request);
-		String xml = this.authenticationRequestFactory.createAuthenticationRequest(authNRequest);
-		String encoded = encode(deflate(xml));
-		String relayState = request.getParameter("RelayState");
-		UriComponentsBuilder uriBuilder = UriComponentsBuilder
-				.fromUriString(relyingParty.getIdpWebSsoUrl())
-				.queryParam("SAMLRequest", UriUtils.encode(encoded, StandardCharsets.ISO_8859_1));
-
-		if (StringUtils.hasText(relayState)) {
-			uriBuilder.queryParam("RelayState", UriUtils.encode(relayState, StandardCharsets.ISO_8859_1));
-		}
-
+		Saml2AuthenticationRequestContext authnRequest = createRedirectAuthenticationRequestContext(relyingParty, request);
+		Saml2RedirectAuthenticationRequest authNData =
+				this.authenticationRequestFactory.createRedirectAuthenticationRequest(authnRequest);
+		UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authNData.getAuthenticationRequestUri());
+		addParameter("SAMLRequest", authNData.getSamlRequest(), uriBuilder);
+		addParameter("RelayState", authNData.getRelayState(), uriBuilder);
+		addParameter("SigAlg", authNData.getSigAlg(), uriBuilder);
+		addParameter("Signature", authNData.getSignature(), uriBuilder);
 		return uriBuilder
 				.build(true)
 				.toUriString();
 	}
 
-	private Saml2AuthenticationRequest createAuthenticationRequest(RelyingPartyRegistration relyingParty, HttpServletRequest request) {
-		String localSpEntityId = Saml2Utils.getServiceProviderEntityId(relyingParty, request);
-		return Saml2AuthenticationRequest
+	private void addParameter(String name, String value, UriComponentsBuilder builder) {
+		Assert.hasText(name, "name cannot be empty or null");
+		if (hasText(value)) {
+			builder.queryParam(
+					UriUtils.encode(name, ISO_8859_1),
+					UriUtils.encode(value, ISO_8859_1)
+			);
+		}
+	}
+
+	private Saml2AuthenticationRequestContext createRedirectAuthenticationRequestContext(
+			RelyingPartyRegistration relyingParty,
+			HttpServletRequest request) {
+		String localSpEntityId = Saml2ServletUtils.getServiceProviderEntityId(relyingParty, request);
+		return Saml2AuthenticationRequestContext
 				.builder()
 				.issuer(localSpEntityId)
-				.destination(relyingParty.getIdpWebSsoUrl())
-				.credentials(c -> c.addAll(relyingParty.getCredentials()))
+				.relyingPartyRegistration(relyingParty)
 				.assertionConsumerServiceUrl(
-						Saml2Utils.resolveUrlTemplate(
+						Saml2ServletUtils.resolveUrlTemplate(
 								relyingParty.getAssertionConsumerServiceUrlTemplate(),
-								Saml2Utils.getApplicationUri(request),
+								Saml2ServletUtils.getApplicationUri(request),
 								relyingParty.getRemoteIdpEntityId(),
 								relyingParty.getRegistrationId()
 						)
 				)
-				.build();
+				.relayState(request.getParameter("RelayState"))
+				.build()
+				;
 	}
-
 }

+ 59 - 11
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,39 +23,77 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.opensaml.saml.common.xml.SAMLConstants;
 import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
 
+import java.nio.charset.StandardCharsets;
+
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlDecode;
 import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials;
+import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.POST;
+import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.REDIRECT;
 
+/**
+ * Tests for {@link OpenSamlAuthenticationRequestFactory}
+ */
 public class OpenSamlAuthenticationRequestFactoryTests {
 
 	private OpenSamlAuthenticationRequestFactory factory;
-	private Saml2AuthenticationRequest request;
+	private Saml2AuthenticationRequestContext.Builder contextBuilder;
+	private Saml2AuthenticationRequestContext context;
 
 	@Rule
 	public ExpectedException exception = ExpectedException.none();
 
 	@Before
 	public void setUp() {
-		request = Saml2AuthenticationRequest.builder()
-				.issuer("https://issuer")
-				.destination("https://destination/sso")
-				.assertionConsumerServiceUrl("https://issuer/sso")
+		RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("id")
+				.assertionConsumerServiceUrlTemplate("template")
+				.idpWebSsoUrl("https://destination/sso")
+				.remoteIdpEntityId("remote-entity-id")
+				.localEntityIdTemplate("local-entity-id")
 				.credentials(c -> c.addAll(relyingPartyCredentials()))
 				.build();
+		contextBuilder = Saml2AuthenticationRequestContext.builder()
+				.issuer("https://issuer")
+				.relyingPartyRegistration(registration)
+				.assertionConsumerServiceUrl("https://issuer/sso");
+		context = contextBuilder.build();
 		factory = new OpenSamlAuthenticationRequestFactory();
 	}
 
+	@Test
+	public void createAuthenticationRequestWhenInvokingDeprecatedMethodThenReturnsXML() {
+		Saml2AuthenticationRequest request = Saml2AuthenticationRequest.withAuthenticationRequestContext(context).build();
+		String result = factory.createAuthenticationRequest(request);
+		assertThat(result.replace("\n", "")).startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?><saml2p:AuthnRequest");
+	}
+
+	@Test
+	public void createRedirectAuthenticationRequestWhenUsingContextThenAllValuesAreSet() {
+		context = contextBuilder
+				.relayState("Relay State Value")
+				.build();
+		Saml2RedirectAuthenticationRequest result = factory.createRedirectAuthenticationRequest(context);
+		assertThat(result.getSamlRequest()).isNotEmpty();
+		assertThat(result.getRelayState()).isEqualTo("Relay State Value");
+		assertThat(result.getSigAlg()).isNotEmpty();
+		assertThat(result.getSignature()).isNotEmpty();
+		assertThat(result.getBinding()).isEqualTo(REDIRECT);
+	}
+
 	@Test
 	public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() {
-		AuthnRequest authn = getAuthNRequest();
+		AuthnRequest authn = getAuthNRequest(POST);
 		Assert.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding());
 	}
 
 	@Test
 	public void createAuthenticationRequestWhenSetUriThenReturnsCorrectBinding() {
 		factory.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
-		AuthnRequest authn = getAuthNRequest();
+		AuthnRequest authn = getAuthNRequest(POST);
 		Assert.assertEquals(SAMLConstants.SAML2_REDIRECT_BINDING_URI, authn.getProtocolBinding());
 	}
 
@@ -66,8 +104,18 @@ public class OpenSamlAuthenticationRequestFactoryTests {
 		factory.setProtocolBinding("my-invalid-binding");
 	}
 
-	private AuthnRequest getAuthNRequest() {
-		String xml = factory.createAuthenticationRequest(request);
-		return (AuthnRequest) OpenSamlImplementation.getInstance().resolve(xml);
+	private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) {
+		AbstractSaml2AuthenticationRequest result = (binding == REDIRECT) ?
+				factory.createRedirectAuthenticationRequest(context) :
+				factory.createPostAuthenticationRequest(context);
+		String samlRequest = result.getSamlRequest();
+		assertThat(samlRequest).isNotEmpty();
+		if (result.getBinding() == REDIRECT) {
+			samlRequest = Saml2Utils.samlInflate(samlDecode(samlRequest));
+		}
+		else {
+			samlRequest = new String(samlDecode(samlRequest), StandardCharsets.UTF_8);
+		}
+		return (AuthnRequest) OpenSamlImplementation.getInstance().resolve(samlRequest);
 	}
 }

+ 55 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementationTests.java

@@ -17,6 +17,23 @@
 package org.springframework.security.saml2.provider.service.authentication;
 
 import org.junit.Test;
+import org.opensaml.security.credential.BasicCredential;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialSupport;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.xmlsec.crypto.XMLSigningUtil;
+import org.springframework.security.saml2.credentials.Saml2X509Credential;
+import org.springframework.web.util.UriUtils;
+
+import java.util.List;
+import java.util.Map;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
+import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.assertingPartyCredentials;
+import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials;
 
 public class OpenSamlImplementationTests {
 
@@ -24,4 +41,42 @@ public class OpenSamlImplementationTests {
 	public void getInstance() {
 		OpenSamlImplementation.getInstance();
 	}
+
+	@Test
+	public void signQueryParametersWhenDataSuppliedReturnsValidSignature() throws Exception {
+		OpenSamlImplementation impl = OpenSamlImplementation.getInstance();
+		List<Saml2X509Credential> signCredentials = relyingPartyCredentials();
+		List<Saml2X509Credential> verifyCredentials = assertingPartyCredentials();
+		String samlRequest = "saml-request-example";
+		String encoded = Saml2Utils.samlEncode(samlRequest.getBytes(UTF_8));
+		String relayState = "test relay state";
+		Map<String, String> parameters = impl.signQueryParameters(signCredentials, encoded, relayState);
+
+		String queryString = "SAMLRequest=" +
+				UriUtils.encode(encoded, ISO_8859_1) +
+				"&RelayState=" +
+				UriUtils.encode(relayState, ISO_8859_1) +
+				"&SigAlg=" +
+				UriUtils.encode(ALGO_ID_SIGNATURE_RSA_SHA256, ISO_8859_1);
+
+
+		byte[] signature = Saml2Utils.samlDecode(parameters.get("Signature"));
+		boolean result = XMLSigningUtil.verifyWithURI(
+				getOpenSamlCredential(verifyCredentials.get(1), "local-sp-entity-id", UsageType.SIGNING),
+				ALGO_ID_SIGNATURE_RSA_SHA256,
+				signature,
+				queryString.getBytes(UTF_8)
+		);
+		assertThat(result).isTrue();
+	}
+
+	private Credential getOpenSamlCredential(Saml2X509Credential credential, String localSpEntityId, UsageType usageType) {
+		BasicCredential cred = CredentialSupport.getSimpleCredential(
+				credential.getCertificate(),
+				credential.getPrivateKey()
+		);
+		cred.setEntityId(localSpEntityId);
+		cred.setUsageType(usageType);
+		return cred;
+	}
 }

+ 72 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.security.saml2.provider.service.authentication;
+
+import org.junit.Test;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlDecode;
+import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlInflate;
+import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials;
+
+/**
+ * Tests for {@link Saml2AuthenticationRequestFactory} default interface methods
+ */
+public class Saml2AuthenticationRequestFactoryTests {
+
+	private RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("id")
+			.assertionConsumerServiceUrlTemplate("template")
+			.idpWebSsoUrl("https://example.com/destination")
+			.remoteIdpEntityId("remote-entity-id")
+			.localEntityIdTemplate("local-entity-id")
+			.credentials(c -> c.addAll(relyingPartyCredentials()))
+			.build();
+
+	@Test
+	public void createAuthenticationRequestParametersWhenRedirectDefaultIsUsedMessageIsDeflatedAndEncoded() {
+		final String value = "Test String: "+ UUID.randomUUID().toString();
+		Saml2AuthenticationRequestFactory factory = request -> value;
+		Saml2AuthenticationRequestContext request = Saml2AuthenticationRequestContext.builder()
+				.relyingPartyRegistration(registration)
+				.issuer("https://example.com/issuer")
+				.assertionConsumerServiceUrl("https://example.com/acs-url")
+				.build();
+		Saml2RedirectAuthenticationRequest response = factory.createRedirectAuthenticationRequest(request);
+		String resultValue = response.getSamlRequest();
+		byte[] decoded = samlDecode(resultValue);
+		String inflated = samlInflate(decoded);
+		assertThat(inflated).isEqualTo(value);
+	}
+
+	@Test
+	public void createAuthenticationRequestParametersWhenPostDefaultIsUsedMessageIsEncoded() {
+		final String value = "Test String: "+ UUID.randomUUID().toString();
+		Saml2AuthenticationRequestFactory factory = request -> value;
+		Saml2AuthenticationRequestContext request = Saml2AuthenticationRequestContext.builder()
+				.relyingPartyRegistration(registration)
+				.issuer("https://example.com/issuer")
+				.assertionConsumerServiceUrl("https://example.com/acs-url")
+				.build();
+		Saml2PostAuthenticationRequest response = factory.createPostAuthenticationRequest(request);
+		String resultValue = response.getSamlRequest();
+		byte[] decoded = samlDecode(resultValue);
+		assertThat(new String(decoded)).isEqualTo(value);
+	}
+}

+ 2 - 2
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2UtilsTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -50,7 +50,7 @@ public class Saml2UtilsTests {
 	@Test
 	public void decodeWhenUsingSamlUtilsBase64ThenXmlIsValid() throws Exception {
 		String responseUrlDecoded = getSsoCircleEncodedXml();
-		String xml = new String(Saml2Utils.decode(responseUrlDecoded), UTF_8);
+		String xml = new String(Saml2Utils.samlDecode(responseUrlDecoded), UTF_8);
 		validateSsoCircleXml(xml);
 	}
 

+ 50 - 29
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,10 +17,9 @@
 package org.springframework.security.saml2.provider.service.servlet.filter;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletResponse;
 
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.springframework.mock.web.MockFilterChain;
@@ -28,18 +27,22 @@ import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.web.util.UriUtils;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.springframework.security.saml2.provider.service.servlet.filter.TestSaml2SigningCredentials.signingCredential;
 
 public class Saml2WebSsoAuthenticationRequestFilterTests {
 
+	private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO";
 	private Saml2WebSsoAuthenticationRequestFilter filter;
 	private RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class);
 	private MockHttpServletRequest request;
-	private HttpServletResponse response;
+	private MockHttpServletResponse response;
 	private MockFilterChain filterChain;
+	private RelyingPartyRegistration.Builder rpBuilder;
 
 	@Before
 	public void setup() {
@@ -49,43 +52,61 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
 		request.setPathInfo("/saml2/authenticate/registration-id");
 
 		filterChain = new MockFilterChain();
-	}
 
-	@Test
-	public void createSamlRequestRedirectUrlAndReturnUrlWithoutRelayState() throws ServletException, IOException {
-		RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration
+		rpBuilder = RelyingPartyRegistration
 				.withRegistrationId("registration-id")
 				.remoteIdpEntityId("idp-entity-id")
-				.idpWebSsoUrl("sso-url")
+				.idpWebSsoUrl(IDP_SSO_URL)
 				.assertionConsumerServiceUrlTemplate("template")
-				.credentials(c -> c.add(signingCredential()))
-				.build();
-
-		when(repository.findByRegistrationId("registration-id"))
-				.thenReturn(relyingPartyRegistration);
+				.credentials(c -> c.add(signingCredential()));
+	}
 
+	@Test
+	public void doFilterWhenNoRelayStateThenRedirectDoesNotContainParameter() throws ServletException, IOException {
+		when(repository.findByRegistrationId("registration-id")).thenReturn(rpBuilder.build());
 		filter.doFilterInternal(request, response, filterChain);
-
-		Assert.assertFalse(response.getHeader("Location").contains("RelayState="));
+		assertThat(response.getHeader("Location"))
+				.doesNotContain("RelayState=")
+				.startsWith(IDP_SSO_URL);
 	}
 
 	@Test
-	public void createSamlRequestRedirectUrlAndReturnUrlWithRelayState() throws ServletException, IOException {
-		RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration
-				.withRegistrationId("registration-id")
-				.remoteIdpEntityId("idp-entity-id")
-				.idpWebSsoUrl("sso-url")
-				.assertionConsumerServiceUrlTemplate("template")
-				.credentials(c -> c.add(signingCredential()))
-				.build();
-
-		when(repository.findByRegistrationId("registration-id"))
-				.thenReturn(relyingPartyRegistration);
-
+	public void doFilterWhenRelayStateThenRedirectDoesContainParameter() throws ServletException, IOException {
+		when(repository.findByRegistrationId("registration-id")).thenReturn(rpBuilder.build());
 		request.setParameter("RelayState", "my-relay-state");
+		filter.doFilterInternal(request, response, filterChain);
+		assertThat(response.getHeader("Location"))
+				.contains("RelayState=my-relay-state")
+				.startsWith(IDP_SSO_URL);
+	}
 
+	@Test
+	public void doFilterWhenRelayStateThatRequiresEncodingThenRedirectDoesContainsEncodedParameter() throws Exception {
+		when(repository.findByRegistrationId("registration-id")).thenReturn(rpBuilder.build());
+		final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
+		final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
+		request.setParameter("RelayState", relayStateValue);
 		filter.doFilterInternal(request, response, filterChain);
+		assertThat(response.getHeader("Location"))
+				.contains("RelayState="+relayStateEncoded)
+				.startsWith(IDP_SSO_URL);
+	}
 
-		Assert.assertTrue(response.getHeader("Location").contains("RelayState=my-relay-state"));
+	@Test
+	public void doFilterWhenSimpleSignatureSpecifiedThenSignatureParametersAreInTheRedirectURL() throws Exception {
+		when(repository.findByRegistrationId("registration-id")).thenReturn(
+				rpBuilder
+						.build()
+		);
+		final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param";
+		final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1);
+		request.setParameter("RelayState", relayStateValue);
+		filter.doFilterInternal(request, response, filterChain);
+		assertThat(response.getHeader("Location"))
+				.contains("RelayState="+relayStateEncoded)
+				.contains("SigAlg=")
+				.contains("Signature=")
+				.startsWith(IDP_SSO_URL);
 	}
+
 }

+ 6 - 52
samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/OpenSamlActionTestingSupport.java → samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlActionTestingSupport.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,9 @@
  * limitations under the License.
  */
 
-package org.springframework.security.samples;
-
-import org.springframework.security.saml2.Saml2Exception;
+package org.springframework.security.saml2.provider.service.authentication;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import org.apache.commons.codec.binary.Base64;
 import org.apache.xml.security.algorithms.JCEMapper;
 import org.apache.xml.security.encryption.XMLCipherParameters;
 import org.joda.time.DateTime;
@@ -57,23 +54,16 @@ import org.opensaml.security.credential.CredentialSupport;
 import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters;
 import org.opensaml.xmlsec.encryption.support.EncryptionException;
 import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters;
+import org.springframework.security.saml2.Saml2Exception;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.cert.X509Certificate;
-import java.util.zip.Deflater;
-import java.util.zip.DeflaterOutputStream;
-import java.util.zip.Inflater;
-import java.util.zip.InflaterOutputStream;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.crypto.SecretKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.X509Certificate;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.asList;
-import static java.util.zip.Deflater.DEFLATED;
 import static org.opensaml.security.crypto.KeySupport.generateKey;
 
 /**
@@ -83,8 +73,6 @@ import static org.opensaml.security.crypto.KeySupport.generateKey;
  */
 public class OpenSamlActionTestingSupport {
 
-	static Base64 UNCHUNKED_ENCODER = new Base64(0, new byte[] { '\n' });
-
 	/** ID used for all generated {@link Response} objects. */
 	final static String REQUEST_ID = "request";
 
@@ -94,40 +82,6 @@ public class OpenSamlActionTestingSupport {
 	/** ID used for all generated {@link Assertion} objects. */
 	final static String ASSERTION_ID = "assertion";
 
-	static String encode(byte[] b) {
-		return UNCHUNKED_ENCODER.encodeToString(b);
-	}
-
-	static byte[] decode(String s) {
-		return UNCHUNKED_ENCODER.decode(s);
-	}
-
-	static byte[] deflate(String s) {
-		try {
-			ByteArrayOutputStream b = new ByteArrayOutputStream();
-			DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(DEFLATED, true));
-			deflater.write(s.getBytes(UTF_8));
-			deflater.finish();
-			return b.toByteArray();
-		}
-		catch (IOException e) {
-			throw new Saml2Exception("Unable to deflate string", e);
-		}
-	}
-
-	static String inflate(byte[] b) {
-		try {
-			ByteArrayOutputStream out = new ByteArrayOutputStream();
-			InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
-			iout.write(b);
-			iout.finish();
-			return new String(out.toByteArray(), UTF_8);
-		}
-		catch (IOException e) {
-			throw new Saml2Exception("Unable to inflate string", e);
-		}
-	}
-
 	static EncryptedAssertion encryptAssertion(Assertion assertion, X509Certificate certificate) {
 		Encrypter encrypter = getEncrypter(certificate);
 		try {

+ 19 - 14
samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java → samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.samples;
+
+package org.springframework.security.saml2.provider.service.authentication;
 
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.xml.BasicParserPool;
@@ -52,7 +53,6 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.http.MediaType;
-import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
 import org.springframework.test.context.junit4.SpringRunner;
 import org.springframework.test.util.AssertionErrors;
 import org.springframework.test.web.servlet.MockMvc;
@@ -63,6 +63,7 @@ import org.springframework.web.util.UriComponentsBuilder;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
+import javax.servlet.http.HttpSession;
 import java.io.ByteArrayInputStream;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
@@ -73,19 +74,18 @@ import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.UUID;
-import javax.servlet.http.HttpSession;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.matchesRegex;
 import static org.hamcrest.Matchers.startsWith;
-import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildConditions;
-import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildIssuer;
-import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildSubject;
-import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildSubjectConfirmation;
-import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildSubjectConfirmationData;
-import static org.springframework.security.samples.OpenSamlActionTestingSupport.encryptNameId;
-import static org.springframework.security.samples.OpenSamlActionTestingSupport.inflate;
+import static org.springframework.security.saml2.provider.service.authentication.OpenSamlActionTestingSupport.buildConditions;
+import static org.springframework.security.saml2.provider.service.authentication.OpenSamlActionTestingSupport.buildIssuer;
+import static org.springframework.security.saml2.provider.service.authentication.OpenSamlActionTestingSupport.buildSubject;
+import static org.springframework.security.saml2.provider.service.authentication.OpenSamlActionTestingSupport.buildSubjectConfirmation;
+import static org.springframework.security.saml2.provider.service.authentication.OpenSamlActionTestingSupport.buildSubjectConfirmationData;
+import static org.springframework.security.saml2.provider.service.authentication.OpenSamlActionTestingSupport.encryptNameId;
 import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
 import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
 import static org.springframework.security.web.WebAttributes.AUTHENTICATION_EXCEPTION;
@@ -133,10 +133,15 @@ public class Saml2LoginIntegrationTests {
 		mockMvc.perform(
 				get("http://localhost:8080/saml2/authenticate/simplesamlphp")
 						.param("RelayState", "relay state value with spaces")
+						.param("OtherParam", "OtherParamValue")
+						.param("OtherParam2", "OtherParamValue2")
 		)
 				.andExpect(status().is3xxRedirection())
 				.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest=")))
-				.andExpect(header().string("Location", containsString("RelayState=relay%20state%20value%20with%20spaces")));
+				.andExpect(header().string("Location", containsString("RelayState=relay%20state%20value%20with%20spaces")))
+				//check order of parameters
+				.andExpect(header().string("Location", matchesRegex(".*\\?SAMLRequest\\=.*\\&RelayState\\=.*\\&SigAlg\\=.*\\&Signature\\=.*")));
+
 	}
 
 	@Test
@@ -151,7 +156,7 @@ public class Saml2LoginIntegrationTests {
 		String request = parameters.getFirst("SAMLRequest");
 		AssertionErrors.assertNotNull("SAMLRequest parameter is missing", request);
 		request = URLDecoder.decode(request);
-		request = inflate(OpenSamlActionTestingSupport.decode(request));
+		request = Saml2Utils.samlInflate(Saml2Utils.samlDecode(request));
 		AuthnRequest authnRequest = (AuthnRequest) fromXml(request);
 		String destination = authnRequest.getDestination();
 		assertEquals(
@@ -298,7 +303,7 @@ public class Saml2LoginIntegrationTests {
 		String xml = toXml(response);
 		return mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp")
 				.contentType(MediaType.APPLICATION_FORM_URLENCODED)
-				.param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8))))
+				.param("SAMLResponse", Saml2Utils.samlEncode(xml.getBytes(UTF_8))))
 				.andExpect(status().is3xxRedirection())
 				.andExpect(redirectedUrl(redirectUrl));
 	}