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
This commit is contained in:
Filip Hanik 2020-02-11 12:18:01 -08:00
parent f9b783bcee
commit a3e09fadd7
21 changed files with 1297 additions and 248 deletions

View File

@ -0,0 +1,148 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
/**
* Data holder for {@code AuthNRequest} parameters to be sent using either the
* {@link Saml2MessageBinding#POST} or {@link Saml2MessageBinding#REDIRECT} binding.
* Data will be encoded and possibly deflated, but will not be escaped for transport,
* ie URL encoded, {@link org.springframework.web.util.UriUtils#encode(String, Charset)}
* or HTML encoded, {@link org.springframework.web.util.HtmlUtils#htmlEscape(String)}.
* https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
*
* @see Saml2AuthenticationRequestFactory#createPostAuthenticationRequest(Saml2AuthenticationRequestContext)
* @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext)
* @since 5.3
*/
abstract class AbstractSaml2AuthenticationRequest {
private final String samlRequest;
private final String relayState;
private final String authenticationRequestUri;
/**
* Mandatory constructor for the {@link AbstractSaml2AuthenticationRequest}
* @param samlRequest - the SAMLRequest XML data, SAML encoded, cannot be empty or null
* @param relayState - RelayState value that accompanies the request, may be null
* @param authenticationRequestUri - The authenticationRequestUri, a URL, where to send the XML message, cannot be empty or null
*/
AbstractSaml2AuthenticationRequest(
String samlRequest,
String relayState,
String authenticationRequestUri) {
Assert.hasText(samlRequest, "samlRequest cannot be null or empty");
Assert.hasText(authenticationRequestUri, "authenticationRequestUri cannot be null or empty");
this.authenticationRequestUri = authenticationRequestUri;
this.samlRequest = samlRequest;
this.relayState = relayState;
}
/**
* Returns the AuthNRequest XML value to be sent. This value is already encoded for transport.
* If {@link #getBinding()} is {@link Saml2MessageBinding#REDIRECT} the value is deflated and SAML encoded.
* If {@link #getBinding()} is {@link Saml2MessageBinding#POST} the value is SAML encoded.
* @return the SAMLRequest parameter value
*/
public String getSamlRequest() {
return this.samlRequest;
}
/**
* Returns the RelayState value, if present in the parameters
* @return the RelayState value, or null if not available
*/
public String getRelayState() {
return this.relayState;
}
/**
* Returns the URI endpoint that this AuthNRequest should be sent to.
* @return the URI endpoint for this message
*/
public String getAuthenticationRequestUri() {
return this.authenticationRequestUri;
}
/**
* Returns the binding this AuthNRequest will be sent and
* encoded with. If {@link Saml2MessageBinding#REDIRECT} is used, the DEFLATE encoding will be automatically applied.
* @return the binding this message will be sent with.
*/
public abstract Saml2MessageBinding getBinding();
/**
* A builder for {@link AbstractSaml2AuthenticationRequest} and its subclasses.
*/
static class Builder<T extends Builder<T>> {
String authenticationRequestUri;
String samlRequest;
String relayState;
protected Builder() {
}
/**
* Casting the return as the generic subtype, when returning itself
* @return this object
*/
@SuppressWarnings("unchecked")
protected final T _this() {
return (T) this;
}
/**
* Sets the {@code RelayState} parameter that will accompany this AuthNRequest
*
* @param relayState the relay state value, unencoded. if null or empty, the parameter will be removed from the
* map.
* @return this object
*/
public T relayState(String relayState) {
this.relayState = relayState;
return _this();
}
/**
* Sets the {@code SAMLRequest} parameter that will accompany this AuthNRequest
*
* @param samlRequest the SAMLRequest parameter.
* @return this object
*/
public T samlRequest(String samlRequest) {
this.samlRequest = samlRequest;
return _this();
}
/**
* Sets the {@code authenticationRequestUri}, a URL that will receive the AuthNRequest message
*
* @param authenticationRequestUri the relay state value, unencoded.
* @return this object
*/
public T authenticationRequestUri(String authenticationRequestUri) {
this.authenticationRequestUri = authenticationRequestUri;
return _this();
}
}
}

View File

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

View File

@ -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<String, String> signQueryParameters(
List<Saml2X509Credential> signingCredentials,
String samlRequest,
String relayState) {
Assert.notNull(samlRequest, "samlRequest cannot be null");
String algorithmUri = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
StringBuilder queryString = new StringBuilder();
queryString
.append("SAMLRequest")
.append("=")
.append(UriUtils.encode(samlRequest, StandardCharsets.ISO_8859_1))
.append("&");
if (hasText(relayState)) {
queryString
.append("RelayState")
.append("=")
.append(UriUtils.encode(relayState, StandardCharsets.ISO_8859_1))
.append("&");
}
queryString
.append("SigAlg")
.append("=")
.append(UriUtils.encode(algorithmUri, StandardCharsets.ISO_8859_1));
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(
getSigningCredential(signingCredentials, ""),
algorithmUri,
queryString.toString().getBytes(StandardCharsets.UTF_8)
);
String b64Signature = Saml2Utils.samlEncode(rawSignature);
Map<String, String> result = new LinkedHashMap<>();
result.put("SAMLRequest", samlRequest);
if (hasText(relayState)) {
result.put("RelayState", relayState);
}
result.put("SigAlg", algorithmUri);
result.put("Signature", b64Signature);
return result;
}
catch (SecurityException e) {
throw new Saml2Exception(e);
}
}
/*
* ==============================================================
* PRIVATE METHODS
* ==============================================================
*/
private XMLObject resolve(byte[] xml) {
XMLObject parsed = parse(xml);
if (parsed != null) {

View File

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

View File

@ -0,0 +1,172 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
/**
* Data holder for information required to create an {@code AuthNRequest}
* to be sent from the service provider to the identity provider
* <a href="https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf">
* Assertions and Protocols for SAML 2 (line 2031)</a>
*
* @see Saml2AuthenticationRequestFactory#createPostAuthenticationRequest(Saml2AuthenticationRequestContext)
* @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext)
* @since 5.3
*/
public final class Saml2AuthenticationRequestContext {
private final RelyingPartyRegistration relyingPartyRegistration;
private final String issuer;
private final String assertionConsumerServiceUrl;
private final String relayState;
private Saml2AuthenticationRequestContext(
RelyingPartyRegistration relyingPartyRegistration,
String issuer,
String assertionConsumerServiceUrl,
String relayState) {
Assert.hasText(issuer, "issuer cannot be null or empty");
Assert.notNull(relyingPartyRegistration, "relyingPartyRegistration cannot be null");
Assert.hasText(assertionConsumerServiceUrl, "spAssertionConsumerServiceUrl cannot be null or empty");
this.issuer = issuer;
this.relyingPartyRegistration = relyingPartyRegistration;
this.assertionConsumerServiceUrl = assertionConsumerServiceUrl;
this.relayState = relayState;
}
/**
* Returns the {@link RelyingPartyRegistration} configuration for which the AuthNRequest is intended for.
* @return the {@link RelyingPartyRegistration} configuration
*/
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.relyingPartyRegistration;
}
/**
* Returns the {@code Issuer} value to be used in the {@code AuthNRequest} object.
* This property should be used to populate the {@code AuthNRequest.Issuer} XML element.
* This value typically is a URI, but can be an arbitrary string.
* @return the Issuer value
*/
public String getIssuer() {
return this.issuer;
}
/**
* Returns the desired {@code AssertionConsumerServiceUrl} that this SP wishes to receive the
* assertion on. The IDP may or may not honor this request.
* This property populates the {@code AuthNRequest.AssertionConsumerServiceURL} XML attribute.
* @return the AssertionConsumerServiceURL value
*/
public String getAssertionConsumerServiceUrl() {
return assertionConsumerServiceUrl;
}
/**
* Returns the RelayState value, if present in the parameters
* @return the RelayState value, or null if not available
*/
public String getRelayState() {
return this.relayState;
}
/**
* Returns the {@code Destination}, the WEB Single Sign On URI, for this authentication request.
* This property can also populate the {@code AuthNRequest.Destination} XML attribute.
* @return the Destination value
*/
public String getDestination() {
return this.getRelyingPartyRegistration().getIdpWebSsoUrl();
}
/**
* A builder for {@link Saml2AuthenticationRequestContext}.
* @return a builder object
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder for {@link Saml2AuthenticationRequestContext}.
*/
public static class Builder {
private String issuer;
private String assertionConsumerServiceUrl;
private String relayState;
private RelyingPartyRegistration relyingPartyRegistration;
private Builder() {
}
/**
* Sets the issuer for the authentication request.
* @param issuer - a required value
* @return this {@code Builder}
*/
public Builder issuer(String issuer) {
this.issuer = issuer;
return this;
}
/**
* Sets the {@link RelyingPartyRegistration} used to build the authentication request.
* @param relyingPartyRegistration - a required value
* @return this {@code Builder}
*/
public Builder relyingPartyRegistration(RelyingPartyRegistration relyingPartyRegistration) {
this.relyingPartyRegistration = relyingPartyRegistration;
return this;
}
/**
* Sets the {@code assertionConsumerServiceURL} for the authentication request.
* Typically the {@code Service Provider EntityID}
* @param assertionConsumerServiceUrl - a required value
* @return this {@code Builder}
*/
public Builder assertionConsumerServiceUrl(String assertionConsumerServiceUrl) {
this.assertionConsumerServiceUrl = assertionConsumerServiceUrl;
return this;
}
/**
* Sets the {@code RelayState} parameter that will accompany this AuthNRequest
* @param relayState the relay state value, unencoded. if null or empty, the parameter will be removed from the map.
* @return this object
*/
public Builder relayState(String relayState) {
this.relayState = relayState;
return this;
}
/**
* Creates a {@link Saml2AuthenticationRequestContext} object.
* @return the Saml2AuthenticationRequest object
* @throws {@link IllegalArgumentException} if a required property is not set
*/
public Saml2AuthenticationRequestContext build() {
return new Saml2AuthenticationRequestContext(
this.relyingPartyRegistration,
this.issuer,
this.assertionConsumerServiceUrl,
this.relayState
);
}
}
}

View File

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

View File

@ -0,0 +1,85 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.POST;
/**
* Data holder for information required to send an {@code AuthNRequest} over a POST binding
* from the service provider to the identity provider
* https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
*
* @see Saml2AuthenticationRequestFactory
* @since 5.3
*/
public class Saml2PostAuthenticationRequest extends AbstractSaml2AuthenticationRequest {
private Saml2PostAuthenticationRequest(
String samlRequest,
String relayState,
String authenticationRequestUri) {
super(samlRequest, relayState, authenticationRequestUri);
}
/**
* @return {@link Saml2MessageBinding#POST}
*/
@Override
public Saml2MessageBinding getBinding() {
return POST;
}
/**
* Constructs a {@link Builder} from a {@link Saml2AuthenticationRequestContext} object.
* By default the {@link Saml2PostAuthenticationRequest#getAuthenticationRequestUri()} will be set to the
* {@link Saml2AuthenticationRequestContext#getDestination()} value.
* @param context input providing {@code Destination}, {@code RelayState}, and {@code Issuer} objects.
* @return a modifiable builder object
*/
public static Builder withAuthenticationRequestContext(Saml2AuthenticationRequestContext context) {
return new Builder()
.authenticationRequestUri(context.getDestination())
.relayState(context.getRelayState())
;
}
/**
* Builder class for a {@link Saml2PostAuthenticationRequest} object.
*/
public static class Builder extends AbstractSaml2AuthenticationRequest.Builder<Builder> {
private Builder() {
super();
}
/**
* Constructs an immutable {@link Saml2PostAuthenticationRequest} object.
* @return an immutable {@link Saml2PostAuthenticationRequest} object.
*/
public Saml2PostAuthenticationRequest build() {
return new Saml2PostAuthenticationRequest(
this.samlRequest,
this.relayState,
this.authenticationRequestUri
);
}
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.REDIRECT;
/**
* Data holder for information required to send an {@code AuthNRequest} over a REDIRECT binding
* from the service provider to the identity provider
* https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
*
* @see Saml2AuthenticationRequestFactory
* @since 5.3
*/
public class Saml2RedirectAuthenticationRequest extends AbstractSaml2AuthenticationRequest {
private final String sigAlg;
private final String signature;
private Saml2RedirectAuthenticationRequest(
String samlRequest,
String sigAlg,
String signature,
String relayState,
String authenticationRequestUri) {
super(samlRequest, relayState, authenticationRequestUri);
this.sigAlg = sigAlg;
this.signature = signature;
}
/**
* Returns the SigAlg value for {@link Saml2MessageBinding#REDIRECT} requests
* @return the SigAlg value
*/
public String getSigAlg() {
return this.sigAlg;
}
/**
* Returns the Signature value for {@link Saml2MessageBinding#REDIRECT} requests
* @return the Signature value
*/
public String getSignature() {
return this.signature;
}
/**
* @return {@link Saml2MessageBinding#REDIRECT}
*/
@Override
public Saml2MessageBinding getBinding() {
return REDIRECT;
}
/**
* Constructs a {@link Saml2RedirectAuthenticationRequest.Builder} from a {@link Saml2AuthenticationRequestContext} object.
* By default the {@link Saml2RedirectAuthenticationRequest#getAuthenticationRequestUri()} will be set to the
* {@link Saml2AuthenticationRequestContext#getDestination()} value.
* @param context input providing {@code Destination}, {@code RelayState}, and {@code Issuer} objects.
* @return a modifiable builder object
*/
public static Builder withAuthenticationRequestContext(Saml2AuthenticationRequestContext context) {
return new Builder()
.authenticationRequestUri(context.getDestination())
.relayState(context.getRelayState())
;
}
/**
* Builder class for a {@link Saml2RedirectAuthenticationRequest} object.
*/
public static class Builder extends AbstractSaml2AuthenticationRequest.Builder<Builder> {
private String sigAlg;
private String signature;
private Builder() {
super();
}
/**
* Sets the {@code SigAlg} parameter that will accompany this AuthNRequest
* @param sigAlg the SigAlg parameter value.
* @return this object
*/
public Builder sigAlg(String sigAlg) {
this.sigAlg = sigAlg;
return _this();
}
/**
* Sets the {@code Signature} parameter that will accompany this AuthNRequest
* @param signature the Signature parameter value.
* @return this object
*/
public Builder signature(String signature) {
this.signature = signature;
return _this();
}
/**
* Constructs an immutable {@link Saml2RedirectAuthenticationRequest} object.
* @return an immutable {@link Saml2RedirectAuthenticationRequest} object.
*/
public Saml2RedirectAuthenticationRequest build() {
return new Saml2RedirectAuthenticationRequest(
this.samlRequest,
this.sigAlg,
this.signature,
this.relayState,
this.authenticationRequestUri
);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.servlet.filter;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.security.web.util.UrlUtils.buildFullRequestUrl;
import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl;
/**
* @since 5.3
*/
final class Saml2ServletUtils {
private static final char PATH_DELIMITER = '/';
static String getServiceProviderEntityId(RelyingPartyRegistration rp, HttpServletRequest request) {
return resolveUrlTemplate(
rp.getLocalEntityIdTemplate(),
getApplicationUri(request),
rp.getRemoteIdpEntityId(),
rp.getRegistrationId()
);
}
static String resolveUrlTemplate(String template, String baseUrl, String entityId, String registrationId) {
if (!StringUtils.hasText(template)) {
return baseUrl;
}
Map<String, String> uriVariables = new HashMap<>();
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl)
.replaceQuery(null)
.fragment(null)
.build();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", scheme == null ? "" : scheme);
String host = uriComponents.getHost();
uriVariables.put("baseHost", host == null ? "" : host);
// following logic is based on HierarchicalUriComponents#toUriString()
int port = uriComponents.getPort();
uriVariables.put("basePort", port == -1 ? "" : ":" + port);
String path = uriComponents.getPath();
if (StringUtils.hasLength(path)) {
if (path.charAt(0) != PATH_DELIMITER) {
path = PATH_DELIMITER + path;
}
}
uriVariables.put("basePath", path == null ? "" : path);
uriVariables.put("baseUrl", uriComponents.toUriString());
uriVariables.put("entityId", StringUtils.hasText(entityId) ? entityId : "");
uriVariables.put("registrationId", StringUtils.hasText(registrationId) ? registrationId : "");
return UriComponentsBuilder.fromUriString(template)
.buildAndExpand(uriVariables)
.toUriString();
}
static String getApplicationUri(HttpServletRequest request) {
UriComponents uriComponents = fromHttpUrl(buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.build();
return uriComponents.toUriString();
}
}

View File

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

View File

@ -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);

View File

@ -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()
;
}
}

View File

@ -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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<saml2p:AuthnRequest");
}
@Test
public void createRedirectAuthenticationRequestWhenUsingContextThenAllValuesAreSet() {
context = contextBuilder
.relayState("Relay State Value")
.build();
Saml2RedirectAuthenticationRequest result = factory.createRedirectAuthenticationRequest(context);
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isEqualTo("Relay State Value");
assertThat(result.getSigAlg()).isNotEmpty();
assertThat(result.getSignature()).isNotEmpty();
assertThat(result.getBinding()).isEqualTo(REDIRECT);
}
@Test
public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() {
AuthnRequest authn = getAuthNRequest();
AuthnRequest authn = getAuthNRequest(POST);
Assert.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding());
}
@Test
public void createAuthenticationRequestWhenSetUriThenReturnsCorrectBinding() {
factory.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
AuthnRequest authn = getAuthNRequest();
AuthnRequest authn = getAuthNRequest(POST);
Assert.assertEquals(SAMLConstants.SAML2_REDIRECT_BINDING_URI, authn.getProtocolBinding());
}
@ -66,8 +104,18 @@ public class OpenSamlAuthenticationRequestFactoryTests {
factory.setProtocolBinding("my-invalid-binding");
}
private AuthnRequest getAuthNRequest() {
String xml = factory.createAuthenticationRequest(request);
return (AuthnRequest) OpenSamlImplementation.getInstance().resolve(xml);
private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) {
AbstractSaml2AuthenticationRequest result = (binding == REDIRECT) ?
factory.createRedirectAuthenticationRequest(context) :
factory.createPostAuthenticationRequest(context);
String samlRequest = result.getSamlRequest();
assertThat(samlRequest).isNotEmpty();
if (result.getBinding() == REDIRECT) {
samlRequest = Saml2Utils.samlInflate(samlDecode(samlRequest));
}
else {
samlRequest = new String(samlDecode(samlRequest), StandardCharsets.UTF_8);
}
return (AuthnRequest) OpenSamlImplementation.getInstance().resolve(samlRequest);
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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));
}