Add OpenSamlInitializationService

Closes gh-8772
This commit is contained in:
Josh Cummings 2020-07-23 14:45:31 -06:00
parent 43f2904059
commit 2276fcf34a
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
7 changed files with 229 additions and 89 deletions

View File

@ -0,0 +1,146 @@
/*
* 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.core;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import javax.xml.XMLConstants;
import net.shibboleth.utilities.java.support.xml.BasicParserPool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.config.InitializationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.springframework.security.saml2.Saml2Exception;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.setParserPool;
/**
* An initialization service for initializing OpenSAML. Each Spring Security OpenSAML-based component invokes
* the {@link #initialize()} method at static initialization time.
*
* {@link #initialize()} is idempotent and may be safely called in custom classes that need OpenSAML to be
* initialized in order to function correctly. It's recommended that you call this {@link #initialize()} method
* when using Spring Security and OpenSAML instead of OpenSAML's {@link InitializationService#initialize()}.
*
* The primary purpose of {@link #initialize()} is to prepare OpenSAML's {@link XMLObjectProviderRegistry}
* with some reasonable defaults. Any changes that Spring Security makes to the registry happen in this method.
*
* To override those defaults, call {@link #requireInitialize(Consumer)} and change the registry:
*
* <pre>
* static {
* OpenSamlInitializationService.requireInitialize(registry -> {
* registry.setParserPool(...);
* registry.getBuilderFactory().registerBuilder(...);
* });
* }
* </pre>
*
* {@link #requireInitialize(Consumer)} may only be called once per application.
*
* If the application already initialized OpenSAML before {@link #requireInitialize(Consumer)} was called,
* then the configuration changes will not be applied and an exception will be thrown. The reason for this is to
* alert you to the fact that there are likely some initialization ordering problems in your application that
* would otherwise lead to an unpredictable state.
*
* If you must change the registry's configuration in multiple places in your application, you are expected
* to handle the initialization ordering issues yourself instead of trying to call {@link #requireInitialize(Consumer)}
* multiple times.
*
* @author Josh Cummings
* @since 5.4
*/
public class OpenSamlInitializationService {
private static final Log log = LogFactory.getLog(OpenSamlInitializationService.class);
private static final AtomicBoolean initialized = new AtomicBoolean(false);
/**
* Ready OpenSAML for use and configure it with reasonable defaults.
*
* Initialization is guaranteed to happen only once per application. This method will passively return
* {@code false} if initialization already took place earlier in the application.
*
* @return whether or not initialization was performed. The first thread to initialize OpenSAML will
* return {@code true} while the rest will return {@code false}.
* @throws Saml2Exception if OpenSAML failed to initialize
*/
public static boolean initialize() {
return initialize(registry -> {});
}
/**
* Ready OpenSAML for use, configure it with reasonable defaults, and modify the {@link XMLObjectProviderRegistry}
* using the provided {@link Consumer}.
*
* Initialization is guaranteed to happen only once per application. This method will throw an exception
* if initialization already took place earlier in the application.
*
* @param registryConsumer the {@link Consumer} to further configure the {@link XMLObjectProviderRegistry}
* @throws Saml2Exception if initialization already happened previously or if OpenSAML failed to initialize
*/
public static void requireInitialize(Consumer<XMLObjectProviderRegistry> registryConsumer) {
if (!initialize(registryConsumer)) {
throw new Saml2Exception("OpenSAML was already initialized previously");
}
}
private static boolean initialize(Consumer<XMLObjectProviderRegistry> registryConsumer) {
if (initialized.compareAndSet(false, true)) {
log.trace("Initializing OpenSAML");
try {
InitializationService.initialize();
} catch (Exception e) {
throw new Saml2Exception(e);
}
BasicParserPool parserPool = new BasicParserPool();
parserPool.setMaxPoolSize(50);
Map<String, Boolean> parserBuilderFeatures = new HashMap<>();
parserBuilderFeatures.put("http://apache.org/xml/features/disallow-doctype-decl", TRUE);
parserBuilderFeatures.put(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE);
parserBuilderFeatures.put("http://xml.org/sax/features/external-general-entities", FALSE);
parserBuilderFeatures.put("http://apache.org/xml/features/validation/schema/normalized-value", FALSE);
parserBuilderFeatures.put("http://xml.org/sax/features/external-parameter-entities", FALSE);
parserBuilderFeatures.put("http://apache.org/xml/features/dom/defer-node-expansion", FALSE);
parserPool.setBuilderFeatures(parserBuilderFeatures);
try {
parserPool.initialize();
} catch (Exception e) {
throw new Saml2Exception(e);
}
setParserPool(parserPool);
registryConsumer.accept(ConfigurationService.get(XMLObjectProviderRegistry.class));
log.debug("Initialized OpenSAML");
return true;
} else {
log.debug("Refused to re-initialize OpenSAML");
return false;
}
}
}

View File

@ -98,6 +98,7 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
@ -160,6 +161,10 @@ import static org.springframework.util.Assert.notNull;
*/
public final class OpenSamlAuthenticationProvider implements AuthenticationProvider {
static {
OpenSamlInitializationService.initialize();
}
private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class);
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
@ -270,7 +275,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
} catch (Saml2Exception x) {
throw authException(MALFORMED_RESPONSE_DATA, x.getMessage(), x);
}
}
private void process(Saml2AuthenticationToken token, Response response) {

View File

@ -17,6 +17,7 @@
package org.springframework.security.saml2.provider.service.authentication;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Instant;
import java.util.Collection;
@ -24,7 +25,6 @@ import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.security.cert.X509Certificate;
import org.joda.time.DateTime;
import org.opensaml.core.xml.io.MarshallingException;
@ -43,6 +43,7 @@ import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest.Builder;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
@ -56,6 +57,10 @@ import static org.springframework.security.saml2.provider.service.authentication
* @since 5.2
*/
public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
static {
OpenSamlInitializationService.initialize();
}
private Clock clock = Clock.systemUTC();
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();

View File

@ -20,22 +20,14 @@ import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.xml.BasicParserPool;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import net.shibboleth.utilities.java.support.xml.XMLParserException;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.config.InitializationException;
import org.opensaml.core.config.InitializationService;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.MarshallerFactory;
import org.opensaml.core.xml.io.MarshallingException;
@ -62,24 +54,27 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.web.util.UriUtils;
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.getParserPool;
import static org.springframework.util.StringUtils.hasText;
/**
* @since 5.2
*/
final class OpenSamlImplementation {
static {
OpenSamlInitializationService.initialize();
}
private static OpenSamlImplementation instance = new OpenSamlImplementation();
private static XMLObjectBuilderFactory xmlObjectBuilderFactory =
XMLObjectProviderRegistrySupport.getBuilderFactory();
private final BasicParserPool parserPool = new BasicParserPool();
private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
asList(
new InlineEncryptedKeyResolver(),
@ -88,74 +83,6 @@ final class OpenSamlImplementation {
)
);
private OpenSamlImplementation() {
bootstrap();
}
/*
* ==============================================================
* PRIVATE METHODS
* ==============================================================
*/
private void bootstrap() {
// configure default values
// maxPoolSize = 5;
this.parserPool.setMaxPoolSize(50);
// coalescing = true;
this.parserPool.setCoalescing(true);
// expandEntityReferences = false;
this.parserPool.setExpandEntityReferences(false);
// ignoreComments = true;
this.parserPool.setIgnoreComments(true);
// ignoreElementContentWhitespace = true;
this.parserPool.setIgnoreElementContentWhitespace(true);
// namespaceAware = true;
this.parserPool.setNamespaceAware(true);
// schema = null;
this.parserPool.setSchema(null);
// dtdValidating = false;
this.parserPool.setDTDValidating(false);
// xincludeAware = false;
this.parserPool.setXincludeAware(false);
Map<String, Object> builderAttributes = new HashMap<>();
this.parserPool.setBuilderAttributes(builderAttributes);
Map<String, Boolean> parserBuilderFeatures = new HashMap<>();
parserBuilderFeatures.put("http://apache.org/xml/features/disallow-doctype-decl", TRUE);
parserBuilderFeatures.put(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE);
parserBuilderFeatures.put("http://xml.org/sax/features/external-general-entities", FALSE);
parserBuilderFeatures.put("http://apache.org/xml/features/validation/schema/normalized-value", FALSE);
parserBuilderFeatures.put("http://xml.org/sax/features/external-parameter-entities", FALSE);
parserBuilderFeatures.put("http://apache.org/xml/features/dom/defer-node-expansion", FALSE);
this.parserPool.setBuilderFeatures(parserBuilderFeatures);
try {
this.parserPool.initialize();
}
catch (ComponentInitializationException x) {
throw new Saml2Exception("Unable to initialize OpenSaml v3 ParserPool", x);
}
try {
InitializationService.initialize();
}
catch (InitializationException e) {
throw new Saml2Exception("Unable to initialize OpenSaml v3", e);
}
XMLObjectProviderRegistry registry;
synchronized (ConfigurationService.class) {
registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
if (registry == null) {
registry = new XMLObjectProviderRegistry();
ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
}
}
registry.setParserPool(this.parserPool);
}
/*
* ==============================================================
* PUBLIC METHODS
@ -259,7 +186,7 @@ final class OpenSamlImplementation {
private XMLObject parse(byte[] xml) {
try {
Document document = this.parserPool.parse(new ByteArrayInputStream(xml));
Document document = getParserPool().parse(new ByteArrayInputStream(xml));
Element element = document.getDocumentElement();
return getUnmarshallerFactory().getUnmarshaller(element).unmarshall(element);
}

View File

@ -0,0 +1,47 @@
/*
* 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.core;
import org.junit.Test;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.springframework.security.saml2.Saml2Exception;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
/**
* Tests for {@link OpenSamlInitializationService}
*
* @author Josh Cummings
*/
public class OpenSamlInitializationServiceTests {
@Test
public void initializeWhenInvokedMultipleTimesThenInitializesOnce() {
OpenSamlInitializationService.initialize();
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
assertThat(registry.getParserPool()).isNotNull();
registry.setParserPool(null);
OpenSamlInitializationService.initialize();
assertThat(registry.getParserPool()).isNull();
assertThatCode(() -> OpenSamlInitializationService.requireInitialize(r -> {}))
.isInstanceOf(Saml2Exception.class)
.hasMessageContaining("OpenSAML was already initialized previously");
}
}

View File

@ -74,9 +74,14 @@ 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.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2X509Credential;
final class TestOpenSamlObjects {
static {
OpenSamlInitializationService.initialize();
}
private static OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
private static String USERNAME = "test@saml.user";

View File

@ -16,6 +16,13 @@
package org.springframework.security.saml2.provider.service.authentication;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.X509Certificate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.SecretKey;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.encryption.XMLCipherParameters;
@ -54,14 +61,9 @@ 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 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 org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import static java.util.Arrays.asList;
import static org.opensaml.security.crypto.KeySupport.generateKey;
@ -73,6 +75,10 @@ import static org.opensaml.security.crypto.KeySupport.generateKey;
*/
public class OpenSamlActionTestingSupport {
static {
OpenSamlInitializationService.initialize();
}
/** ID used for all generated {@link Response} objects. */
final static String REQUEST_ID = "request";