Skip to content

Commit ff9a925

Browse files
committed
Use OpenSAML API for metadata
Issue gh-11658
1 parent 80b3182 commit ff9a925

File tree

11 files changed

+1513
-271
lines changed

11 files changed

+1513
-271
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2323
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2424
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
25-
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
25+
import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver;
2626
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
2727
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
2828
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -104,7 +104,7 @@ public Saml2MetadataConfigurer<H> metadataUrl(String metadataUrl) {
104104
Assert.hasText(metadataUrl, "metadataUrl cannot be empty");
105105
this.metadataResponseResolver = (registrations) -> {
106106
RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations,
107-
new OpenSamlMetadataResolver());
107+
new OpenSaml4MetadataResolver());
108108
metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl));
109109
return metadata;
110110
};
@@ -143,7 +143,7 @@ private Saml2MetadataResponseResolver createMetadataResponseResolver(H http) {
143143
return metadataResponseResolver;
144144
}
145145
RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
146-
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver());
146+
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver());
147147
}
148148

149149
private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) {

config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
3131
import org.springframework.security.config.test.SpringTestContext;
3232
import org.springframework.security.config.test.SpringTestContextExtension;
33-
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
33+
import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver;
3434
import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
3535
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
3636
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
@@ -159,7 +159,7 @@ SecurityFilterChain filters(HttpSecurity http) throws Exception {
159159
// should ignore
160160
@Bean
161161
Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) {
162-
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver());
162+
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver());
163163
}
164164

165165
}

saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ sourceSets.configureEach { set ->
1919
filter { line -> line.replaceAll(".saml2.internal", ".saml2.provider.service.authentication") }
2020
with from
2121
}
22+
23+
copy {
24+
into "$projectDir/src/$set.name/java/org/springframework/security/saml2/provider/service/metadata"
25+
filter { line -> line.replaceAll(".saml2.internal", ".saml2.provider.service.metadata") }
26+
with from
27+
}
2228
}
2329

2430
dependencies {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/*
2+
* Copyright 2002-2024 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.provider.service.metadata;
18+
19+
import java.security.cert.CertificateEncodingException;
20+
import java.util.ArrayList;
21+
import java.util.Base64;
22+
import java.util.Collection;
23+
import java.util.List;
24+
import java.util.function.Consumer;
25+
26+
import org.apache.commons.logging.Log;
27+
import org.apache.commons.logging.LogFactory;
28+
import org.opensaml.saml.common.xml.SAMLConstants;
29+
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
30+
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
31+
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
32+
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
33+
import org.opensaml.saml.saml2.metadata.NameIDFormat;
34+
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
35+
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
36+
import org.opensaml.security.credential.UsageType;
37+
import org.opensaml.xmlsec.signature.KeyInfo;
38+
import org.opensaml.xmlsec.signature.X509Certificate;
39+
import org.opensaml.xmlsec.signature.X509Data;
40+
41+
import org.springframework.security.saml2.Saml2Exception;
42+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
43+
import org.springframework.security.saml2.core.Saml2X509Credential;
44+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
45+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
46+
import org.springframework.util.Assert;
47+
48+
/**
49+
* Resolves the SAML 2.0 Relying Party Metadata for a given
50+
* {@link RelyingPartyRegistration} using the OpenSAML API.
51+
*
52+
* @author Jakub Kubrynski
53+
* @author Josh Cummings
54+
* @since 5.4
55+
*/
56+
final class BaseOpenSamlMetadataResolver implements Saml2MetadataResolver {
57+
58+
static {
59+
OpenSamlInitializationService.initialize();
60+
}
61+
62+
private final Log logger = LogFactory.getLog(this.getClass());
63+
64+
private OpenSamlOperations saml;
65+
66+
private Consumer<EntityDescriptorParameters> entityDescriptorCustomizer = (parameters) -> {
67+
};
68+
69+
private boolean usePrettyPrint = true;
70+
71+
private boolean signMetadata = false;
72+
73+
BaseOpenSamlMetadataResolver(OpenSamlOperations saml) {
74+
this.saml = saml;
75+
}
76+
77+
@Override
78+
public String resolve(RelyingPartyRegistration relyingPartyRegistration) {
79+
EntityDescriptor entityDescriptor = entityDescriptor(relyingPartyRegistration);
80+
return serialize(entityDescriptor);
81+
}
82+
83+
@Override
84+
public String resolve(Iterable<RelyingPartyRegistration> relyingPartyRegistrations) {
85+
Collection<EntityDescriptor> entityDescriptors = new ArrayList<>();
86+
for (RelyingPartyRegistration registration : relyingPartyRegistrations) {
87+
EntityDescriptor entityDescriptor = entityDescriptor(registration);
88+
entityDescriptors.add(entityDescriptor);
89+
}
90+
if (entityDescriptors.size() == 1) {
91+
return serialize(entityDescriptors.iterator().next());
92+
}
93+
EntitiesDescriptor entities = this.saml.build(EntitiesDescriptor.DEFAULT_ELEMENT_NAME);
94+
entities.getEntityDescriptors().addAll(entityDescriptors);
95+
return serialize(entities);
96+
}
97+
98+
private EntityDescriptor entityDescriptor(RelyingPartyRegistration registration) {
99+
EntityDescriptor entityDescriptor = this.saml.build(EntityDescriptor.DEFAULT_ELEMENT_NAME);
100+
entityDescriptor.setEntityID(registration.getEntityId());
101+
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration);
102+
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
103+
this.entityDescriptorCustomizer.accept(new EntityDescriptorParameters(entityDescriptor, registration));
104+
if (this.signMetadata) {
105+
return this.saml.withSigningKeys(registration.getSigningX509Credentials())
106+
.algorithms(registration.getAssertingPartyMetadata().getSigningAlgorithms())
107+
.sign(entityDescriptor);
108+
}
109+
else {
110+
this.logger.trace("Did not sign metadata since `signMetadata` is `false`");
111+
}
112+
return entityDescriptor;
113+
}
114+
115+
/**
116+
* Set a {@link Consumer} for modifying the OpenSAML {@link EntityDescriptor}
117+
* @param entityDescriptorCustomizer a consumer that accepts an
118+
* {@link EntityDescriptorParameters}
119+
* @since 5.7
120+
*/
121+
void setEntityDescriptorCustomizer(Consumer<EntityDescriptorParameters> entityDescriptorCustomizer) {
122+
Assert.notNull(entityDescriptorCustomizer, "entityDescriptorCustomizer cannot be null");
123+
this.entityDescriptorCustomizer = entityDescriptorCustomizer;
124+
}
125+
126+
/**
127+
* Configure whether to pretty-print the metadata XML. This can be helpful when
128+
* signing the metadata payload.
129+
*
130+
* @since 6.2
131+
**/
132+
void setUsePrettyPrint(boolean usePrettyPrint) {
133+
this.usePrettyPrint = usePrettyPrint;
134+
}
135+
136+
private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) {
137+
SPSSODescriptor spSsoDescriptor = this.saml.build(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
138+
spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
139+
spSsoDescriptor.getKeyDescriptors()
140+
.addAll(buildKeys(registration.getSigningX509Credentials(), UsageType.SIGNING));
141+
spSsoDescriptor.getKeyDescriptors()
142+
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
143+
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
144+
if (registration.getSingleLogoutServiceLocation() != null) {
145+
for (Saml2MessageBinding binding : registration.getSingleLogoutServiceBindings()) {
146+
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration, binding));
147+
}
148+
}
149+
if (registration.getNameIdFormat() != null) {
150+
spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration));
151+
}
152+
return spSsoDescriptor;
153+
}
154+
155+
private List<KeyDescriptor> buildKeys(Collection<Saml2X509Credential> credentials, UsageType usageType) {
156+
List<KeyDescriptor> list = new ArrayList<>();
157+
for (Saml2X509Credential credential : credentials) {
158+
KeyDescriptor keyDescriptor = buildKeyDescriptor(usageType, credential.getCertificate());
159+
list.add(keyDescriptor);
160+
}
161+
return list;
162+
}
163+
164+
private KeyDescriptor buildKeyDescriptor(UsageType usageType, java.security.cert.X509Certificate certificate) {
165+
KeyDescriptor keyDescriptor = this.saml.build(KeyDescriptor.DEFAULT_ELEMENT_NAME);
166+
KeyInfo keyInfo = this.saml.build(KeyInfo.DEFAULT_ELEMENT_NAME);
167+
X509Certificate x509Certificate = this.saml.build(X509Certificate.DEFAULT_ELEMENT_NAME);
168+
X509Data x509Data = this.saml.build(X509Data.DEFAULT_ELEMENT_NAME);
169+
try {
170+
x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded())));
171+
}
172+
catch (CertificateEncodingException ex) {
173+
throw new Saml2Exception("Cannot encode certificate " + certificate.toString());
174+
}
175+
x509Data.getX509Certificates().add(x509Certificate);
176+
keyInfo.getX509Datas().add(x509Data);
177+
keyDescriptor.setUse(usageType);
178+
keyDescriptor.setKeyInfo(keyInfo);
179+
return keyDescriptor;
180+
}
181+
182+
private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration) {
183+
AssertionConsumerService assertionConsumerService = this.saml
184+
.build(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
185+
assertionConsumerService.setLocation(registration.getAssertionConsumerServiceLocation());
186+
assertionConsumerService.setBinding(registration.getAssertionConsumerServiceBinding().getUrn());
187+
assertionConsumerService.setIndex(1);
188+
return assertionConsumerService;
189+
}
190+
191+
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration,
192+
Saml2MessageBinding binding) {
193+
SingleLogoutService singleLogoutService = this.saml.build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
194+
singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation());
195+
singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
196+
singleLogoutService.setBinding(binding.getUrn());
197+
return singleLogoutService;
198+
}
199+
200+
private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) {
201+
NameIDFormat nameIdFormat = this.saml.build(NameIDFormat.DEFAULT_ELEMENT_NAME);
202+
nameIdFormat.setURI(registration.getNameIdFormat());
203+
return nameIdFormat;
204+
}
205+
206+
private String serialize(EntityDescriptor entityDescriptor) {
207+
return this.saml.serialize(entityDescriptor).prettyPrint(this.usePrettyPrint).serialize();
208+
}
209+
210+
private String serialize(EntitiesDescriptor entities) {
211+
return this.saml.serialize(entities).prettyPrint(this.usePrettyPrint).serialize();
212+
}
213+
214+
/**
215+
* Configure whether to sign the metadata, defaults to {@code false}.
216+
*
217+
* @since 6.4
218+
*/
219+
void setSignMetadata(boolean signMetadata) {
220+
this.signMetadata = signMetadata;
221+
}
222+
223+
/**
224+
* A tuple containing an OpenSAML {@link EntityDescriptor} and its associated
225+
* {@link RelyingPartyRegistration}
226+
*
227+
* @since 5.7
228+
*/
229+
static final class EntityDescriptorParameters {
230+
231+
private final EntityDescriptor entityDescriptor;
232+
233+
private final RelyingPartyRegistration registration;
234+
235+
EntityDescriptorParameters(EntityDescriptor entityDescriptor, RelyingPartyRegistration registration) {
236+
this.entityDescriptor = entityDescriptor;
237+
this.registration = registration;
238+
}
239+
240+
EntityDescriptor getEntityDescriptor() {
241+
return this.entityDescriptor;
242+
}
243+
244+
RelyingPartyRegistration getRelyingPartyRegistration() {
245+
return this.registration;
246+
}
247+
248+
}
249+
250+
}

0 commit comments

Comments
 (0)