forked from spring-projects/spring-security
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix spring-projects#8693 Support SAML 2.0 SP Metadata Endpoints
- Loading branch information
1 parent
793926b
commit 89e48df
Showing
7 changed files
with
440 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
160 changes: 160 additions & 0 deletions
160
...springframework/security/saml2/provider/service/servlet/filter/SamlMetadataGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/* | ||
* 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.provider.service.servlet.filter; | ||
|
||
import net.shibboleth.utilities.java.support.xml.SerializeSupport; | ||
import org.opensaml.core.config.ConfigurationService; | ||
import org.opensaml.core.xml.XMLObjectBuilder; | ||
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.Marshaller; | ||
import org.opensaml.core.xml.io.MarshallingException; | ||
import org.opensaml.saml.common.xml.SAMLConstants; | ||
import org.opensaml.saml.saml2.core.NameIDType; | ||
import org.opensaml.saml.saml2.metadata.AssertionConsumerService; | ||
import org.opensaml.saml.saml2.metadata.EntityDescriptor; | ||
import org.opensaml.saml.saml2.metadata.KeyDescriptor; | ||
import org.opensaml.saml.saml2.metadata.NameIDFormat; | ||
import org.opensaml.saml.saml2.metadata.SPSSODescriptor; | ||
import org.opensaml.security.credential.UsageType; | ||
import org.opensaml.xmlsec.signature.KeyInfo; | ||
import org.opensaml.xmlsec.signature.X509Certificate; | ||
import org.opensaml.xmlsec.signature.X509Data; | ||
import org.springframework.security.saml2.Saml2Exception; | ||
import org.springframework.security.saml2.credentials.Saml2X509Credential; | ||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; | ||
import org.w3c.dom.Element; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import javax.xml.namespace.QName; | ||
import java.security.cert.CertificateEncodingException; | ||
import java.util.Base64; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* @author Jakub Kubrynski | ||
* @since 5.4 | ||
*/ | ||
class SamlMetadataGenerator { | ||
|
||
String generateMetadata(RelyingPartyRegistration registration, HttpServletRequest request) { | ||
|
||
XMLObjectBuilderFactory builderFactory = ConfigurationService.get(XMLObjectProviderRegistry.class).getBuilderFactory(); | ||
|
||
EntityDescriptor entityDescriptor = buildObject(builderFactory, EntityDescriptor.ELEMENT_QNAME); | ||
|
||
entityDescriptor.setEntityID( | ||
resolveTemplate(registration.getLocalEntityIdTemplate(), registration, request)); | ||
|
||
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration, builderFactory, request); | ||
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor); | ||
|
||
return serializeToXmlString(entityDescriptor); | ||
} | ||
|
||
private String serializeToXmlString(EntityDescriptor entityDescriptor) { | ||
try { | ||
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(entityDescriptor); | ||
if (marshaller == null) { | ||
throw new Saml2Exception("Unable to resolve Marshaller"); | ||
} | ||
Element element = marshaller.marshall(entityDescriptor); | ||
return SerializeSupport.prettyPrintXML(element); | ||
} catch (MarshallingException e) { | ||
throw new Saml2Exception(e); | ||
} | ||
} | ||
|
||
private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration, | ||
XMLObjectBuilderFactory builderFactory, HttpServletRequest request) { | ||
|
||
SPSSODescriptor spSsoDescriptor = buildObject(builderFactory, SPSSODescriptor.DEFAULT_ELEMENT_NAME); | ||
spSsoDescriptor.setAuthnRequestsSigned(registration.getProviderDetails().isSignAuthNRequest()); | ||
spSsoDescriptor.setWantAssertionsSigned(true); | ||
spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); | ||
|
||
NameIDFormat nameIdFormat = buildObject(builderFactory, NameIDFormat.DEFAULT_ELEMENT_NAME); | ||
nameIdFormat.setFormat(NameIDType.EMAIL); | ||
spSsoDescriptor.getNameIDFormats().add(nameIdFormat); | ||
|
||
spSsoDescriptor.getAssertionConsumerServices().add( | ||
buildAssertionConsumerService(registration, builderFactory, request)); | ||
|
||
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(builderFactory, | ||
registration.getSigningCredentials(), UsageType.SIGNING)); | ||
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(builderFactory, | ||
registration.getEncryptionCredentials(), UsageType.ENCRYPTION)); | ||
|
||
return spSsoDescriptor; | ||
} | ||
|
||
private List<KeyDescriptor> buildKeys(XMLObjectBuilderFactory builderFactory, | ||
List<Saml2X509Credential> credentials, UsageType usageType) { | ||
return credentials | ||
.stream() | ||
.map(credential -> buildKeyDescriptor(builderFactory, usageType, credential.getCertificate())) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private KeyDescriptor buildKeyDescriptor(XMLObjectBuilderFactory builderFactory, UsageType usageType, | ||
java.security.cert.X509Certificate certificate) { | ||
KeyDescriptor keyDescriptor = buildObject(builderFactory, KeyDescriptor.DEFAULT_ELEMENT_NAME); | ||
KeyInfo keyInfo = buildObject(builderFactory, KeyInfo.DEFAULT_ELEMENT_NAME); | ||
X509Certificate x509Certificate = buildObject(builderFactory, X509Certificate.DEFAULT_ELEMENT_NAME); | ||
X509Data x509Data = buildObject(builderFactory, X509Data.DEFAULT_ELEMENT_NAME); | ||
|
||
try { | ||
x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded()))); | ||
} catch (CertificateEncodingException e) { | ||
throw new Saml2Exception("Cannot encode certificate " + certificate.toString()); | ||
} | ||
|
||
x509Data.getX509Certificates().add(x509Certificate); | ||
keyInfo.getX509Datas().add(x509Data); | ||
|
||
keyDescriptor.setUse(usageType); | ||
keyDescriptor.setKeyInfo(keyInfo); | ||
return keyDescriptor; | ||
} | ||
|
||
private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration, | ||
XMLObjectBuilderFactory builderFactory, HttpServletRequest request) { | ||
AssertionConsumerService assertionConsumerService = buildObject(builderFactory, AssertionConsumerService.DEFAULT_ELEMENT_NAME); | ||
|
||
assertionConsumerService.setLocation( | ||
resolveTemplate(registration.getAssertionConsumerServiceUrlTemplate(), registration, request)); | ||
assertionConsumerService.setBinding(registration.getProviderDetails().getBinding().getUrn()); | ||
assertionConsumerService.setIndex(1); | ||
return assertionConsumerService; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private <T> T buildObject(XMLObjectBuilderFactory builderFactory, QName elementName) { | ||
XMLObjectBuilder<?> builder = builderFactory.getBuilder(elementName); | ||
if (builder == null) { | ||
throw new Saml2Exception("Cannot build object - builder not defined for element " + elementName); | ||
} | ||
return (T) builder.buildObject(elementName); | ||
} | ||
|
||
private String resolveTemplate(String template, RelyingPartyRegistration registration, HttpServletRequest request) { | ||
return Saml2ServletUtils.resolveUrlTemplate(template, Saml2ServletUtils.getApplicationUri(request), registration); | ||
} | ||
|
||
} |
89 changes: 89 additions & 0 deletions
89
...ork/security/saml2/provider/service/servlet/filter/SamlServiceProviderMetadataFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
* 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.provider.service.servlet.filter; | ||
|
||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; | ||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; | ||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | ||
import org.springframework.security.web.util.matcher.RequestMatcher; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import javax.servlet.FilterChain; | ||
import javax.servlet.ServletException; | ||
import javax.servlet.ServletOutputStream; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
import java.io.IOException; | ||
|
||
/** | ||
* This {@code Servlet} returns a generated Service Provider Metadata XML | ||
* | ||
* @since 5.4 | ||
* @author Jakub Kubrynski | ||
*/ | ||
public class SamlServiceProviderMetadataFilter extends OncePerRequestFilter { | ||
|
||
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; | ||
private final SamlMetadataGenerator samlMetadataGenerator; | ||
|
||
private RequestMatcher redirectMatcher = new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"); | ||
|
||
public SamlServiceProviderMetadataFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { | ||
this(relyingPartyRegistrationRepository, new SamlMetadataGenerator()); | ||
} | ||
|
||
SamlServiceProviderMetadataFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository, SamlMetadataGenerator samlMetadataGenerator) { | ||
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository; | ||
this.samlMetadataGenerator = samlMetadataGenerator; | ||
} | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
|
||
RequestMatcher.MatchResult matcher = this.redirectMatcher.matcher(request); | ||
if (!matcher.isMatch()) { | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
String registrationId = matcher.getVariables().get("registrationId"); | ||
|
||
RelyingPartyRegistration registration = relyingPartyRegistrationRepository.findByRegistrationId(registrationId); | ||
|
||
if (registration == null) { | ||
response.setStatus(404); | ||
return; | ||
} | ||
|
||
String xml = samlMetadataGenerator.generateMetadata(registration, request); | ||
|
||
writeMetadataToResponse(response, registrationId, xml); | ||
} | ||
|
||
private void writeMetadataToResponse(HttpServletResponse response, String registrationId, String xml) throws IOException { | ||
response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"saml-" + registrationId + "-metadata.xml\""); | ||
response.setContentLength(xml.length()); | ||
ServletOutputStream outputStream = response.getOutputStream(); | ||
outputStream.print(xml); | ||
outputStream.flush(); | ||
outputStream.close(); | ||
} | ||
|
||
} |
64 changes: 64 additions & 0 deletions
64
...ngframework/security/saml2/provider/service/servlet/filter/SamlMetadataGeneratorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* 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.provider.service.servlet.filter; | ||
|
||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.springframework.mock.web.MockHttpServletRequest; | ||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory; | ||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; | ||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.REDIRECT; | ||
|
||
public class SamlMetadataGeneratorTest { | ||
|
||
@Before | ||
public void setUp() { | ||
new OpenSamlAuthenticationRequestFactory(); // ensure OpenSaml is bootstraped | ||
} | ||
|
||
@Test | ||
public void shouldGenerateMetadata() { | ||
// given | ||
SamlMetadataGenerator samlMetadataGenerator = new SamlMetadataGenerator(); | ||
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.relyingPartyRegistration() | ||
.providerDetails(p -> p.binding(REDIRECT)) | ||
.providerDetails(p -> p.signAuthNRequest(true)) | ||
.build(); | ||
HttpServletRequest servletRequestMock = new MockHttpServletRequest(); | ||
|
||
// when | ||
String metadataXml = samlMetadataGenerator.generateMetadata(relyingPartyRegistration, servletRequestMock); | ||
|
||
// then | ||
assertThat(metadataXml) | ||
.contains("<EntityDescriptor") | ||
.contains("entityID=\"http://localhost/saml2/service-provider-metadata/simplesamlphp\"") | ||
.contains("AuthnRequestsSigned=\"true\"") | ||
.contains("WantAssertionsSigned=\"true\"") | ||
.contains("<md:KeyDescriptor use=\"signing\">") | ||
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") | ||
.contains("<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>") | ||
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") | ||
.contains("Location=\"http://localhost/login/saml2/sso/simplesamlphp\" index=\"1\""); | ||
} | ||
|
||
} |
Oops, something went wrong.