1
0
mirror of synced 2026-05-22 21:33:16 +00:00

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