From a3e09fadd77cb6bbacbd626c48deb2a4473e1e54 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 11 Feb 2020 12:18:01 -0800 Subject: [PATCH] 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 --- .../AbstractSaml2AuthenticationRequest.java | 148 +++++++++++++++ .../OpenSamlAuthenticationRequestFactory.java | 74 ++++++-- .../OpenSamlImplementation.java | 82 ++++++++- .../Saml2AuthenticationRequest.java | 20 +- .../Saml2AuthenticationRequestContext.java | 172 ++++++++++++++++++ .../Saml2AuthenticationRequestFactory.java | 95 +++++++++- .../Saml2PostAuthenticationRequest.java | 85 +++++++++ .../Saml2RedirectAuthenticationRequest.java | 133 ++++++++++++++ .../service/authentication/Saml2Utils.java | 73 ++++++++ .../registration/Saml2MessageBinding.java | 45 +++++ .../servlet/filter/Saml2ServletUtils.java | 88 +++++++++ .../service/servlet/filter/Saml2Utils.java | 76 +------- .../Saml2WebSsoAuthenticationFilter.java | 10 +- ...aml2WebSsoAuthenticationRequestFilter.java | 65 ++++--- ...SamlAuthenticationRequestFactoryTests.java | 70 +++++-- .../OpenSamlImplementationTests.java | 55 ++++++ ...aml2AuthenticationRequestFactoryTests.java | 72 ++++++++ .../servlet/filter/Saml2UtilsTests.java | 4 +- ...ebSsoAuthenticationRequestFilterTests.java | 87 +++++---- .../OpenSamlActionTestingSupport.java | 58 +----- .../Saml2LoginIntegrationTests.java | 33 ++-- 21 files changed, 1297 insertions(+), 248 deletions(-) create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java rename samples/boot/saml2login/src/integration-test/java/org/springframework/security/{samples => saml2/provider/service/authentication}/OpenSamlActionTestingSupport.java (92%) rename samples/boot/saml2login/src/integration-test/java/org/springframework/security/{samples => saml2/provider/service/authentication}/Saml2LoginIntegrationTests.java (94%) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java new file mode 100644 index 0000000000..99ef2cdc22 --- /dev/null +++ b/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> { + 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(); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java index 035a2dec4c..67983a12f9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java +++ b/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 signingCredentials = context.getRelyingPartyRegistration().getSigningCredentials(); + Builder result = Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context); + + String deflatedAndEncoded = samlEncode(samlDeflate(xml)); + Map 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 credentials) { + return createAuthenticationRequest(Saml2AuthenticationRequest.withAuthenticationRequestContext(request).build(), credentials); + } + + private String createAuthenticationRequest(Saml2AuthenticationRequest context, List 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) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java index 67d2fb2cc4..a049fc7d9c 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java +++ b/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 java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.List; -import java.util.Map; 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 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 signQueryParameters( + List 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 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) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java index 89adbedac7..1fcc1a80a2 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java +++ b/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 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}. */ diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java new file mode 100644 index 0000000000..e769b4354d --- /dev/null +++ b/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 + * + * Assertions and Protocols for SAML 2 (line 2031) + * + * @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 + ); + } + } +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java index 35edb477b1..9ae7c284e4 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java +++ b/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, samlp:AuthnRequestType 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, samlp:AuthnRequestType 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}. + * 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. + * @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}. + * 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. + * @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(); + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java new file mode 100644 index 0000000000..d621a7c704 --- /dev/null +++ b/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 { + + 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 + ); + } + } + + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java new file mode 100644 index 0000000000..fdfc8372aa --- /dev/null +++ b/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 { + 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 + ); + } + + } + + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java new file mode 100644 index 0000000000..ae271df111 --- /dev/null +++ b/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); + } + } +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java new file mode 100644 index 0000000000..154dcc88f4 --- /dev/null +++ b/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; + } +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java new file mode 100644 index 0000000000..e78017184e --- /dev/null +++ b/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 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(); + } +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2Utils.java index 994db67849..f472a2376b 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2Utils.java +++ b/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 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(); - } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java index 5c1ba70c53..666013f1cc 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java +++ b/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); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java index b27927647f..881afda820 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java +++ b/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() + ; } - } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java index b7823e07c5..1142714203 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java +++ b/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).startsWith("\n signCredentials = relyingPartyCredentials(); + List verifyCredentials = assertingPartyCredentials(); + String samlRequest = "saml-request-example"; + String encoded = Saml2Utils.samlEncode(samlRequest.getBytes(UTF_8)); + String relayState = "test relay state"; + Map 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; + } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java new file mode 100644 index 0000000000..6c79ba187d --- /dev/null +++ b/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); + } +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2UtilsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2UtilsTests.java index 972937715c..be3405f924 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2UtilsTests.java +++ b/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); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java index c17e9c820c..3e2af6e65d 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java +++ b/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(); + + rpBuilder = RelyingPartyRegistration + .withRegistrationId("registration-id") + .remoteIdpEntityId("idp-entity-id") + .idpWebSsoUrl(IDP_SSO_URL) + .assertionConsumerServiceUrlTemplate("template") + .credentials(c -> c.add(signingCredential())); } @Test - public void createSamlRequestRedirectUrlAndReturnUrlWithoutRelayState() 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 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); - - Assert.assertTrue(response.getHeader("Location").contains("RelayState=my-relay-state")); + 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); + } + + @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); + } + } diff --git a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/OpenSamlActionTestingSupport.java b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlActionTestingSupport.java similarity index 92% rename from samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/OpenSamlActionTestingSupport.java rename to samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlActionTestingSupport.java index b56eeb1a2d..09806bef97 100644 --- a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/OpenSamlActionTestingSupport.java +++ b/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 { diff --git a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java similarity index 94% rename from samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java rename to samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java index 1f40142987..1cb224713e 100644 --- a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/samples/Saml2LoginIntegrationTests.java +++ b/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)); }