7 changed files with 231 additions and 451 deletions
@ -1,228 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2023 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.registration; |
|
||||||
|
|
||||||
import java.io.InputStream; |
|
||||||
import java.security.cert.CertificateException; |
|
||||||
import java.security.cert.X509Certificate; |
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.Arrays; |
|
||||||
import java.util.Collection; |
|
||||||
import java.util.List; |
|
||||||
|
|
||||||
import net.shibboleth.utilities.java.support.xml.ParserPool; |
|
||||||
import org.opensaml.core.config.ConfigurationService; |
|
||||||
import org.opensaml.core.xml.XMLObject; |
|
||||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry; |
|
||||||
import org.opensaml.core.xml.io.Unmarshaller; |
|
||||||
import org.opensaml.saml.common.xml.SAMLConstants; |
|
||||||
import org.opensaml.saml.ext.saml2alg.SigningMethod; |
|
||||||
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; |
|
||||||
import org.opensaml.saml.saml2.metadata.EntityDescriptor; |
|
||||||
import org.opensaml.saml.saml2.metadata.Extensions; |
|
||||||
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; |
|
||||||
import org.opensaml.saml.saml2.metadata.KeyDescriptor; |
|
||||||
import org.opensaml.saml.saml2.metadata.SingleLogoutService; |
|
||||||
import org.opensaml.saml.saml2.metadata.SingleSignOnService; |
|
||||||
import org.opensaml.security.credential.UsageType; |
|
||||||
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; |
|
||||||
import org.w3c.dom.Document; |
|
||||||
import org.w3c.dom.Element; |
|
||||||
|
|
||||||
import org.springframework.security.saml2.Saml2Exception; |
|
||||||
import org.springframework.security.saml2.core.OpenSamlInitializationService; |
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential; |
|
||||||
|
|
||||||
class OpenSamlMetadataRelyingPartyRegistrationConverter { |
|
||||||
|
|
||||||
static { |
|
||||||
OpenSamlInitializationService.initialize(); |
|
||||||
} |
|
||||||
|
|
||||||
private final XMLObjectProviderRegistry registry; |
|
||||||
|
|
||||||
private final ParserPool parserPool; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a {@link OpenSamlMetadataRelyingPartyRegistrationConverter} |
|
||||||
*/ |
|
||||||
OpenSamlMetadataRelyingPartyRegistrationConverter() { |
|
||||||
this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); |
|
||||||
this.parserPool = this.registry.getParserPool(); |
|
||||||
} |
|
||||||
|
|
||||||
OpenSamlRelyingPartyRegistration.Builder convert(EntityDescriptor descriptor) { |
|
||||||
IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); |
|
||||||
if (idpssoDescriptor == null) { |
|
||||||
throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); |
|
||||||
} |
|
||||||
List<Saml2X509Credential> verification = new ArrayList<>(); |
|
||||||
List<Saml2X509Credential> encryption = new ArrayList<>(); |
|
||||||
for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { |
|
||||||
if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { |
|
||||||
List<X509Certificate> certificates = certificates(keyDescriptor); |
|
||||||
for (X509Certificate certificate : certificates) { |
|
||||||
verification.add(Saml2X509Credential.verification(certificate)); |
|
||||||
} |
|
||||||
} |
|
||||||
if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { |
|
||||||
List<X509Certificate> certificates = certificates(keyDescriptor); |
|
||||||
for (X509Certificate certificate : certificates) { |
|
||||||
encryption.add(Saml2X509Credential.encryption(certificate)); |
|
||||||
} |
|
||||||
} |
|
||||||
if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { |
|
||||||
List<X509Certificate> certificates = certificates(keyDescriptor); |
|
||||||
for (X509Certificate certificate : certificates) { |
|
||||||
verification.add(Saml2X509Credential.verification(certificate)); |
|
||||||
encryption.add(Saml2X509Credential.encryption(certificate)); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
if (verification.isEmpty()) { |
|
||||||
throw new Saml2Exception( |
|
||||||
"Metadata response is missing verification certificates, necessary for verifying SAML assertions"); |
|
||||||
} |
|
||||||
OpenSamlRelyingPartyRegistration.Builder builder = OpenSamlRelyingPartyRegistration |
|
||||||
.withAssertingPartyEntityDescriptor(descriptor) |
|
||||||
.assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID()) |
|
||||||
.wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) |
|
||||||
.verificationX509Credentials((c) -> c.addAll(verification)) |
|
||||||
.encryptionX509Credentials((c) -> c.addAll(encryption))); |
|
||||||
|
|
||||||
List<SigningMethod> signingMethods = signingMethods(idpssoDescriptor); |
|
||||||
for (SigningMethod method : signingMethods) { |
|
||||||
builder.assertingPartyDetails( |
|
||||||
(party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm()))); |
|
||||||
} |
|
||||||
if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) { |
|
||||||
throw new Saml2Exception( |
|
||||||
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); |
|
||||||
} |
|
||||||
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) { |
|
||||||
Saml2MessageBinding binding; |
|
||||||
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { |
|
||||||
binding = Saml2MessageBinding.POST; |
|
||||||
} |
|
||||||
else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { |
|
||||||
binding = Saml2MessageBinding.REDIRECT; |
|
||||||
} |
|
||||||
else { |
|
||||||
continue; |
|
||||||
} |
|
||||||
builder |
|
||||||
.assertingPartyDetails((party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation()) |
|
||||||
.singleSignOnServiceBinding(binding)); |
|
||||||
break; |
|
||||||
} |
|
||||||
for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) { |
|
||||||
Saml2MessageBinding binding; |
|
||||||
if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { |
|
||||||
binding = Saml2MessageBinding.POST; |
|
||||||
} |
|
||||||
else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { |
|
||||||
binding = Saml2MessageBinding.REDIRECT; |
|
||||||
} |
|
||||||
else { |
|
||||||
continue; |
|
||||||
} |
|
||||||
String responseLocation = (singleLogoutService.getResponseLocation() == null) |
|
||||||
? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation(); |
|
||||||
builder |
|
||||||
.assertingPartyDetails((party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation()) |
|
||||||
.singleLogoutServiceResponseLocation(responseLocation) |
|
||||||
.singleLogoutServiceBinding(binding)); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
return builder; |
|
||||||
} |
|
||||||
|
|
||||||
Collection<RelyingPartyRegistration.Builder> convert(InputStream inputStream) { |
|
||||||
List<RelyingPartyRegistration.Builder> builders = new ArrayList<>(); |
|
||||||
XMLObject xmlObject = xmlObject(inputStream); |
|
||||||
if (xmlObject instanceof EntitiesDescriptor) { |
|
||||||
EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject; |
|
||||||
for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) { |
|
||||||
if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) { |
|
||||||
builders.add(convert(descriptor)); |
|
||||||
} |
|
||||||
} |
|
||||||
if (builders.isEmpty()) { |
|
||||||
throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements"); |
|
||||||
} |
|
||||||
return builders; |
|
||||||
} |
|
||||||
if (xmlObject instanceof EntityDescriptor) { |
|
||||||
EntityDescriptor descriptor = (EntityDescriptor) xmlObject; |
|
||||||
return Arrays.asList(convert(descriptor)); |
|
||||||
} |
|
||||||
throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass()); |
|
||||||
} |
|
||||||
|
|
||||||
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) { |
|
||||||
try { |
|
||||||
return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); |
|
||||||
} |
|
||||||
catch (CertificateException ex) { |
|
||||||
throw new Saml2Exception(ex); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private List<SigningMethod> signingMethods(IDPSSODescriptor idpssoDescriptor) { |
|
||||||
Extensions extensions = idpssoDescriptor.getExtensions(); |
|
||||||
List<SigningMethod> result = signingMethods(extensions); |
|
||||||
if (!result.isEmpty()) { |
|
||||||
return result; |
|
||||||
} |
|
||||||
EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent(); |
|
||||||
extensions = descriptor.getExtensions(); |
|
||||||
return signingMethods(extensions); |
|
||||||
} |
|
||||||
|
|
||||||
private XMLObject xmlObject(InputStream inputStream) { |
|
||||||
Document document = document(inputStream); |
|
||||||
Element element = document.getDocumentElement(); |
|
||||||
Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); |
|
||||||
if (unmarshaller == null) { |
|
||||||
throw new Saml2Exception("Unsupported element of type " + element.getTagName()); |
|
||||||
} |
|
||||||
try { |
|
||||||
return unmarshaller.unmarshall(element); |
|
||||||
} |
|
||||||
catch (Exception ex) { |
|
||||||
throw new Saml2Exception(ex); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private Document document(InputStream inputStream) { |
|
||||||
try { |
|
||||||
return this.parserPool.parse(inputStream); |
|
||||||
} |
|
||||||
catch (Exception ex) { |
|
||||||
throw new Saml2Exception(ex); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private <T> List<T> signingMethods(Extensions extensions) { |
|
||||||
if (extensions != null) { |
|
||||||
return (List<T>) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME); |
|
||||||
} |
|
||||||
return new ArrayList<>(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,79 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2024 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.registration; |
||||||
|
|
||||||
|
import java.io.InputStream; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Collections; |
||||||
|
|
||||||
|
import org.opensaml.core.xml.XMLObject; |
||||||
|
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; |
||||||
|
import org.opensaml.core.xml.io.Unmarshaller; |
||||||
|
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; |
||||||
|
import org.opensaml.saml.saml2.metadata.EntityDescriptor; |
||||||
|
import org.w3c.dom.Document; |
||||||
|
import org.w3c.dom.Element; |
||||||
|
|
||||||
|
import org.springframework.security.saml2.Saml2Exception; |
||||||
|
import org.springframework.security.saml2.core.OpenSamlInitializationService; |
||||||
|
|
||||||
|
final class OpenSamlMetadataUtils { |
||||||
|
|
||||||
|
static { |
||||||
|
OpenSamlInitializationService.initialize(); |
||||||
|
} |
||||||
|
|
||||||
|
private OpenSamlMetadataUtils() { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static Collection<EntityDescriptor> descriptors(InputStream metadata) { |
||||||
|
XMLObject object = xmlObject(metadata); |
||||||
|
if (object instanceof EntityDescriptor descriptor) { |
||||||
|
return Collections.singleton(descriptor); |
||||||
|
} |
||||||
|
if (object instanceof EntitiesDescriptor descriptors) { |
||||||
|
return descriptors.getEntityDescriptors(); |
||||||
|
} |
||||||
|
throw new Saml2Exception("Unsupported element type: " + object.getClass().getName()); |
||||||
|
} |
||||||
|
|
||||||
|
static XMLObject xmlObject(InputStream inputStream) { |
||||||
|
Document document = document(inputStream); |
||||||
|
Element element = document.getDocumentElement(); |
||||||
|
Unmarshaller unmarshaller = XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element); |
||||||
|
if (unmarshaller == null) { |
||||||
|
throw new Saml2Exception("Unsupported element of type " + element.getTagName()); |
||||||
|
} |
||||||
|
try { |
||||||
|
return unmarshaller.unmarshall(element); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
throw new Saml2Exception(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static Document document(InputStream inputStream) { |
||||||
|
try { |
||||||
|
return XMLObjectProviderRegistrySupport.getParserPool().parse(inputStream); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
throw new Saml2Exception(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -1,208 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2023 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.registration; |
|
||||||
|
|
||||||
import java.io.BufferedReader; |
|
||||||
import java.io.ByteArrayInputStream; |
|
||||||
import java.io.InputStream; |
|
||||||
import java.io.InputStreamReader; |
|
||||||
import java.nio.charset.StandardCharsets; |
|
||||||
import java.security.cert.CertificateFactory; |
|
||||||
import java.security.cert.X509Certificate; |
|
||||||
import java.util.Base64; |
|
||||||
import java.util.stream.Collectors; |
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach; |
|
||||||
import org.junit.jupiter.api.Test; |
|
||||||
import org.opensaml.saml.saml2.metadata.EntityDescriptor; |
|
||||||
import org.opensaml.xmlsec.signature.support.SignatureConstants; |
|
||||||
|
|
||||||
import org.springframework.core.io.ClassPathResource; |
|
||||||
import org.springframework.security.saml2.Saml2Exception; |
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat; |
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
|
||||||
|
|
||||||
public class OpenSamlMetadataRelyingPartyRegistrationConverterTests { |
|
||||||
|
|
||||||
private static final String CERTIFICATE = "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk"; |
|
||||||
|
|
||||||
private static final String ENTITIES_DESCRIPTOR_TEMPLATE = "<md:EntitiesDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\">\n%s</md:EntitiesDescriptor>"; |
|
||||||
|
|
||||||
private static final String ENTITY_DESCRIPTOR_TEMPLATE = "<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" " |
|
||||||
+ "xmlns:alg=\"urn:oasis:names:tc:SAML:metadata:algsupport\" " + "entityID=\"entity-id\" " |
|
||||||
+ "ID=\"_bf133aac099b99b3d81286e1a341f2d34188043a77fe15bf4bf1487dae9b2ea3\">\n%s" |
|
||||||
+ "</md:EntityDescriptor>"; |
|
||||||
|
|
||||||
private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "<md:IDPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n" |
|
||||||
+ "%s\n" + "</md:IDPSSODescriptor>"; |
|
||||||
|
|
||||||
private static final String KEY_DESCRIPTOR_TEMPLATE = "<md:KeyDescriptor %s>\n" |
|
||||||
+ "<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n" + "<ds:X509Data>\n" |
|
||||||
+ "<ds:X509Certificate>" + CERTIFICATE + "</ds:X509Certificate>\n" + "</ds:X509Data>\n" + "</ds:KeyInfo>\n" |
|
||||||
+ "</md:KeyDescriptor>"; |
|
||||||
|
|
||||||
private static final String EXTENSIONS_TEMPLATE = "<md:Extensions>" + "<alg:SigningMethod Algorithm=\"" |
|
||||||
+ SignatureConstants.ALGO_ID_DIGEST_SHA512 + "\"/>" + "</md:Extensions>"; |
|
||||||
|
|
||||||
private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = "<md:SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" " |
|
||||||
+ "Location=\"sso-location\"/>"; |
|
||||||
|
|
||||||
private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); |
|
||||||
|
|
||||||
private String metadata; |
|
||||||
|
|
||||||
@BeforeEach |
|
||||||
public void setup() throws Exception { |
|
||||||
ClassPathResource resource = new ClassPathResource("test-metadata.xml"); |
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { |
|
||||||
this.metadata = reader.lines().collect(Collectors.joining()); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// gh-12667
|
|
||||||
@Test |
|
||||||
public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception { |
|
||||||
try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) { |
|
||||||
this.converter.convert(source) |
|
||||||
.forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails()) |
|
||||||
.isInstanceOf(OpenSamlAssertingPartyDetails.class)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void readWhenMissingIDPSSODescriptorThenException() { |
|
||||||
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, ""); |
|
||||||
InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); |
|
||||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) |
|
||||||
.withMessageContaining("Metadata response is missing the necessary IDPSSODescriptor element"); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void readWhenMissingVerificationKeyThenException() { |
|
||||||
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, "")); |
|
||||||
InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); |
|
||||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) |
|
||||||
.withMessageContaining( |
|
||||||
"Metadata response is missing verification certificates, necessary for verifying SAML assertions"); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void readWhenMissingSingleSignOnServiceThenException() { |
|
||||||
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, |
|
||||||
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\""))); |
|
||||||
InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); |
|
||||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) |
|
||||||
.withMessageContaining( |
|
||||||
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception { |
|
||||||
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, |
|
||||||
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, |
|
||||||
String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"") |
|
||||||
+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE |
|
||||||
+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); |
|
||||||
InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); |
|
||||||
RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) |
|
||||||
.iterator() |
|
||||||
.next() |
|
||||||
.build() |
|
||||||
.getAssertingPartyDetails(); |
|
||||||
assertThat(details.getWantAuthnRequestsSigned()).isFalse(); |
|
||||||
assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512); |
|
||||||
assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); |
|
||||||
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); |
|
||||||
assertThat(details.getEntityId()).isEqualTo("entity-id"); |
|
||||||
assertThat(details.getVerificationX509Credentials()).hasSize(1); |
|
||||||
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) |
|
||||||
.isEqualTo(x509Certificate(CERTIFICATE)); |
|
||||||
assertThat(details.getEncryptionX509Credentials()).hasSize(1); |
|
||||||
assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) |
|
||||||
.isEqualTo(x509Certificate(CERTIFICATE)); |
|
||||||
assertThat(details).isInstanceOf(OpenSamlAssertingPartyDetails.class); |
|
||||||
OpenSamlAssertingPartyDetails openSamlDetails = (OpenSamlAssertingPartyDetails) details; |
|
||||||
EntityDescriptor entityDescriptor = openSamlDetails.getEntityDescriptor(); |
|
||||||
assertThat(entityDescriptor).isNotNull(); |
|
||||||
assertThat(entityDescriptor.getEntityID()).isEqualTo(details.getEntityId()); |
|
||||||
} |
|
||||||
|
|
||||||
// gh-9051
|
|
||||||
@Test |
|
||||||
public void readWhenEntitiesDescriptorThenConfigures() throws Exception { |
|
||||||
String payload = String.format(ENTITIES_DESCRIPTOR_TEMPLATE, |
|
||||||
String.format(ENTITY_DESCRIPTOR_TEMPLATE, |
|
||||||
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, |
|
||||||
String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"") |
|
||||||
+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") |
|
||||||
+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)))); |
|
||||||
InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); |
|
||||||
RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) |
|
||||||
.iterator() |
|
||||||
.next() |
|
||||||
.build() |
|
||||||
.getAssertingPartyDetails(); |
|
||||||
assertThat(details.getWantAuthnRequestsSigned()).isFalse(); |
|
||||||
assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); |
|
||||||
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); |
|
||||||
assertThat(details.getEntityId()).isEqualTo("entity-id"); |
|
||||||
assertThat(details.getVerificationX509Credentials()).hasSize(1); |
|
||||||
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) |
|
||||||
.isEqualTo(x509Certificate(CERTIFICATE)); |
|
||||||
assertThat(details.getEncryptionX509Credentials()).hasSize(1); |
|
||||||
assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) |
|
||||||
.isEqualTo(x509Certificate(CERTIFICATE)); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exception { |
|
||||||
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, |
|
||||||
String.format(KEY_DESCRIPTOR_TEMPLATE, "") + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); |
|
||||||
InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); |
|
||||||
RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) |
|
||||||
.iterator() |
|
||||||
.next() |
|
||||||
.build() |
|
||||||
.getAssertingPartyDetails(); |
|
||||||
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) |
|
||||||
.isEqualTo(x509Certificate(CERTIFICATE)); |
|
||||||
assertThat(details.getEncryptionX509Credentials()).hasSize(1); |
|
||||||
assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) |
|
||||||
.isEqualTo(x509Certificate(CERTIFICATE)); |
|
||||||
} |
|
||||||
|
|
||||||
X509Certificate x509Certificate(String data) { |
|
||||||
try { |
|
||||||
InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes())); |
|
||||||
return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificate); |
|
||||||
} |
|
||||||
catch (Exception ex) { |
|
||||||
throw new IllegalArgumentException(ex); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// gh-9051
|
|
||||||
@Test |
|
||||||
public void readWhenUnsupportedElementThenSaml2Exception() { |
|
||||||
String payload = "<saml2:Assertion xmlns:saml2=\"https://some.endpoint\"/>"; |
|
||||||
InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); |
|
||||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) |
|
||||||
.withMessage("Unsupported element of type saml2:Assertion"); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
Loading…
Reference in new issue