Skip to content

Commit 2276fcf

Browse files
committed
Add OpenSamlInitializationService
Closes gh-8772
1 parent 43f2904 commit 2276fcf

File tree

7 files changed

+229
-89
lines changed

7 files changed

+229
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.core;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.concurrent.atomic.AtomicBoolean;
22+
import java.util.function.Consumer;
23+
import javax.xml.XMLConstants;
24+
25+
import net.shibboleth.utilities.java.support.xml.BasicParserPool;
26+
import org.apache.commons.logging.Log;
27+
import org.apache.commons.logging.LogFactory;
28+
import org.opensaml.core.config.ConfigurationService;
29+
import org.opensaml.core.config.InitializationService;
30+
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
31+
32+
import org.springframework.security.saml2.Saml2Exception;
33+
34+
import static java.lang.Boolean.FALSE;
35+
import static java.lang.Boolean.TRUE;
36+
import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.setParserPool;
37+
38+
/**
39+
* An initialization service for initializing OpenSAML. Each Spring Security OpenSAML-based component invokes
40+
* the {@link #initialize()} method at static initialization time.
41+
*
42+
* {@link #initialize()} is idempotent and may be safely called in custom classes that need OpenSAML to be
43+
* initialized in order to function correctly. It's recommended that you call this {@link #initialize()} method
44+
* when using Spring Security and OpenSAML instead of OpenSAML's {@link InitializationService#initialize()}.
45+
*
46+
* The primary purpose of {@link #initialize()} is to prepare OpenSAML's {@link XMLObjectProviderRegistry}
47+
* with some reasonable defaults. Any changes that Spring Security makes to the registry happen in this method.
48+
*
49+
* To override those defaults, call {@link #requireInitialize(Consumer)} and change the registry:
50+
*
51+
* <pre>
52+
* static {
53+
* OpenSamlInitializationService.requireInitialize(registry -> {
54+
* registry.setParserPool(...);
55+
* registry.getBuilderFactory().registerBuilder(...);
56+
* });
57+
* }
58+
* </pre>
59+
*
60+
* {@link #requireInitialize(Consumer)} may only be called once per application.
61+
*
62+
* If the application already initialized OpenSAML before {@link #requireInitialize(Consumer)} was called,
63+
* then the configuration changes will not be applied and an exception will be thrown. The reason for this is to
64+
* alert you to the fact that there are likely some initialization ordering problems in your application that
65+
* would otherwise lead to an unpredictable state.
66+
*
67+
* If you must change the registry's configuration in multiple places in your application, you are expected
68+
* to handle the initialization ordering issues yourself instead of trying to call {@link #requireInitialize(Consumer)}
69+
* multiple times.
70+
*
71+
* @author Josh Cummings
72+
* @since 5.4
73+
*/
74+
public class OpenSamlInitializationService {
75+
private static final Log log = LogFactory.getLog(OpenSamlInitializationService.class);
76+
private static final AtomicBoolean initialized = new AtomicBoolean(false);
77+
78+
/**
79+
* Ready OpenSAML for use and configure it with reasonable defaults.
80+
*
81+
* Initialization is guaranteed to happen only once per application. This method will passively return
82+
* {@code false} if initialization already took place earlier in the application.
83+
*
84+
* @return whether or not initialization was performed. The first thread to initialize OpenSAML will
85+
* return {@code true} while the rest will return {@code false}.
86+
* @throws Saml2Exception if OpenSAML failed to initialize
87+
*/
88+
public static boolean initialize() {
89+
return initialize(registry -> {});
90+
}
91+
92+
/**
93+
* Ready OpenSAML for use, configure it with reasonable defaults, and modify the {@link XMLObjectProviderRegistry}
94+
* using the provided {@link Consumer}.
95+
*
96+
* Initialization is guaranteed to happen only once per application. This method will throw an exception
97+
* if initialization already took place earlier in the application.
98+
*
99+
* @param registryConsumer the {@link Consumer} to further configure the {@link XMLObjectProviderRegistry}
100+
* @throws Saml2Exception if initialization already happened previously or if OpenSAML failed to initialize
101+
*/
102+
public static void requireInitialize(Consumer<XMLObjectProviderRegistry> registryConsumer) {
103+
if (!initialize(registryConsumer)) {
104+
throw new Saml2Exception("OpenSAML was already initialized previously");
105+
}
106+
}
107+
108+
private static boolean initialize(Consumer<XMLObjectProviderRegistry> registryConsumer) {
109+
if (initialized.compareAndSet(false, true)) {
110+
log.trace("Initializing OpenSAML");
111+
112+
try {
113+
InitializationService.initialize();
114+
} catch (Exception e) {
115+
throw new Saml2Exception(e);
116+
}
117+
118+
BasicParserPool parserPool = new BasicParserPool();
119+
parserPool.setMaxPoolSize(50);
120+
121+
Map<String, Boolean> parserBuilderFeatures = new HashMap<>();
122+
parserBuilderFeatures.put("http://apache.org/xml/features/disallow-doctype-decl", TRUE);
123+
parserBuilderFeatures.put(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE);
124+
parserBuilderFeatures.put("http://xml.org/sax/features/external-general-entities", FALSE);
125+
parserBuilderFeatures.put("http://apache.org/xml/features/validation/schema/normalized-value", FALSE);
126+
parserBuilderFeatures.put("http://xml.org/sax/features/external-parameter-entities", FALSE);
127+
parserBuilderFeatures.put("http://apache.org/xml/features/dom/defer-node-expansion", FALSE);
128+
parserPool.setBuilderFeatures(parserBuilderFeatures);
129+
130+
try {
131+
parserPool.initialize();
132+
} catch (Exception e) {
133+
throw new Saml2Exception(e);
134+
}
135+
setParserPool(parserPool);
136+
137+
registryConsumer.accept(ConfigurationService.get(XMLObjectProviderRegistry.class));
138+
139+
log.debug("Initialized OpenSAML");
140+
return true;
141+
} else {
142+
log.debug("Refused to re-initialize OpenSAML");
143+
return false;
144+
}
145+
}
146+
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
import org.springframework.security.core.authority.SimpleGrantedAuthority;
9999
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
100100
import org.springframework.security.saml2.Saml2Exception;
101+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
101102
import org.springframework.security.saml2.core.Saml2Error;
102103
import org.springframework.security.saml2.core.Saml2X509Credential;
103104
import org.springframework.util.Assert;
@@ -160,6 +161,10 @@
160161
*/
161162
public final class OpenSamlAuthenticationProvider implements AuthenticationProvider {
162163

164+
static {
165+
OpenSamlInitializationService.initialize();
166+
}
167+
163168
private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class);
164169

165170
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
@@ -270,7 +275,6 @@ private Response parse(String response) throws Saml2Exception, Saml2Authenticati
270275
} catch (Saml2Exception x) {
271276
throw authException(MALFORMED_RESPONSE_DATA, x.getMessage(), x);
272277
}
273-
274278
}
275279

276280
private void process(Saml2AuthenticationToken token, Response response) {

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
package org.springframework.security.saml2.provider.service.authentication;
1818

1919
import java.security.PrivateKey;
20+
import java.security.cert.X509Certificate;
2021
import java.time.Clock;
2122
import java.time.Instant;
2223
import java.util.Collection;
2324
import java.util.Map;
2425
import java.util.UUID;
2526
import java.util.function.Consumer;
2627
import java.util.function.Function;
27-
import java.security.cert.X509Certificate;
2828

2929
import org.joda.time.DateTime;
3030
import org.opensaml.core.xml.io.MarshallingException;
@@ -43,6 +43,7 @@
4343

4444
import org.springframework.core.convert.converter.Converter;
4545
import org.springframework.security.saml2.Saml2Exception;
46+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
4647
import org.springframework.security.saml2.core.Saml2X509Credential;
4748
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest.Builder;
4849
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
@@ -56,6 +57,10 @@
5657
* @since 5.2
5758
*/
5859
public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
60+
static {
61+
OpenSamlInitializationService.initialize();
62+
}
63+
5964
private Clock clock = Clock.systemUTC();
6065
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
6166

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

+7-80
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,14 @@
2020
import java.nio.charset.Charset;
2121
import java.nio.charset.StandardCharsets;
2222
import java.util.Collection;
23-
import java.util.HashMap;
2423
import java.util.LinkedHashMap;
2524
import java.util.Map;
26-
import javax.xml.XMLConstants;
2725
import javax.xml.namespace.QName;
2826

29-
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
30-
import net.shibboleth.utilities.java.support.xml.BasicParserPool;
3127
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
3228
import net.shibboleth.utilities.java.support.xml.XMLParserException;
33-
import org.opensaml.core.config.ConfigurationService;
34-
import org.opensaml.core.config.InitializationException;
35-
import org.opensaml.core.config.InitializationService;
3629
import org.opensaml.core.xml.XMLObject;
3730
import org.opensaml.core.xml.XMLObjectBuilderFactory;
38-
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
3931
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
4032
import org.opensaml.core.xml.io.MarshallerFactory;
4133
import org.opensaml.core.xml.io.MarshallingException;
@@ -62,24 +54,27 @@
6254
import org.w3c.dom.Element;
6355

6456
import org.springframework.security.saml2.Saml2Exception;
57+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
6558
import org.springframework.security.saml2.core.Saml2X509Credential;
6659
import org.springframework.util.Assert;
6760
import org.springframework.web.util.UriUtils;
6861

69-
import static java.lang.Boolean.FALSE;
70-
import static java.lang.Boolean.TRUE;
7162
import static java.util.Arrays.asList;
63+
import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getParserPool;
7264
import static org.springframework.util.StringUtils.hasText;
7365

7466
/**
7567
* @since 5.2
7668
*/
7769
final class OpenSamlImplementation {
70+
static {
71+
OpenSamlInitializationService.initialize();
72+
}
73+
7874
private static OpenSamlImplementation instance = new OpenSamlImplementation();
7975
private static XMLObjectBuilderFactory xmlObjectBuilderFactory =
8076
XMLObjectProviderRegistrySupport.getBuilderFactory();
8177

82-
private final BasicParserPool parserPool = new BasicParserPool();
8378
private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
8479
asList(
8580
new InlineEncryptedKeyResolver(),
@@ -88,74 +83,6 @@ final class OpenSamlImplementation {
8883
)
8984
);
9085

91-
private OpenSamlImplementation() {
92-
bootstrap();
93-
}
94-
95-
/*
96-
* ==============================================================
97-
* PRIVATE METHODS
98-
* ==============================================================
99-
*/
100-
private void bootstrap() {
101-
// configure default values
102-
// maxPoolSize = 5;
103-
this.parserPool.setMaxPoolSize(50);
104-
// coalescing = true;
105-
this.parserPool.setCoalescing(true);
106-
// expandEntityReferences = false;
107-
this.parserPool.setExpandEntityReferences(false);
108-
// ignoreComments = true;
109-
this.parserPool.setIgnoreComments(true);
110-
// ignoreElementContentWhitespace = true;
111-
this.parserPool.setIgnoreElementContentWhitespace(true);
112-
// namespaceAware = true;
113-
this.parserPool.setNamespaceAware(true);
114-
// schema = null;
115-
this.parserPool.setSchema(null);
116-
// dtdValidating = false;
117-
this.parserPool.setDTDValidating(false);
118-
// xincludeAware = false;
119-
this.parserPool.setXincludeAware(false);
120-
121-
Map<String, Object> builderAttributes = new HashMap<>();
122-
this.parserPool.setBuilderAttributes(builderAttributes);
123-
124-
Map<String, Boolean> parserBuilderFeatures = new HashMap<>();
125-
parserBuilderFeatures.put("http://apache.org/xml/features/disallow-doctype-decl", TRUE);
126-
parserBuilderFeatures.put(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE);
127-
parserBuilderFeatures.put("http://xml.org/sax/features/external-general-entities", FALSE);
128-
parserBuilderFeatures.put("http://apache.org/xml/features/validation/schema/normalized-value", FALSE);
129-
parserBuilderFeatures.put("http://xml.org/sax/features/external-parameter-entities", FALSE);
130-
parserBuilderFeatures.put("http://apache.org/xml/features/dom/defer-node-expansion", FALSE);
131-
this.parserPool.setBuilderFeatures(parserBuilderFeatures);
132-
133-
try {
134-
this.parserPool.initialize();
135-
}
136-
catch (ComponentInitializationException x) {
137-
throw new Saml2Exception("Unable to initialize OpenSaml v3 ParserPool", x);
138-
}
139-
140-
try {
141-
InitializationService.initialize();
142-
}
143-
catch (InitializationException e) {
144-
throw new Saml2Exception("Unable to initialize OpenSaml v3", e);
145-
}
146-
147-
XMLObjectProviderRegistry registry;
148-
synchronized (ConfigurationService.class) {
149-
registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
150-
if (registry == null) {
151-
registry = new XMLObjectProviderRegistry();
152-
ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
153-
}
154-
}
155-
156-
registry.setParserPool(this.parserPool);
157-
}
158-
15986
/*
16087
* ==============================================================
16188
* PUBLIC METHODS
@@ -259,7 +186,7 @@ private XMLObject resolve(byte[] xml) {
259186

260187
private XMLObject parse(byte[] xml) {
261188
try {
262-
Document document = this.parserPool.parse(new ByteArrayInputStream(xml));
189+
Document document = getParserPool().parse(new ByteArrayInputStream(xml));
263190
Element element = document.getDocumentElement();
264191
return getUnmarshallerFactory().getUnmarshaller(element).unmarshall(element);
265192
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.core;
18+
19+
import org.junit.Test;
20+
import org.opensaml.core.config.ConfigurationService;
21+
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
22+
23+
import org.springframework.security.saml2.Saml2Exception;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatCode;
27+
28+
/**
29+
* Tests for {@link OpenSamlInitializationService}
30+
*
31+
* @author Josh Cummings
32+
*/
33+
public class OpenSamlInitializationServiceTests {
34+
35+
@Test
36+
public void initializeWhenInvokedMultipleTimesThenInitializesOnce() {
37+
OpenSamlInitializationService.initialize();
38+
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
39+
assertThat(registry.getParserPool()).isNotNull();
40+
registry.setParserPool(null);
41+
OpenSamlInitializationService.initialize();
42+
assertThat(registry.getParserPool()).isNull();
43+
assertThatCode(() -> OpenSamlInitializationService.requireInitialize(r -> {}))
44+
.isInstanceOf(Saml2Exception.class)
45+
.hasMessageContaining("OpenSAML was already initialized previously");
46+
}
47+
}

0 commit comments

Comments
 (0)