Skip to content

Commit b999faa

Browse files
committed
Complete SAML 2.0 SP Metadata Endpoint
Closes gh-8693
1 parent 8a35524 commit b999faa

File tree

15 files changed

+371
-358
lines changed

15 files changed

+371
-358
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java

-3
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
7373
filterToOrder.put(
7474
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
7575
order.next());
76-
filterToOrder.put(
77-
"org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter",
78-
order.next());
7976
filterToOrder.put(
8077
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
8178
order.next());

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

-27
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,8 @@
3838
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
3939
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
4040
import org.springframework.security.saml2.provider.service.web.DefaultSaml2AuthenticationRequestContextResolver;
41-
import org.springframework.security.saml2.provider.service.web.OpenSamlMetadataResolver;
4241
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
4342
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
44-
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
45-
import org.springframework.security.saml2.provider.service.web.Saml2MetadataResolver;
4643
import org.springframework.security.web.authentication.AuthenticationConverter;
4744
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
4845
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
@@ -113,15 +110,10 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extend
113110
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
114111

115112
private AuthenticationConverter authenticationConverter;
116-
117-
private Saml2MetadataResolver saml2MetadataResolver;
118-
119113
private AuthenticationManager authenticationManager;
120114

121115
private Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter;
122116

123-
private Saml2MetadataFilter saml2MetadataFilter;
124-
125117
/**
126118
* Use this {@link AuthenticationConverter} when converting incoming requests to an {@link Authentication}.
127119
* By default the {@link Saml2AuthenticationTokenConverter} is used.
@@ -162,16 +154,6 @@ public Saml2LoginConfigurer relyingPartyRegistrationRepository(RelyingPartyRegis
162154
return this;
163155
}
164156

165-
/**
166-
* Sets the {@code Saml2MetadataResolver}
167-
* @param saml2MetadataResolver the implementation of the metadata resolver
168-
* @return the {@link Saml2LoginConfigurer} for further configuration
169-
*/
170-
public Saml2LoginConfigurer saml2MetadataResolver(Saml2MetadataResolver saml2MetadataResolver) {
171-
this.saml2MetadataResolver = saml2MetadataResolver;
172-
return this;
173-
}
174-
175157
/**
176158
* {@inheritDoc}
177159
*/
@@ -229,14 +211,6 @@ public void init(B http) throws Exception {
229211
setAuthenticationFilter(saml2WebSsoAuthenticationFilter);
230212
super.loginProcessingUrl(this.loginProcessingUrl);
231213

232-
if (this.saml2MetadataResolver == null) {
233-
this.saml2MetadataResolver = new OpenSamlMetadataResolver();
234-
}
235-
236-
saml2MetadataFilter = new Saml2MetadataFilter(
237-
this.relyingPartyRegistrationRepository, this.saml2MetadataResolver
238-
);
239-
240214
if (hasText(this.loginPage)) {
241215
// Set custom login page
242216
super.loginPage(this.loginPage);
@@ -276,7 +250,6 @@ public void init(B http) throws Exception {
276250
@Override
277251
public void configure(B http) throws Exception {
278252
http.addFilter(this.authenticationRequestEndpoint.build(http));
279-
http.addFilter(saml2MetadataFilter);
280253
super.configure(http);
281254
if (this.authenticationManager == null) {
282255
registerDefaultAuthenticationProvider(http);

config/src/test/kotlin/org/springframework/security/config/web/servlet/Saml2DslTests.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import org.springframework.security.saml2.credentials.Saml2X509Credential
3030
import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION
3131
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository
3232
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration
33-
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter
33+
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter
3434
import org.springframework.test.web.servlet.MockMvc
3535
import org.springframework.test.web.servlet.get
3636
import java.security.cert.Certificate
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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.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 javax.xml.namespace.QName;
25+
26+
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
27+
import org.opensaml.core.xml.XMLObjectBuilder;
28+
import org.opensaml.saml.common.xml.SAMLConstants;
29+
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
30+
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
31+
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
32+
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
33+
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
34+
import org.opensaml.security.credential.UsageType;
35+
import org.opensaml.xmlsec.signature.KeyInfo;
36+
import org.opensaml.xmlsec.signature.X509Certificate;
37+
import org.opensaml.xmlsec.signature.X509Data;
38+
import org.w3c.dom.Element;
39+
40+
import org.springframework.security.saml2.Saml2Exception;
41+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
42+
import org.springframework.security.saml2.core.Saml2X509Credential;
43+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
44+
import org.springframework.util.Assert;
45+
46+
import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getBuilderFactory;
47+
import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getMarshallerFactory;
48+
49+
/**
50+
* Resolves the SAML 2.0 Relying Party Metadata for a given {@link RelyingPartyRegistration}
51+
* using the OpenSAML API.
52+
*
53+
* @author Jakub Kubrynski
54+
* @author Josh Cummings
55+
* @since 5.4
56+
*/
57+
public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
58+
static {
59+
OpenSamlInitializationService.initialize();
60+
}
61+
62+
private final EntityDescriptorMarshaller entityDescriptorMarshaller;
63+
64+
public OpenSamlMetadataResolver() {
65+
this.entityDescriptorMarshaller = (EntityDescriptorMarshaller)
66+
getMarshallerFactory().getMarshaller(EntityDescriptor.DEFAULT_ELEMENT_NAME);
67+
Assert.notNull(this.entityDescriptorMarshaller, "entityDescriptorMarshaller cannot be null");
68+
}
69+
70+
/**
71+
* {@inheritDoc}
72+
*/
73+
@Override
74+
public String resolve(RelyingPartyRegistration relyingPartyRegistration) {
75+
EntityDescriptor entityDescriptor = build(EntityDescriptor.ELEMENT_QNAME);
76+
entityDescriptor.setEntityID(relyingPartyRegistration.getEntityId());
77+
78+
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(relyingPartyRegistration);
79+
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
80+
81+
return serialize(entityDescriptor);
82+
}
83+
84+
private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) {
85+
SPSSODescriptor spSsoDescriptor = build(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
86+
spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
87+
spSsoDescriptor.setWantAssertionsSigned(true);
88+
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(
89+
registration.getSigningX509Credentials(), UsageType.SIGNING));
90+
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(
91+
registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
92+
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
93+
return spSsoDescriptor;
94+
}
95+
96+
private List<KeyDescriptor> buildKeys(Collection<Saml2X509Credential> credentials, UsageType usageType) {
97+
List<KeyDescriptor> list = new ArrayList<>();
98+
for (Saml2X509Credential credential : credentials) {
99+
KeyDescriptor keyDescriptor = buildKeyDescriptor(usageType, credential.getCertificate());
100+
list.add(keyDescriptor);
101+
}
102+
return list;
103+
}
104+
105+
private KeyDescriptor buildKeyDescriptor(UsageType usageType, java.security.cert.X509Certificate certificate) {
106+
KeyDescriptor keyDescriptor = build(KeyDescriptor.DEFAULT_ELEMENT_NAME);
107+
KeyInfo keyInfo = build(KeyInfo.DEFAULT_ELEMENT_NAME);
108+
X509Certificate x509Certificate = build(X509Certificate.DEFAULT_ELEMENT_NAME);
109+
X509Data x509Data = build(X509Data.DEFAULT_ELEMENT_NAME);
110+
111+
try {
112+
x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded())));
113+
} catch (CertificateEncodingException e) {
114+
throw new Saml2Exception("Cannot encode certificate " + certificate.toString());
115+
}
116+
117+
x509Data.getX509Certificates().add(x509Certificate);
118+
keyInfo.getX509Datas().add(x509Data);
119+
120+
keyDescriptor.setUse(usageType);
121+
keyDescriptor.setKeyInfo(keyInfo);
122+
return keyDescriptor;
123+
}
124+
125+
private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration) {
126+
AssertionConsumerService assertionConsumerService = build(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
127+
assertionConsumerService.setLocation(registration.getAssertionConsumerServiceLocation());
128+
assertionConsumerService.setBinding(registration.getAssertionConsumerServiceBinding().getUrn());
129+
assertionConsumerService.setIndex(1);
130+
return assertionConsumerService;
131+
}
132+
133+
@SuppressWarnings("unchecked")
134+
private <T> T build(QName elementName) {
135+
XMLObjectBuilder<?> builder = getBuilderFactory().getBuilder(elementName);
136+
if (builder == null) {
137+
throw new Saml2Exception("Unable to resolve Builder for " + elementName);
138+
}
139+
return (T) builder.buildObject(elementName);
140+
}
141+
142+
143+
private String serialize(EntityDescriptor entityDescriptor) {
144+
try {
145+
Element element = this.entityDescriptorMarshaller.marshall(entityDescriptor);
146+
return SerializeSupport.prettyPrintXML(element);
147+
} catch (Exception e) {
148+
throw new Saml2Exception(e);
149+
}
150+
}
151+
}
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,23 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.security.saml2.provider.service.web;
17+
package org.springframework.security.saml2.provider.service.metadata;
1818

1919
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
2020

21-
import javax.servlet.http.HttpServletRequest;
22-
2321
/**
22+
* Resolves the SAML 2.0 Relying Party Metadata for a given {@link RelyingPartyRegistration}
23+
*
2424
* @author Jakub Kubrynski
25+
* @author Josh Cummings
2526
* @since 5.4
2627
*/
2728
public interface Saml2MetadataResolver {
28-
String resolveMetadata(HttpServletRequest request, RelyingPartyRegistration registration);
29+
/**
30+
* Resolve the given relying party's metadata
31+
*
32+
* @param relyingPartyRegistration the relying party
33+
* @return the relying party's metadata
34+
*/
35+
String resolve(RelyingPartyRegistration relyingPartyRegistration);
2936
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java

-28
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
import org.springframework.security.saml2.core.Saml2X509Credential;
3131
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
32-
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter;
3332
import org.springframework.util.Assert;
3433

3534
/**
@@ -361,7 +360,6 @@ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration regi
361360
.encryptionX509Credentials(c -> c.addAll(registration.getAssertingPartyDetails().getEncryptionX509Credentials()))
362361
.singleSignOnServiceLocation(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
363362
.singleSignOnServiceBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
364-
.nameIdFormat(registration.getAssertingPartyDetails().getNameIdFormat())
365363
);
366364
}
367365

@@ -377,7 +375,6 @@ public final static class AssertingPartyDetails {
377375
private final Collection<Saml2X509Credential> verificationX509Credentials;
378376
private final Collection<Saml2X509Credential> encryptionX509Credentials;
379377
private final String singleSignOnServiceLocation;
380-
private final String nameIdFormat;
381378
private final Saml2MessageBinding singleSignOnServiceBinding;
382379

383380
private AssertingPartyDetails(
@@ -386,7 +383,6 @@ private AssertingPartyDetails(
386383
Collection<Saml2X509Credential> verificationX509Credentials,
387384
Collection<Saml2X509Credential> encryptionX509Credentials,
388385
String singleSignOnServiceLocation,
389-
String nameIdFormat,
390386
Saml2MessageBinding singleSignOnServiceBinding) {
391387

392388
Assert.hasText(entityId, "entityId cannot be null or empty");
@@ -409,7 +405,6 @@ private AssertingPartyDetails(
409405
this.verificationX509Credentials = verificationX509Credentials;
410406
this.encryptionX509Credentials = encryptionX509Credentials;
411407
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
412-
this.nameIdFormat = nameIdFormat;
413408
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
414409
}
415410

@@ -477,15 +472,6 @@ public String getSingleSignOnServiceLocation() {
477472
return this.singleSignOnServiceLocation;
478473
}
479474

480-
/**
481-
* Get the NameIDFormat setting, indicating which user property should be used as a NameID Format attribute
482-
*
483-
* @return the NameIdFormat value
484-
*/
485-
public String getNameIdFormat() {
486-
return nameIdFormat;
487-
}
488-
489475
/**
490476
* Get the
491477
* <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
@@ -507,7 +493,6 @@ public final static class Builder {
507493
private Collection<Saml2X509Credential> verificationX509Credentials = new HashSet<>();
508494
private Collection<Saml2X509Credential> encryptionX509Credentials = new HashSet<>();
509495
private String singleSignOnServiceLocation;
510-
private String nameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
511496
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
512497

513498
/**
@@ -577,18 +562,6 @@ public Builder singleSignOnServiceLocation(String singleSignOnServiceLocation) {
577562
return this;
578563
}
579564

580-
/**
581-
* Set the preference for name identifier returned by IdP.
582-
* See <a href="https://wiki.shibboleth.net/confluence/display/SHIB/NameIdentifierFormat">for possible values</a>
583-
*
584-
* @param nameIdFormat the name identifier
585-
* @return the {@link ProviderDetails.Builder} for further configuration
586-
*/
587-
public Builder nameIdFormat(String nameIdFormat) {
588-
this.nameIdFormat = nameIdFormat;
589-
return this;
590-
}
591-
592565
/**
593566
* Set the
594567
* <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
@@ -617,7 +590,6 @@ public AssertingPartyDetails build() {
617590
this.verificationX509Credentials,
618591
this.encryptionX509Credentials,
619592
this.singleSignOnServiceLocation,
620-
this.nameIdFormat,
621593
this.singleSignOnServiceBinding
622594
);
623595
}

0 commit comments

Comments
 (0)