Skip to content

Commit 8a35524

Browse files
jkubrynskijzheaux
authored andcommitted
SAML 2.0 SP Metadata Endpoint Support
Issue gh-8693
1 parent 31bae54 commit 8a35524

File tree

14 files changed

+518
-20
lines changed

14 files changed

+518
-20
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,6 +73,9 @@ 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());
7679
filterToOrder.put(
7780
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
7881
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,8 +38,11 @@
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;
4142
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
4243
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;
4346
import org.springframework.security.web.authentication.AuthenticationConverter;
4447
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
4548
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
@@ -110,10 +113,15 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extend
110113
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
111114

112115
private AuthenticationConverter authenticationConverter;
116+
117+
private Saml2MetadataResolver saml2MetadataResolver;
118+
113119
private AuthenticationManager authenticationManager;
114120

115121
private Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter;
116122

123+
private Saml2MetadataFilter saml2MetadataFilter;
124+
117125
/**
118126
* Use this {@link AuthenticationConverter} when converting incoming requests to an {@link Authentication}.
119127
* By default the {@link Saml2AuthenticationTokenConverter} is used.
@@ -154,6 +162,16 @@ public Saml2LoginConfigurer relyingPartyRegistrationRepository(RelyingPartyRegis
154162
return this;
155163
}
156164

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+
157175
/**
158176
* {@inheritDoc}
159177
*/
@@ -211,6 +229,14 @@ public void init(B http) throws Exception {
211229
setAuthenticationFilter(saml2WebSsoAuthenticationFilter);
212230
super.loginProcessingUrl(this.loginProcessingUrl);
213231

232+
if (this.saml2MetadataResolver == null) {
233+
this.saml2MetadataResolver = new OpenSamlMetadataResolver();
234+
}
235+
236+
saml2MetadataFilter = new Saml2MetadataFilter(
237+
this.relyingPartyRegistrationRepository, this.saml2MetadataResolver
238+
);
239+
214240
if (hasText(this.loginPage)) {
215241
// Set custom login page
216242
super.loginPage(this.loginPage);
@@ -250,6 +276,7 @@ public void init(B http) throws Exception {
250276
@Override
251277
public void configure(B http) throws Exception {
252278
http.addFilter(this.authenticationRequestEndpoint.build(http));
279+
http.addFilter(saml2MetadataFilter);
253280
super.configure(http);
254281
if (this.authenticationManager == null) {
255282
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.servlet.filter.Saml2WebSsoAuthenticationFilter
33+
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter
3434
import org.springframework.test.web.servlet.MockMvc
3535
import org.springframework.test.web.servlet.get
3636
import java.security.cert.Certificate

docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc

+3-15
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ the IDP sends an assertion to the SP.
6161

6262
1. Mappings assertion conditions and attributes to session features (timeout, tracking, etc)
6363
2. Single logout
64-
3. Dynamic metadata generation
65-
4. Receiving and validating standalone assertion (not wrapped in a response object)
64+
3. Receiving and validating standalone assertion (not wrapped in a response object)
6665

6766
[[servlet-saml2-javaconfig]]
6867
=== Saml 2 Login - Introduction to Java Configuration
@@ -200,19 +199,8 @@ credentials on all the identity providers.
200199
[[servlet-saml2-serviceprovider-metadata]]
201200
==== Service Provider Metadata
202201

203-
The Spring Security SAML 2 implementation does not yet provide an endpoint for downloading
204-
SP metadata in XML format. The minimal pieces that are exchanged
205-
206-
* *entity ID* - defaults to `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`
207-
Other known configuration names that also use this same value
208-
** Audience Restriction
209-
* *single signon URL* - defaults to `+{baseUrl}/login/saml2/sso/{registrationId}+`
210-
Other known configuration names that also use this same value
211-
** Recipient URL
212-
** Destination URL
213-
** Assertion Consumer Service URL
214-
* X509Certificate - the certificate that you configure as part of your {SIGNING,DECRYPTION}
215-
credentials must be shared with the Identity Provider
202+
The Spring Security SAML 2 implementation does provide an endpoint for downloading
203+
SP metadata in XML format. The provider is mapped to: `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`
216204

217205
[[servlet-saml2-sp-initiated]]
218206
==== Authentication Requests - SP Initiated Flow

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,6 +29,7 @@
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;
3233
import org.springframework.util.Assert;
3334

3435
/**
@@ -360,6 +361,7 @@ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration regi
360361
.encryptionX509Credentials(c -> c.addAll(registration.getAssertingPartyDetails().getEncryptionX509Credentials()))
361362
.singleSignOnServiceLocation(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
362363
.singleSignOnServiceBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
364+
.nameIdFormat(registration.getAssertingPartyDetails().getNameIdFormat())
363365
);
364366
}
365367

@@ -375,6 +377,7 @@ public final static class AssertingPartyDetails {
375377
private final Collection<Saml2X509Credential> verificationX509Credentials;
376378
private final Collection<Saml2X509Credential> encryptionX509Credentials;
377379
private final String singleSignOnServiceLocation;
380+
private final String nameIdFormat;
378381
private final Saml2MessageBinding singleSignOnServiceBinding;
379382

380383
private AssertingPartyDetails(
@@ -383,6 +386,7 @@ private AssertingPartyDetails(
383386
Collection<Saml2X509Credential> verificationX509Credentials,
384387
Collection<Saml2X509Credential> encryptionX509Credentials,
385388
String singleSignOnServiceLocation,
389+
String nameIdFormat,
386390
Saml2MessageBinding singleSignOnServiceBinding) {
387391

388392
Assert.hasText(entityId, "entityId cannot be null or empty");
@@ -405,6 +409,7 @@ private AssertingPartyDetails(
405409
this.verificationX509Credentials = verificationX509Credentials;
406410
this.encryptionX509Credentials = encryptionX509Credentials;
407411
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
412+
this.nameIdFormat = nameIdFormat;
408413
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
409414
}
410415

@@ -472,6 +477,15 @@ public String getSingleSignOnServiceLocation() {
472477
return this.singleSignOnServiceLocation;
473478
}
474479

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+
475489
/**
476490
* Get the
477491
* <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
@@ -493,6 +507,7 @@ public final static class Builder {
493507
private Collection<Saml2X509Credential> verificationX509Credentials = new HashSet<>();
494508
private Collection<Saml2X509Credential> encryptionX509Credentials = new HashSet<>();
495509
private String singleSignOnServiceLocation;
510+
private String nameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
496511
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
497512

498513
/**
@@ -562,6 +577,18 @@ public Builder singleSignOnServiceLocation(String singleSignOnServiceLocation) {
562577
return this;
563578
}
564579

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+
565592
/**
566593
* Set the
567594
* <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
@@ -590,6 +617,7 @@ public AssertingPartyDetails build() {
590617
this.verificationX509Credentials,
591618
this.encryptionX509Credentials,
592619
this.singleSignOnServiceLocation,
620+
this.nameIdFormat,
593621
this.singleSignOnServiceBinding
594622
);
595623
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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.web;
18+
19+
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
20+
import org.opensaml.core.xml.XMLObjectBuilder;
21+
import org.opensaml.core.xml.XMLObjectBuilderFactory;
22+
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
23+
import org.opensaml.core.xml.io.Marshaller;
24+
import org.opensaml.saml.common.xml.SAMLConstants;
25+
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
26+
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
27+
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
28+
import org.opensaml.saml.saml2.metadata.NameIDFormat;
29+
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
30+
import org.opensaml.security.credential.UsageType;
31+
import org.opensaml.xmlsec.signature.KeyInfo;
32+
import org.opensaml.xmlsec.signature.X509Certificate;
33+
import org.opensaml.xmlsec.signature.X509Data;
34+
import org.springframework.security.saml2.Saml2Exception;
35+
import org.springframework.security.saml2.credentials.Saml2X509Credential;
36+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
37+
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2ServletUtils;
38+
import org.w3c.dom.Element;
39+
40+
import javax.servlet.http.HttpServletRequest;
41+
import javax.xml.namespace.QName;
42+
import java.security.cert.CertificateEncodingException;
43+
import java.util.ArrayList;
44+
import java.util.Base64;
45+
import java.util.List;
46+
47+
/**
48+
* @author Jakub Kubrynski
49+
* @since 5.4
50+
*/
51+
public class OpenSamlMetadataResolver implements Saml2MetadataResolver {
52+
53+
@Override
54+
public String resolveMetadata(HttpServletRequest request, RelyingPartyRegistration registration) {
55+
56+
XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
57+
58+
EntityDescriptor entityDescriptor = buildObject(builderFactory, EntityDescriptor.ELEMENT_QNAME);
59+
60+
entityDescriptor.setEntityID(
61+
resolveTemplate(registration.getEntityId(), registration, request));
62+
63+
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration, builderFactory, request);
64+
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
65+
66+
return serializeToXmlString(entityDescriptor);
67+
}
68+
69+
private String serializeToXmlString(EntityDescriptor entityDescriptor) {
70+
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(entityDescriptor);
71+
if (marshaller == null) {
72+
throw new Saml2Exception("Unable to resolve Marshaller");
73+
}
74+
Element element;
75+
try {
76+
element = marshaller.marshall(entityDescriptor);
77+
} catch (Exception e) {
78+
throw new Saml2Exception(e);
79+
}
80+
return SerializeSupport.prettyPrintXML(element);
81+
}
82+
83+
private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration,
84+
XMLObjectBuilderFactory builderFactory, HttpServletRequest request) {
85+
86+
SPSSODescriptor spSsoDescriptor = buildObject(builderFactory, SPSSODescriptor.DEFAULT_ELEMENT_NAME);
87+
spSsoDescriptor.setAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned());
88+
spSsoDescriptor.setWantAssertionsSigned(true);
89+
spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
90+
91+
NameIDFormat nameIdFormat = buildObject(builderFactory, NameIDFormat.DEFAULT_ELEMENT_NAME);
92+
nameIdFormat.setFormat(registration.getAssertingPartyDetails().getNameIdFormat());
93+
spSsoDescriptor.getNameIDFormats().add(nameIdFormat);
94+
95+
spSsoDescriptor.getAssertionConsumerServices().add(
96+
buildAssertionConsumerService(registration, builderFactory, request));
97+
98+
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(builderFactory,
99+
registration.getSigningCredentials(), UsageType.SIGNING));
100+
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(builderFactory,
101+
registration.getEncryptionCredentials(), UsageType.ENCRYPTION));
102+
103+
return spSsoDescriptor;
104+
}
105+
106+
private List<KeyDescriptor> buildKeys(XMLObjectBuilderFactory builderFactory,
107+
List<Saml2X509Credential> credentials, UsageType usageType) {
108+
List<KeyDescriptor> list = new ArrayList<>();
109+
for (Saml2X509Credential credential : credentials) {
110+
KeyDescriptor keyDescriptor = buildKeyDescriptor(builderFactory, usageType, credential.getCertificate());
111+
list.add(keyDescriptor);
112+
}
113+
return list;
114+
}
115+
116+
private KeyDescriptor buildKeyDescriptor(XMLObjectBuilderFactory builderFactory, UsageType usageType,
117+
java.security.cert.X509Certificate certificate) {
118+
KeyDescriptor keyDescriptor = buildObject(builderFactory, KeyDescriptor.DEFAULT_ELEMENT_NAME);
119+
KeyInfo keyInfo = buildObject(builderFactory, KeyInfo.DEFAULT_ELEMENT_NAME);
120+
X509Certificate x509Certificate = buildObject(builderFactory, X509Certificate.DEFAULT_ELEMENT_NAME);
121+
X509Data x509Data = buildObject(builderFactory, X509Data.DEFAULT_ELEMENT_NAME);
122+
123+
try {
124+
x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded())));
125+
} catch (CertificateEncodingException e) {
126+
throw new Saml2Exception("Cannot encode certificate " + certificate.toString());
127+
}
128+
129+
x509Data.getX509Certificates().add(x509Certificate);
130+
keyInfo.getX509Datas().add(x509Data);
131+
132+
keyDescriptor.setUse(usageType);
133+
keyDescriptor.setKeyInfo(keyInfo);
134+
return keyDescriptor;
135+
}
136+
137+
private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration,
138+
XMLObjectBuilderFactory builderFactory, HttpServletRequest request) {
139+
AssertionConsumerService assertionConsumerService = buildObject(builderFactory, AssertionConsumerService.DEFAULT_ELEMENT_NAME);
140+
141+
assertionConsumerService.setLocation(
142+
resolveTemplate(registration.getAssertionConsumerServiceLocation(), registration, request));
143+
assertionConsumerService.setBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding().getUrn());
144+
assertionConsumerService.setIndex(1);
145+
return assertionConsumerService;
146+
}
147+
148+
@SuppressWarnings("unchecked")
149+
private <T> T buildObject(XMLObjectBuilderFactory builderFactory, QName elementName) {
150+
XMLObjectBuilder<?> builder = builderFactory.getBuilder(elementName);
151+
if (builder == null) {
152+
throw new Saml2Exception("Cannot build object - builder not defined for element " + elementName);
153+
}
154+
return (T) builder.buildObject(elementName);
155+
}
156+
157+
private String resolveTemplate(String template, RelyingPartyRegistration registration, HttpServletRequest request) {
158+
return Saml2ServletUtils.resolveUrlTemplate(template, Saml2ServletUtils.getApplicationUri(request), registration);
159+
}
160+
161+
}

0 commit comments

Comments
 (0)