diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java index c5cfda1a475..be4e3f6c36a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java @@ -37,6 +37,13 @@ public interface Saml2ErrorCodes { */ String MALFORMED_RESPONSE_DATA = "malformed_response_data"; + /** + * Request is invalid in a general way. + * + * @since 5.5 + */ + String INVALID_REQUEST = "invalid_request"; + /** * Response is invalid in a general way. * diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java index 46bc579bbf2..5ae5cae2bc0 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java @@ -460,7 +460,7 @@ public static Converter createDefaultRespons String username = assertion.getSubject().getNameID().getValue(); Map> attributes = getAssertionAttributes(assertion); return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal(username, attributes), - token.getSaml2Response(), Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))); + token.getSaml2Response(), Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), responseToken.token.getRelyingPartyRegistration()); }; } @@ -709,7 +709,8 @@ private Converter createCompatibleResponseAu Map> attributes = getAssertionAttributes(assertion); return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal(username, attributes), token.getSaml2Response(), - this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))); + this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion)), + responseToken.token.getRelyingPartyRegistration()); }; } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java index d37792456bb..a550bed9c1f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java @@ -22,6 +22,7 @@ import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.util.Assert; /** @@ -41,14 +42,38 @@ public class Saml2Authentication extends AbstractAuthenticationToken { private final String saml2Response; + private final RelyingPartyRegistration registration; + + /** + * Construct a {@link Saml2Authentication} using the provided parameters + * @param principal the logged in user + * @param saml2Response the SAML 2.0 response used to authenticate the user + * @param authorities the authorities for the logged in user + * @deprecated Use {@link Saml2Authentication(AuthenticatedPrincipal, String, + * Collection, RelyingPartyRegistration) instead} + */ + @Deprecated public Saml2Authentication(AuthenticatedPrincipal principal, String saml2Response, Collection authorities) { + this(principal, saml2Response, authorities, null); + } + + /** + * Construct a {@link Saml2Authentication} using the provided parameters + * @param principal the logged in user + * @param saml2Response the SAML 2.0 response used to authenticate the user + * @param authorities the authorities for the logged in user + * @param registration the {@link RelyingPartyRegistration} associated with this user + */ + public Saml2Authentication(AuthenticatedPrincipal principal, String saml2Response, + Collection authorities, RelyingPartyRegistration registration) { super(authorities); Assert.notNull(principal, "principal cannot be null"); Assert.hasText(saml2Response, "saml2Response cannot be null"); this.principal = principal; this.saml2Response = saml2Response; setAuthenticated(true); + this.registration = registration; } @Override @@ -69,4 +94,8 @@ public Object getCredentials() { return getSaml2Response(); } + public RelyingPartyRegistration getRegistration() { + return this.registration; + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java new file mode 100644 index 00000000000..ce5ef1ea12a --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java @@ -0,0 +1,68 @@ +/* + * 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.authentication.logout; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class Saml2LogoutRequest { + + private final Map parameters; + + private Saml2LogoutRequest(Map parameters) { + this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters)); + } + + public String getSamlRequest() { + return this.parameters.get("SAMLRequest"); + } + + public String getParameter(String name) { + return this.parameters.get(name); + } + + public Map getParameters() { + return this.parameters; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private Map parameters = new HashMap<>(); + + public Builder samlRequest(String samlRequest) { + this.parameters.put("SAMLRequest", samlRequest); + return this; + } + + public Builder parameters(Consumer> parametersConsumer) { + parametersConsumer.accept(this.parameters); + return this; + } + + public Saml2LogoutRequest build() { + return new Saml2LogoutRequest(this.parameters); + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java new file mode 100644 index 00000000000..d1cd6037e31 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java @@ -0,0 +1,68 @@ +/* + * 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.authentication.logout; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class Saml2LogoutResponse { + + private final Map parameters; + + private Saml2LogoutResponse(Map parameters) { + this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters)); + } + + public String getSamlResponse() { + return this.parameters.get("SAMLResponse"); + } + + public String getParameter(String name) { + return this.parameters.get(name); + } + + public Map getParameters() { + return this.parameters; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private Map parameters = new HashMap<>(); + + public Builder samlRequest(String samlRequest) { + this.parameters.put("SAMLRequest", samlRequest); + return this; + } + + public Builder parameters(Consumer> parametersConsumer) { + parametersConsumer.accept(this.parameters); + return this; + } + + public Saml2LogoutResponse build() { + return new Saml2LogoutResponse(this.parameters); + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java index c7f04d90f4a..21e9f3b0840 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java @@ -34,6 +34,7 @@ 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; @@ -105,6 +106,10 @@ RelyingPartyRegistration.Builder convert(InputStream inputStream) { 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())) { @@ -119,10 +124,26 @@ else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.ge builder.assertingPartyDetails( (party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation()) .singleSignOnServiceBinding(binding)); - return builder; + 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; + } + builder.assertingPartyDetails( + (party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation()) + .singleLogoutServiceResponseLocation(singleLogoutService.getResponseLocation()) + .singleLogoutServiceBinding(binding)); + break; } - throw new Saml2Exception( - "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); + return builder; } private List certificates(KeyDescriptor keyDescriptor) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index 3af9371561b..290bb6b6bc8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -81,6 +81,12 @@ public final class RelyingPartyRegistration { private final Saml2MessageBinding assertionConsumerServiceBinding; + private final String singleLogoutServiceLocation; + + private final String singleLogoutServiceResponseLocation; + + private final Saml2MessageBinding singleLogoutServiceBinding; + private final ProviderDetails providerDetails; private final List credentials; @@ -90,7 +96,9 @@ public final class RelyingPartyRegistration { private final Collection signingX509Credentials; private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, - Saml2MessageBinding assertionConsumerServiceBinding, ProviderDetails providerDetails, + Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation, + String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding, + ProviderDetails providerDetails, Collection credentials, Collection decryptionX509Credentials, Collection signingX509Credentials) { @@ -118,6 +126,9 @@ private RelyingPartyRegistration(String registrationId, String entityId, String this.entityId = entityId; this.assertionConsumerServiceLocation = assertionConsumerServiceLocation; this.assertionConsumerServiceBinding = assertionConsumerServiceBinding; + this.singleLogoutServiceLocation = singleLogoutServiceLocation; + this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; + this.singleLogoutServiceBinding = singleLogoutServiceBinding; this.providerDetails = providerDetails; this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials)); this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials)); @@ -177,6 +188,18 @@ public Saml2MessageBinding getAssertionConsumerServiceBinding() { return this.assertionConsumerServiceBinding; } + public Saml2MessageBinding getSingleLogoutServiceBinding() { + return this.singleLogoutServiceBinding; + } + + public String getSingleLogoutServiceLocation() { + return this.singleLogoutServiceLocation; + } + + public String getSingleLogoutServiceResponseLocation() { + return this.singleLogoutServiceResponseLocation; + } + /** * Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated * with this relying party @@ -364,6 +387,9 @@ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration regi .decryptionX509Credentials((c) -> c.addAll(registration.getDecryptionX509Credentials())) .assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation()) .assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding()) + .singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation()) + .singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation()) + .singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding()) .assertingPartyDetails((assertingParty) -> assertingParty .entityId(registration.getAssertingPartyDetails().getEntityId()) .wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) @@ -376,7 +402,13 @@ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration regi .singleSignOnServiceLocation( registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()) .singleSignOnServiceBinding( - registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())); + registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + .singleLogoutServiceLocation( + registration.getAssertingPartyDetails().getSingleLogoutServiceLocation()) + .singleLogoutServiceResponseLocation( + registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation()) + .singleLogoutServiceBinding( + registration.getAssertingPartyDetails().getSingleLogoutServiceBinding())); } private static Saml2X509Credential fromDeprecated( @@ -445,10 +477,17 @@ public static final class AssertingPartyDetails { private final Saml2MessageBinding singleSignOnServiceBinding; + private final String singleLogoutServiceLocation; + + private final String singleLogoutServiceResponseLocation; + + private final Saml2MessageBinding singleLogoutServiceBinding; + private AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, List signingAlgorithms, Collection verificationX509Credentials, Collection encryptionX509Credentials, String singleSignOnServiceLocation, - Saml2MessageBinding singleSignOnServiceBinding) { + Saml2MessageBinding singleSignOnServiceBinding, String singleLogoutServiceLocation, + String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding) { Assert.hasText(entityId, "entityId cannot be null or empty"); Assert.notEmpty(signingAlgorithms, "signingAlgorithms cannot be empty"); Assert.notNull(verificationX509Credentials, "verificationX509Credentials cannot be null"); @@ -472,6 +511,9 @@ private AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, this.encryptionX509Credentials = encryptionX509Credentials; this.singleSignOnServiceLocation = singleSignOnServiceLocation; this.singleSignOnServiceBinding = singleSignOnServiceBinding; + this.singleLogoutServiceLocation = singleLogoutServiceLocation; + this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; + this.singleLogoutServiceBinding = singleLogoutServiceBinding; } /** @@ -565,6 +607,48 @@ public Saml2MessageBinding getSingleSignOnServiceBinding() { return this.singleSignOnServiceBinding; } + /** + * Get the SingleLogoutService + * Location. + * + *

+ * Equivalent to the value found in <SingleLogoutService Location="..."/> in + * the asserting party's <IDPSSODescriptor>. + * @return the SingleLogoutService Location + */ + public String getSingleLogoutServiceLocation() { + return this.singleLogoutServiceLocation; + } + + /** + * Get the SingleLogoutService + * ResponseLocation. + * + *

+ * Equivalent to the value found in <SingleLogoutService Location="..."/> in + * the asserting party's <IDPSSODescriptor>. + * @return the SingleLogoutService Response Location + */ + public String getSingleLogoutServiceResponseLocation() { + return this.singleLogoutServiceResponseLocation; + } + + /** + * Get the SingleLogoutService + * Binding. + * + *

+ * Equivalent to the value found in <SingleLogoutService Binding="..."/> in + * the asserting party's <IDPSSODescriptor>. + * @return the SingleLogoutService Binding + */ + public Saml2MessageBinding getSingleLogoutServiceBinding() { + return this.singleLogoutServiceBinding; + } + public static final class Builder { private String entityId; @@ -581,6 +665,12 @@ public static final class Builder { private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT; + private String singleLogoutServiceLocation; + + private String singleLogoutServiceResponseLocation; + + private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.REDIRECT; + /** * Set the asserting party's EntityID. @@ -677,6 +767,21 @@ public Builder singleSignOnServiceBinding(Saml2MessageBinding singleSignOnServic return this; } + public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) { + this.singleLogoutServiceLocation = singleLogoutServiceLocation; + return this; + } + + public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) { + this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; + return this; + } + + public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) { + this.singleLogoutServiceBinding = singleLogoutServiceBinding; + return this; + } + /** * Creates an immutable ProviderDetails object representing the configuration * for an Identity Provider, IDP @@ -689,7 +794,9 @@ public AssertingPartyDetails build() { return new AssertingPartyDetails(this.entityId, this.wantAuthnRequestsSigned, signingAlgorithms, this.verificationX509Credentials, this.encryptionX509Credentials, - this.singleSignOnServiceLocation, this.singleSignOnServiceBinding); + this.singleSignOnServiceLocation, this.singleSignOnServiceBinding, + this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation, + this.singleLogoutServiceBinding); } } @@ -830,6 +937,12 @@ public static final class Builder { private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST; + private String singleLogoutServiceLocation = "{baseUrl}/logout"; + + private String singleLogoutServiceResponseLocation = "{baseUrl}/logout"; + + private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.REDIRECT; + private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder(); private Collection credentials = new HashSet<>(); @@ -932,6 +1045,21 @@ public Builder assertionConsumerServiceBinding(Saml2MessageBinding assertionCons return this; } + public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) { + this.singleLogoutServiceBinding = singleLogoutServiceBinding; + return this; + } + + public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) { + this.singleLogoutServiceLocation = singleLogoutServiceLocation; + return this; + } + + public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) { + this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; + return this; + } + /** * Apply this {@link Consumer} to further configure the Asserting Party details * @param assertingPartyDetails The {@link Consumer} to apply @@ -1075,8 +1203,9 @@ public RelyingPartyRegistration build() { } return new RelyingPartyRegistration(this.registrationId, this.entityId, this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding, - this.providerDetails.build(), this.credentials, this.decryptionX509Credentials, - this.signingX509Credentials); + this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation, + this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials, + this.decryptionX509Credentials, this.signingX509Credentials); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestHandler.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestHandler.java new file mode 100644 index 00000000000..0f2015f0529 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestHandler.java @@ -0,0 +1,263 @@ +/* + * 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.web.authentication.logout; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialResolver; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; +import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.web.util.UriUtils; + +/** + * Someone else initiated logout, and we are participating + */ +public final class OpenSamlLogoutRequestHandler implements LogoutHandler { + + static { + OpenSamlInitializationService.initialize(); + } + + private final ParserPool parserPool; + + private final LogoutRequestUnmarshaller unmarshaller; + + public OpenSamlLogoutRequestHandler() { + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.parserPool = registry.getParserPool(); + this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() + .getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME); + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String serialized = request.getParameter("SAMLRequest"); + if (serialized == null) { + return; + } + byte[] b = Saml2Utils.samlDecode(serialized); + serialized = Saml2Utils.samlInflate(b); + RelyingPartyRegistration registration = null; + if (authentication instanceof Saml2Authentication) { + registration = ((Saml2Authentication) authentication).getRegistration(); + } + if (registration == null) { + throw new Saml2Exception( + "A RelyingPartyRegistration is required in order to validate the LogoutRequest signature, but none was found"); + } + LogoutRequest logoutRequest = parse(serialized); + Saml2ResponseValidatorResult result = verifySignature(request, logoutRequest, registration); + result.concat(validateRequest(logoutRequest, registration, authentication)); + if (result.hasErrors()) { + throw new Saml2Exception("Failed to validate LogoutRequest: " + result.getErrors().iterator().next()); + } + request.setAttribute(Saml2RequestAttributeNames.LOGOUT_REQUEST_ID, logoutRequest.getID()); + } + + private LogoutRequest parse(String request) throws Saml2Exception { + try { + Document document = this.parserPool + .parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8))); + Element element = document.getDocumentElement(); + return (LogoutRequest) this.unmarshaller.unmarshall(element); + } + catch (Exception ex) { + throw new Saml2Exception("Failed to deserialize LogoutRequest", ex); + } + } + + private Saml2ResponseValidatorResult verifySignature(HttpServletRequest request, LogoutRequest logoutRequest, + RelyingPartyRegistration registration) { + if (logoutRequest.isSigned()) { + return verifyPostSignature(logoutRequest, registration); + } + if (request.getParameter("SigAlg") == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, "Failed to derive signature algorithm from request")); + } + if (request.getParameter("Signature") == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, "Failed to derive signature from request")); + } + return verifyRedirectSignature(request, logoutRequest, registration); + } + + private Saml2ResponseValidatorResult verifyRedirectSignature(HttpServletRequest request, LogoutRequest logoutRequest, + RelyingPartyRegistration registration) { + Collection errors = new ArrayList<>(); + String algorithmUri = request.getParameter("SigAlg"); + byte[] signature = Saml2Utils.samlDecode(request.getParameter("Signature")); + String query = "SAMLRequest=" + UriUtils.encode(request.getParameter("SAMLRequest"), StandardCharsets.ISO_8859_1) + "&" + + "SigAlg=" + UriUtils.encode(algorithmUri, StandardCharsets.ISO_8859_1); + byte[] content = query.getBytes(StandardCharsets.UTF_8); + String issuer = logoutRequest.getIssuer().getValue(); + try { + CriteriaSet criteriaSet = new CriteriaSet(); + criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer))); + criteriaSet.add( + new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); + criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + if (!trustEngine(registration).validate(signature, content, algorithmUri, criteriaSet, null)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Request [" + logoutRequest.getID() + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Request [" + logoutRequest.getID() + "]: ")); + } + return Saml2ResponseValidatorResult.failure(errors); + } + + private Saml2ResponseValidatorResult verifyPostSignature(LogoutRequest response, + RelyingPartyRegistration registration) { + Collection errors = new ArrayList<>(); + String issuer = response.getIssuer().getValue(); + if (response.isSigned()) { + SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); + try { + profileValidator.validate(response.getSignature()); + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Request [" + response.getID() + "]: ")); + } + + try { + CriteriaSet criteriaSet = new CriteriaSet(); + criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer))); + criteriaSet.add( + new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); + criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + if (!trustEngine(registration).validate(response.getSignature(), criteriaSet)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Request [" + response.getID() + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Request [" + response.getID() + "]: ")); + } + } + + return Saml2ResponseValidatorResult.failure(errors); + } + + private Saml2ResponseValidatorResult validateRequest(LogoutRequest request, RelyingPartyRegistration registration, + Authentication authentication) { + Saml2ResponseValidatorResult result = Saml2ResponseValidatorResult.success(); + result.concat(validateIssuer(request, registration)); + result.concat(validateDestination(request, registration)); + return result.concat(validateName(request, authentication)); + } + + private Saml2ResponseValidatorResult validateIssuer(LogoutRequest request, RelyingPartyRegistration registration) { + if (request.getIssuer() == null) { + return Saml2ResponseValidatorResult + .failure(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse")); + } + String issuer = request.getIssuer().getValue(); + if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) { + return Saml2ResponseValidatorResult.failure( + new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer")); + } + return Saml2ResponseValidatorResult.success(); + } + + private Saml2ResponseValidatorResult validateDestination(LogoutRequest request, + RelyingPartyRegistration registration) { + if (request.getDestination() == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to find destination in LogoutResponse")); + } + String destination = request.getDestination(); + if (!destination.equals(registration.getSingleLogoutServiceLocation())) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to match destination to configured destination")); + } + return Saml2ResponseValidatorResult.success(); + } + + private Saml2ResponseValidatorResult validateName(LogoutRequest request, Authentication authentication) { + NameID nameId = request.getNameID(); + if (nameId == null) { + return Saml2ResponseValidatorResult.failure( + new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest")); + } + String name = nameId.getValue(); + if (!name.equals(authentication.getName())) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, + "Failed to match subject in LogoutRequest with currently logged in user")); + } + return Saml2ResponseValidatorResult.success(); + } + + private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { + Set credentials = new HashSet<>(); + Collection keys = registration.getAssertingPartyDetails().getVerificationX509Credentials(); + for (Saml2X509Credential key : keys) { + BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(registration.getAssertingPartyDetails().getEntityId()); + credentials.add(cred); + } + CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); + return new ExplicitKeySignatureTrustEngine(credentialsResolver, + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java new file mode 100644 index 00000000000..c88514fac60 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java @@ -0,0 +1,254 @@ +/* + * 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.web.authentication.logout; + +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import javax.servlet.http.HttpServletRequest; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder; +import org.opensaml.saml.saml2.core.impl.LogoutRequestMarshaller; +import org.opensaml.saml.saml2.core.impl.NameIDBuilder; +import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; +import org.opensaml.security.SecurityException; +import org.opensaml.security.credential.BasicCredential; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.SignatureSigningParameters; +import org.opensaml.xmlsec.SignatureSigningParametersResolver; +import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; +import org.opensaml.xmlsec.crypto.XMLSigningUtil; +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.SignatureSupport; +import org.w3c.dom.Element; + +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +/** + * We want to generate a logout request + */ +public final class OpenSamlLogoutRequestResolver implements Saml2LogoutRequestResolver { + + static { + OpenSamlInitializationService.initialize(); + } + + private final LogoutRequestMarshaller marshaller; + + private final LogoutRequestBuilder logoutRequestBuilder; + + private final IssuerBuilder issuerBuilder; + + private final NameIDBuilder nameIdBuilder; + + /** + * Creates an {@link OpenSamlLogoutRequestResolver} + */ + public OpenSamlLogoutRequestResolver() { + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.marshaller = (LogoutRequestMarshaller) registry.getMarshallerFactory() + .getMarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME); + this.logoutRequestBuilder = (LogoutRequestBuilder) registry.getBuilderFactory() + .getBuilder(LogoutRequest.DEFAULT_ELEMENT_NAME); + this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME); + } + + @Override + public OpenSamlLogoutRequestSpec resolveLogoutRequest(HttpServletRequest request, + RelyingPartyRegistration registration, Authentication authentication) { + return new OpenSamlLogoutRequestSpec(registration) + .destination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation()) + .issuer(registration.getEntityId()) + .name(authentication.getName()); + } + + public class OpenSamlLogoutRequestSpec implements Saml2LogoutRequestSpec { + + LogoutRequest logoutRequest; + + RelyingPartyRegistration registration; + + public OpenSamlLogoutRequestSpec(RelyingPartyRegistration registration) { + this.logoutRequest = logoutRequestBuilder.buildObject(); + this.logoutRequest.setID("LR" + UUID.randomUUID()); + this.registration = registration; + } + + @Override + public OpenSamlLogoutRequestSpec destination(String destination) { + this.logoutRequest.setDestination(destination); + return this; + } + + public OpenSamlLogoutRequestSpec issuer(String issuer) { + Issuer iss = issuerBuilder.buildObject(); + iss.setValue(issuer); + this.logoutRequest.setIssuer(iss); + return this; + } + + public OpenSamlLogoutRequestSpec name(String name) { + NameID nameId = nameIdBuilder.buildObject(); + nameId.setValue(name); + this.logoutRequest.setNameID(nameId); + return this; + } + + public OpenSamlLogoutRequestSpec request(Consumer request) { + request.accept(this.logoutRequest); + return this; + } + + @Override + public Saml2LogoutRequest resolve() { + if (this.registration.getAssertingPartyDetails() + .getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) { + String xml = serialize(sign(this.logoutRequest, this.registration)); + return Saml2LogoutRequest.builder() + .samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build(); + } + else { + String xml = serialize(this.logoutRequest); + Saml2LogoutRequest.Builder result = Saml2LogoutRequest.builder(); + String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); + result.samlRequest(deflatedAndEncoded); + Map parameters = new LinkedHashMap<>(); + parameters.put("SAMLRequest", deflatedAndEncoded); + sign(parameters, this.registration); + return result.parameters((params) -> params.putAll(parameters)).build(); + } + } + + } + + private LogoutRequest sign(LogoutRequest logoutRequest, RelyingPartyRegistration relyingPartyRegistration) { + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); + return sign(logoutRequest, parameters); + } + + private LogoutRequest sign(LogoutRequest logoutRequest, SignatureSigningParameters parameters) { + try { + SignatureSupport.signObject(logoutRequest, parameters); + return logoutRequest; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private void sign(Map components, RelyingPartyRegistration relyingPartyRegistration) { + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); + sign(components, parameters); + } + + private void sign(Map components, SignatureSigningParameters parameters) { + Credential credential = parameters.getSigningCredential(); + String algorithmUri = parameters.getSignatureAlgorithm(); + components.put("SigAlg", algorithmUri); + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry component : components.entrySet()) { + builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); + } + String queryString = builder.build(true).toString().substring(1); + try { + byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, + queryString.getBytes(StandardCharsets.UTF_8)); + String b64Signature = Saml2Utils.samlEncode(rawSignature); + components.put("Signature", b64Signature); + } + catch (SecurityException ex) { + throw new Saml2Exception(ex); + } + } + + private String serialize(LogoutRequest logoutRequest) { + try { + Element element = this.marshaller.marshall(logoutRequest); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + + private SignatureSigningParameters resolveSigningParameters(RelyingPartyRegistration relyingPartyRegistration) { + List credentials = resolveSigningCredentials(relyingPartyRegistration); + List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); + String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; + SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); + CriteriaSet criteria = new CriteriaSet(); + BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); + signingConfiguration.setSigningCredentials(credentials); + signingConfiguration.setSignatureAlgorithms(algorithms); + signingConfiguration.setSignatureReferenceDigestMethods(digests); + signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); + criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); + try { + SignatureSigningParameters parameters = resolver.resolveSingle(criteria); + Assert.notNull(parameters, "Failed to resolve any signing credential"); + return parameters; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private List resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { + List credentials = new ArrayList<>(); + for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { + X509Certificate certificate = x509Credential.getCertificate(); + PrivateKey privateKey = x509Credential.getPrivateKey(); + BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); + credential.setEntityId(relyingPartyRegistration.getEntityId()); + credential.setUsageType(UsageType.SIGNING); + credentials.add(credential); + } + return credentials; + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseHandler.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseHandler.java new file mode 100644 index 00000000000..c6dc1f96e11 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseHandler.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-2021 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.web.authentication.logout; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; +import org.opensaml.saml.saml2.core.LogoutResponse; +import org.opensaml.saml.saml2.core.StatusCode; +import org.opensaml.saml.saml2.core.impl.LogoutResponseUnmarshaller; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialResolver; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; +import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.web.util.UriUtils; + +/** + * We initiated logout, and now its complete + */ +public final class OpenSamlLogoutResponseHandler implements LogoutHandler { + + static { + OpenSamlInitializationService.initialize(); + } + + private final ParserPool parserPool; + + private final LogoutResponseUnmarshaller unmarshaller; + + public OpenSamlLogoutResponseHandler() { + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.parserPool = registry.getParserPool(); + this.unmarshaller = (LogoutResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() + .getUnmarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME); + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String serialized = request.getParameter("SAMLResponse"); + if (serialized == null) { + return; + } + byte[] b = Saml2Utils.samlDecode(serialized); + serialized = Saml2Utils.samlInflate(b); + RelyingPartyRegistration registration = null; + if (authentication instanceof Saml2Authentication) { + registration = ((Saml2Authentication) authentication).getRegistration(); + } + if (registration == null) { + throw new Saml2Exception( + "A RelyingPartyRegistration is required in order to validate the LogoutResponse signature, but none was found"); + } + LogoutResponse logoutResponse = parse(serialized); + Saml2ResponseValidatorResult result = verifySignature(request, logoutResponse, registration); + result.concat(validateRequest(logoutResponse, registration, authentication)); + if (result.hasErrors()) { + throw new Saml2Exception("Failed to validate LogoutResponse: " + result.getErrors().iterator().next()); + } + } + + private LogoutResponse parse(String response) throws Saml2Exception { + try { + Document document = this.parserPool + .parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8))); + Element element = document.getDocumentElement(); + return (LogoutResponse) this.unmarshaller.unmarshall(element); + } + catch (Exception ex) { + throw new Saml2Exception("Failed to deserialize LogoutResponse", ex); + } + } + + private Saml2ResponseValidatorResult verifySignature(HttpServletRequest request, LogoutResponse response, + RelyingPartyRegistration registration) { + if (response.isSigned()) { + return verifyPostSignature(response, registration); + } + if (request.getParameter("SigAlg") == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, "Failed to derive signature algorithm from request")); + } + if (request.getParameter("Signature") == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, "Failed to derive signature from request")); + } + return verifyRedirectSignature(request, response, registration); + } + + private Saml2ResponseValidatorResult verifyRedirectSignature(HttpServletRequest request, LogoutResponse logoutResponse, + RelyingPartyRegistration registration) { + Collection errors = new ArrayList<>(); + String algorithmUri = request.getParameter("SigAlg"); + byte[] signature = Saml2Utils.samlDecode(request.getParameter("Signature")); + String query = "SAMLResponse=" + UriUtils.encode(request.getParameter("SAMLResponse"), StandardCharsets.ISO_8859_1) + "&" + + "SigAlg=" + UriUtils.encode(algorithmUri, StandardCharsets.ISO_8859_1); + byte[] content = query.getBytes(StandardCharsets.UTF_8); + String issuer = logoutResponse.getIssuer().getValue(); + try { + CriteriaSet criteriaSet = new CriteriaSet(); + criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer))); + criteriaSet.add( + new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); + criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + if (!trustEngine(registration).validate(signature, content, algorithmUri, criteriaSet, null)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + logoutResponse.getID() + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + logoutResponse.getID() + "]: ")); + } + return Saml2ResponseValidatorResult.failure(errors); + } + + private Saml2ResponseValidatorResult verifyPostSignature(LogoutResponse response, + RelyingPartyRegistration registration) { + Collection errors = new ArrayList<>(); + String issuer = response.getIssuer().getValue(); + if (response.isSigned()) { + SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); + try { + profileValidator.validate(response.getSignature()); + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]: ")); + } + + try { + CriteriaSet criteriaSet = new CriteriaSet(); + criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer))); + criteriaSet.add( + new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); + criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + if (!trustEngine(registration).validate(response.getSignature(), criteriaSet)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]: ")); + } + } + + return Saml2ResponseValidatorResult.failure(errors); + } + + private Saml2ResponseValidatorResult validateRequest(LogoutResponse response, RelyingPartyRegistration registration, + Authentication authentication) { + Saml2ResponseValidatorResult result = Saml2ResponseValidatorResult.success(); + result.concat(validateIssuer(response, registration)); + result.concat(validateDestination(response, registration)); + return result.concat(validateStatus(response)); + } + + private Saml2ResponseValidatorResult validateIssuer(LogoutResponse response, + RelyingPartyRegistration registration) { + if (response.getIssuer() == null) { + return Saml2ResponseValidatorResult + .failure(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse")); + } + String issuer = response.getIssuer().getValue(); + if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) { + return Saml2ResponseValidatorResult.failure( + new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer")); + } + return Saml2ResponseValidatorResult.success(); + } + + private Saml2ResponseValidatorResult validateDestination(LogoutResponse response, + RelyingPartyRegistration registration) { + if (response.getDestination() == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to find destination in LogoutResponse")); + } + String destination = response.getDestination(); + if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to match destination to configured destination")); + } + return Saml2ResponseValidatorResult.success(); + } + + private Saml2ResponseValidatorResult validateStatus(LogoutResponse response) { + if (response.getStatus() == null) { + return Saml2ResponseValidatorResult.success(); + } + if (response.getStatus().getStatusCode() == null) { + return Saml2ResponseValidatorResult.success(); + } + if (!StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) { + return Saml2ResponseValidatorResult + .failure(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed")); + } + return Saml2ResponseValidatorResult.success(); + } + + private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { + Set credentials = new HashSet<>(); + Collection keys = registration.getAssertingPartyDetails().getVerificationX509Credentials(); + for (Saml2X509Credential key : keys) { + BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(registration.getAssertingPartyDetails().getEntityId()); + credentials.add(cred); + } + CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); + return new ExplicitKeySignatureTrustEngine(credentialsResolver, + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java new file mode 100644 index 00000000000..de4fabf09ed --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java @@ -0,0 +1,268 @@ +/* + * 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.web.authentication.logout; + +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import javax.servlet.http.HttpServletRequest; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.LogoutResponse; +import org.opensaml.saml.saml2.core.Status; +import org.opensaml.saml.saml2.core.StatusCode; +import org.opensaml.saml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml.saml2.core.impl.LogoutResponseBuilder; +import org.opensaml.saml.saml2.core.impl.LogoutResponseMarshaller; +import org.opensaml.saml.saml2.core.impl.StatusBuilder; +import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder; +import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; +import org.opensaml.security.SecurityException; +import org.opensaml.security.credential.BasicCredential; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.SignatureSigningParameters; +import org.opensaml.xmlsec.SignatureSigningParametersResolver; +import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; +import org.opensaml.xmlsec.crypto.XMLSigningUtil; +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.SignatureSupport; +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; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +/** + * We want to generate a logout response + */ +public final class OpenSamlLogoutResponseResolver implements Saml2LogoutResponseResolver { + + static { + OpenSamlInitializationService.initialize(); + } + + private final LogoutResponseMarshaller marshaller; + + private final LogoutResponseBuilder logoutResponseBuilder; + + private final IssuerBuilder issuerBuilder; + + private final StatusBuilder statusBuilder; + + private final StatusCodeBuilder statusCodeBuilder; + + /** + * Creates an {@link OpenSamlLogoutResponseResolver} + */ + public OpenSamlLogoutResponseResolver() { + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.marshaller = (LogoutResponseMarshaller) registry.getMarshallerFactory() + .getMarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME); + this.logoutResponseBuilder = (LogoutResponseBuilder) registry.getBuilderFactory() + .getBuilder(LogoutResponse.DEFAULT_ELEMENT_NAME); + this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + this.statusBuilder = (StatusBuilder) registry.getBuilderFactory().getBuilder(Status.DEFAULT_ELEMENT_NAME); + this.statusCodeBuilder = (StatusCodeBuilder) registry.getBuilderFactory() + .getBuilder(StatusCode.DEFAULT_ELEMENT_NAME); + } + + @Override + public OpenSamlLogoutResponseSpec resolveLogoutResponse(HttpServletRequest request, + RelyingPartyRegistration registration) { + return new OpenSamlLogoutResponseSpec(registration) + .destination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation()) + .issuer(registration.getEntityId()).status(StatusCode.SUCCESS); + } + + public class OpenSamlLogoutResponseSpec implements Saml2LogoutResponseSpec { + + LogoutResponse logoutResponse; + + RelyingPartyRegistration registration; + + public OpenSamlLogoutResponseSpec(RelyingPartyRegistration registration) { + this.logoutResponse = logoutResponseBuilder.buildObject(); + this.logoutResponse.setID("LR" + UUID.randomUUID()); + this.registration = registration; + } + + @Override + public OpenSamlLogoutResponseSpec destination(String destination) { + this.logoutResponse.setDestination(destination); + return this; + } + + @Override + public OpenSamlLogoutResponseSpec issuer(String issuer) { + Issuer iss = issuerBuilder.buildObject(); + iss.setValue(issuer); + this.logoutResponse.setIssuer(iss); + return this; + } + + @Override + public OpenSamlLogoutResponseSpec inResponseTo(String inResponseTo) { + this.logoutResponse.setInResponseTo(inResponseTo); + return this; + } + + @Override + public OpenSamlLogoutResponseSpec status(String status) { + StatusCode code = statusCodeBuilder.buildObject(); + code.setValue(status); + Status s = statusBuilder.buildObject(); + s.setStatusCode(code); + this.logoutResponse.setStatus(s); + return this; + } + + public OpenSamlLogoutResponseSpec response(Consumer response) { + response.accept(this.logoutResponse); + return this; + } + + @Override + public Saml2LogoutResponse resolve() { + if (this.registration.getAssertingPartyDetails() + .getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) { + String xml = serialize(sign(this.logoutResponse, this.registration)); + return Saml2LogoutResponse.builder() + .samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build(); + } + else { + String xml = serialize(this.logoutResponse); + Saml2LogoutResponse.Builder result = Saml2LogoutResponse.builder(); + String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); + result.samlRequest(deflatedAndEncoded); + Map parameters = new LinkedHashMap<>(); + parameters.put("SAMLResponse", deflatedAndEncoded); + sign(parameters, this.registration); + return result.parameters((params) -> params.putAll(parameters)).build(); + } + } + + } + + private LogoutResponse sign(LogoutResponse logoutResponse, RelyingPartyRegistration relyingPartyRegistration) { + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); + return sign(logoutResponse, parameters); + } + + private LogoutResponse sign(LogoutResponse logoutResponse, SignatureSigningParameters parameters) { + try { + SignatureSupport.signObject(logoutResponse, parameters); + return logoutResponse; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private void sign(Map components, RelyingPartyRegistration relyingPartyRegistration) { + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); + sign(components, parameters); + } + + private void sign(Map components, SignatureSigningParameters parameters) { + Credential credential = parameters.getSigningCredential(); + String algorithmUri = parameters.getSignatureAlgorithm(); + components.put("SigAlg", algorithmUri); + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry component : components.entrySet()) { + builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); + } + String queryString = builder.build(true).toString().substring(1); + try { + byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, + queryString.getBytes(StandardCharsets.UTF_8)); + String b64Signature = Saml2Utils.samlEncode(rawSignature); + components.put("Signature", b64Signature); + } + catch (SecurityException ex) { + throw new Saml2Exception(ex); + } + } + + private String serialize(LogoutResponse logoutResponse) { + try { + Element element = this.marshaller.marshall(logoutResponse); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + + private SignatureSigningParameters resolveSigningParameters(RelyingPartyRegistration relyingPartyRegistration) { + List credentials = resolveSigningCredentials(relyingPartyRegistration); + List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); + String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; + SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); + CriteriaSet criteria = new CriteriaSet(); + BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); + signingConfiguration.setSigningCredentials(credentials); + signingConfiguration.setSignatureAlgorithms(algorithms); + signingConfiguration.setSignatureReferenceDigestMethods(digests); + signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); + criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); + try { + SignatureSigningParameters parameters = resolver.resolveSingle(criteria); + Assert.notNull(parameters, "Failed to resolve any signing credential"); + return parameters; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private List resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { + List credentials = new ArrayList<>(); + for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { + X509Certificate certificate = x509Credential.getCertificate(); + PrivateKey privateKey = x509Credential.getPrivateKey(); + BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); + credential.setEntityId(relyingPartyRegistration.getEntityId()); + credential.setUsageType(UsageType.SIGNING); + credentials.add(credential); + } + return credentials; + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2AssertingPartyInitiatedLogoutSuccessHandler.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2AssertingPartyInitiatedLogoutSuccessHandler.java new file mode 100644 index 00000000000..fe3f4040122 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2AssertingPartyInitiatedLogoutSuccessHandler.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2021 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.web.authentication.logout; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +public class Saml2AssertingPartyInitiatedLogoutSuccessHandler implements LogoutSuccessHandler { + + private final Saml2LogoutResponseResolver logoutResponseResolver; + + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + public Saml2AssertingPartyInitiatedLogoutSuccessHandler(Saml2LogoutResponseResolver logoutResponseResolver) { + this.logoutResponseResolver = logoutResponseResolver; + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException { + if (!(authentication instanceof Saml2Authentication)) { + return; + } + RelyingPartyRegistration registration = ((Saml2Authentication) authentication).getRegistration(); + if (request.getAttribute(Saml2RequestAttributeNames.LOGOUT_REQUEST_ID) == null) { + return; + } + String logoutRequestId = (String) request.getAttribute(Saml2RequestAttributeNames.LOGOUT_REQUEST_ID); + Saml2LogoutResponse logoutResponse = this.logoutResponseResolver.resolveLogoutResponse(request, registration) + .inResponseTo(logoutRequestId).resolve(); + if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.REDIRECT) { + doRedirect(request, response, registration, logoutResponse); + } + else { + doPost(request, response, registration, logoutResponse); + } + } + + private void doRedirect(HttpServletRequest request, HttpServletResponse response, + RelyingPartyRegistration registration, Saml2LogoutResponse logoutResponse) throws IOException { + String location = registration.getAssertingPartyDetails().getSingleLogoutServiceLocation(); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location); + addParameter("SAMLRequest", logoutResponse, uriBuilder); + addParameter("SigAlg", logoutResponse, uriBuilder); + addParameter("Signature", logoutResponse, uriBuilder); + this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString()); + } + + private void addParameter(String name, Saml2LogoutResponse logoutResponse, UriComponentsBuilder builder) { + Assert.hasText(name, "name cannot be empty or null"); + if (StringUtils.hasText(logoutResponse.getParameter(name))) { + builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1), + UriUtils.encode(logoutResponse.getParameter(name), StandardCharsets.ISO_8859_1)); + } + } + + private void doPost(HttpServletRequest request, HttpServletResponse response, RelyingPartyRegistration registration, + Saml2LogoutResponse logoutResponse) throws IOException { + String html = createSamlPostRequestFormData(logoutResponse, registration); + response.setContentType(MediaType.TEXT_HTML_VALUE); + response.getWriter().write(html); + } + + private String createSamlPostRequestFormData(Saml2LogoutResponse logoutResponse, + RelyingPartyRegistration registration) { + String authenticationRequestUri = registration.getAssertingPartyDetails().getSingleLogoutServiceLocation(); + String samlRequest = logoutResponse.getSamlResponse(); + StringBuilder html = new StringBuilder(); + html.append("\n"); + html.append("\n").append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append("

\n"); + html.append(" Note: Since your browser does not support JavaScript,\n"); + html.append(" you must press the Continue button once to proceed.\n"); + html.append("

\n"); + html.append(" \n"); + html.append(" \n"); + html.append("
\n"); + html.append("
\n"); + html.append(" \n"); + html.append("
\n"); + html.append(" \n"); + html.append("
\n"); + html.append(" \n"); + html.append(" \n"); + html.append(""); + return html.toString(); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.java new file mode 100644 index 00000000000..c6ffd1aac3e --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.java @@ -0,0 +1,43 @@ +/* + * 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.web.authentication.logout; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +// create a SAML 2.0 LogoutRequest +public interface Saml2LogoutRequestResolver { + + Saml2LogoutRequestSpec resolveLogoutRequest(HttpServletRequest request, RelyingPartyRegistration registration, + Authentication authentication); + + interface Saml2LogoutRequestSpec> { + + T issuer(String issuer); + + T destination(String destination); + + T name(String name); + + Saml2LogoutRequest resolve(); + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java new file mode 100644 index 00000000000..dfca918af0b --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java @@ -0,0 +1,43 @@ +/* + * 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.web.authentication.logout; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +// create a SAML 2.0 Logout Response +public interface Saml2LogoutResponseResolver { + + Saml2LogoutResponseSpec resolveLogoutResponse(HttpServletRequest request, RelyingPartyRegistration registration); + + interface Saml2LogoutResponseSpec> { + + T destination(String destination); + + T inResponseTo(String name); + + T issuer(String issuer); + + T status(String status); + + Saml2LogoutResponse resolve(); + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutFilter.java new file mode 100644 index 00000000000..7f35ed63fc7 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutFilter.java @@ -0,0 +1,153 @@ +/* + * 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.web.authentication.logout; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +/** + * We want to initiate SLO by sending a logout request to the asserting party + */ +public final class Saml2RelyingPartyInitiatedLogoutFilter extends OncePerRequestFilter { + + private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/saml2/logout/request"); + + private final Saml2LogoutRequestResolver logoutRequestResolver; + + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + public Saml2RelyingPartyInitiatedLogoutFilter(Saml2LogoutRequestResolver logoutRequestResolver) { + this.logoutRequestResolver = logoutRequestResolver; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + if (!this.logoutRequestMatcher.matches(request)) { + chain.doFilter(request, response); + return; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (!(authentication instanceof Saml2Authentication)) { + chain.doFilter(request, response); + return; + } + RelyingPartyRegistration registration = ((Saml2Authentication) authentication).getRegistration(); + Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); + // redirect to asserting party + if (binding == Saml2MessageBinding.REDIRECT) { + doRedirect(request, response, authentication); + } + else { + doPost(request, response, authentication); + } + + } + + private void doRedirect(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException { + + RelyingPartyRegistration registration = ((Saml2Authentication) authentication).getRegistration(); + Saml2LogoutRequest logoutRequest = logoutRequestResolver + .resolveLogoutRequest(request, registration, authentication).resolve(); + String location = registration.getAssertingPartyDetails().getSingleLogoutServiceLocation(); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location); + addParameter("SAMLRequest", logoutRequest, uriBuilder); + addParameter("SigAlg", logoutRequest, uriBuilder); + addParameter("Signature", logoutRequest, uriBuilder); + this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString()); + } + + private void addParameter(String name, Saml2LogoutRequest logoutRequest, UriComponentsBuilder builder) { + Assert.hasText(name, "name cannot be empty or null"); + if (StringUtils.hasText(logoutRequest.getParameter(name))) { + builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1), + UriUtils.encode(logoutRequest.getParameter(name), StandardCharsets.ISO_8859_1)); + } + } + + private void doPost(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException { + RelyingPartyRegistration registration = ((Saml2Authentication) authentication).getRegistration(); + Saml2LogoutRequest logoutRequest = logoutRequestResolver + .resolveLogoutRequest(request, registration, authentication).resolve(); + String html = createSamlPostRequestFormData(logoutRequest, registration); + response.setContentType(MediaType.TEXT_HTML_VALUE); + response.getWriter().write(html); + } + + private String createSamlPostRequestFormData(Saml2LogoutRequest logoutRequest, + RelyingPartyRegistration registration) { + String authenticationRequestUri = registration.getAssertingPartyDetails().getSingleLogoutServiceLocation(); + String samlRequest = logoutRequest.getSamlRequest(); + StringBuilder html = new StringBuilder(); + html.append("\n"); + html.append("\n").append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append("
\n"); + html.append("
\n"); + html.append(" \n"); + html.append("
\n"); + html.append(" \n"); + html.append("
\n"); + html.append(" \n"); + html.append(" \n"); + html.append(""); + return html.toString(); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RequestAttributeNames.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RequestAttributeNames.java new file mode 100644 index 00000000000..ca29dbdfdf9 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RequestAttributeNames.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2021 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.web.authentication.logout; + +class Saml2RequestAttributeNames { + + static final String LOGOUT_REQUEST_ID = Saml2RequestAttributeNames.class.getName() + "_LOGOUT_REQUEST_ID"; + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java new file mode 100644 index 00000000000..3aa2d575d65 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java @@ -0,0 +1,75 @@ +/* + * 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.web.authentication.logout; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterOutputStream; + +import org.apache.commons.codec.binary.Base64; + +import org.springframework.security.saml2.Saml2Exception; + +/** + * @since 5.3 + */ +final class Saml2Utils { + + private static Base64 BASE64 = new Base64(0, new byte[] { '\n' }); + + private Saml2Utils() { + } + + static String samlEncode(byte[] b) { + return BASE64.encodeAsString(b); + } + + static byte[] samlDecode(String s) { + return BASE64.decode(s); + } + + static byte[] samlDeflate(String s) { + try { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true)); + deflater.write(s.getBytes(StandardCharsets.UTF_8)); + deflater.finish(); + return b.toByteArray(); + } + catch (IOException ex) { + throw new Saml2Exception("Unable to deflate string", ex); + } + } + + static String samlInflate(byte[] b) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true)); + iout.write(b); + iout.finish(); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + catch (IOException ex) { + throw new Saml2Exception("Unable to inflate string", ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 40e1d8bfe14..0c46ad5e632 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -58,6 +58,8 @@ import org.opensaml.saml.saml2.core.EncryptedAttribute; import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.opensaml.saml.saml2.core.LogoutResponse; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.Status; @@ -67,6 +69,10 @@ import org.opensaml.saml.saml2.core.SubjectConfirmationData; import org.opensaml.saml.saml2.core.impl.AttributeBuilder; import org.opensaml.saml.saml2.core.impl.AttributeStatementBuilder; +import org.opensaml.saml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder; +import org.opensaml.saml.saml2.core.impl.LogoutResponseBuilder; +import org.opensaml.saml.saml2.core.impl.NameIDBuilder; import org.opensaml.saml.saml2.core.impl.StatusBuilder; import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder; import org.opensaml.saml.saml2.encryption.Encrypter; @@ -87,6 +93,7 @@ import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; public final class TestOpenSamlObjects { @@ -97,7 +104,7 @@ public final class TestOpenSamlObjects { private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias"; - private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias"; + public static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias"; private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp"; @@ -233,7 +240,7 @@ static T signed(T signable, Saml2X509Credential c return signable; } - static T signed(T signable, Saml2X509Credential credential, String entityId) { + public static T signed(T signable, Saml2X509Credential credential, String entityId) { return signed(signable, credential, entityId, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); } @@ -361,6 +368,41 @@ static Status status(String code) { return status; } + public static LogoutRequest assertingPartyLogoutRequest(RelyingPartyRegistration registration) { + LogoutRequestBuilder logoutRequestBuilder = new LogoutRequestBuilder(); + LogoutRequest logoutRequest = logoutRequestBuilder.buildObject(); + logoutRequest.setID("id"); + NameIDBuilder nameIdBuilder = new NameIDBuilder(); + NameID nameId = nameIdBuilder.buildObject(); + nameId.setValue("user"); + logoutRequest.setNameID(nameId); + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(registration.getAssertingPartyDetails().getEntityId()); + logoutRequest.setIssuer(issuer); + logoutRequest.setDestination(registration.getSingleLogoutServiceLocation()); + return logoutRequest; + } + + public static LogoutResponse assertingPartyLogoutResponse(RelyingPartyRegistration registration) { + LogoutResponseBuilder logoutResponseBuilder = new LogoutResponseBuilder(); + LogoutResponse logoutResponse = logoutResponseBuilder.buildObject(); + logoutResponse.setID("id"); + StatusBuilder statusBuilder = new StatusBuilder(); + StatusCodeBuilder statusCodeBuilder = new StatusCodeBuilder(); + StatusCode code = statusCodeBuilder.buildObject(); + code.setValue(StatusCode.SUCCESS); + Status status = statusBuilder.buildObject(); + status.setStatusCode(code); + logoutResponse.setStatus(status); + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(registration.getAssertingPartyDetails().getEntityId()); + logoutResponse.setIssuer(issuer); + logoutResponse.setDestination(registration.getSingleLogoutServiceLocation()); + return logoutResponse; + } + static T build(QName qName) { return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java index 7d105aecf3a..94986aaaa01 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java @@ -46,6 +46,8 @@ public static RelyingPartyRegistration.Builder relyingPartyRegistration() { public static RelyingPartyRegistration.Builder noCredentials() { return RelyingPartyRegistration.withRegistrationId("registration-id").entityId("rp-entity-id") + .singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request") + .singleLogoutServiceResponseLocation("https://rp.example.org/logout/saml2/response") .assertionConsumerServiceLocation("https://rp.example.org/acs").assertingPartyDetails((party) -> party .entityId("ap-entity-id").singleSignOnServiceLocation("https://ap.example.org/sso")); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestHandlerTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestHandlerTests.java new file mode 100644 index 00000000000..a82df869d81 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestHandlerTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2021 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.web.authentication.logout; + +import java.util.ArrayList; +import java.util.HashMap; + +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.junit.Test; +import org.opensaml.core.xml.XMLObject; +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.saml2.core.LogoutRequest; +import org.w3c.dom.Element; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OpenSamlLogoutRequestHandlerTests { + + @Test + public void handleWhenAuthenticatedThenSavesRequestId() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + TestOpenSamlObjects.signed(logoutRequest, TestSaml2X509Credentials.assertingPartySigningCredential(), + registration.getAssertingPartyDetails().getEntityId()); + Saml2Authentication authentication = new Saml2Authentication( + new DefaultSaml2AuthenticatedPrincipal(logoutRequest.getNameID().getValue(), new HashMap<>()), + "response", new ArrayList<>(), registration); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("SAMLRequest", Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest)))); + OpenSamlLogoutRequestHandler handler = new OpenSamlLogoutRequestHandler(); + handler.logout(request, null, authentication); + String id = (String) request.getAttribute(Saml2RequestAttributeNames.LOGOUT_REQUEST_ID); + assertThat(id).isEqualTo(logoutRequest.getID()); + } + + private String serialize(XMLObject object) { + try { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); + Element element = marshaller.marshall(object); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java new file mode 100644 index 00000000000..da1f0184b5c --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2021 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.web.authentication.logout; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OpenSamlLogoutRequestResolverTests { + + @Test + public void resolveWhenAuthenticatedThenIncludesName() { + OpenSamlLogoutRequestResolver resolver = new OpenSamlLogoutRequestResolver(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + Saml2Authentication authentication = new Saml2Authentication( + new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response", new ArrayList<>(), + registration); + HttpServletRequest request = new MockHttpServletRequest(); + String serialized = resolver.resolveLogoutRequest(request, registration, authentication).resolve() + .getSamlRequest(); + Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); + LogoutRequest logoutRequest = getLogoutRequest(serialized, binding); + assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName()); + } + + private LogoutRequest getLogoutRequest(String samlRequest, Saml2MessageBinding binding) { + if (binding == Saml2MessageBinding.REDIRECT) { + samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest)); + } + else { + samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8); + } + try { + Document document = XMLObjectProviderRegistrySupport.getParserPool() + .parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8))); + Element element = document.getDocumentElement(); + return (LogoutRequest) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element) + .unmarshall(element); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseHandlerTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseHandlerTests.java new file mode 100644 index 00000000000..3b4234cc00d --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseHandlerTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2021 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.web.authentication.logout; + +import java.util.ArrayList; +import java.util.HashMap; + +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.junit.Test; +import org.opensaml.core.xml.XMLObject; +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.saml2.core.LogoutResponse; +import org.w3c.dom.Element; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +public class OpenSamlLogoutResponseHandlerTests { + + @Test + public void handleWhenAuthenticatedThenHandles() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + TestOpenSamlObjects.signed(logoutResponse, TestSaml2X509Credentials.assertingPartySigningCredential(), + registration.getAssertingPartyDetails().getEntityId()); + Saml2Authentication authentication = new Saml2Authentication( + new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response", new ArrayList<>(), + registration); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("SAMLRequest", Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse)))); + OpenSamlLogoutResponseHandler handler = new OpenSamlLogoutResponseHandler(); + handler.logout(request, null, authentication); + } + + private String serialize(XMLObject object) { + try { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); + Element element = marshaller.marshall(object); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java new file mode 100644 index 00000000000..acdf5b9cdd5 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2021 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.web.authentication.logout; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.saml2.core.LogoutResponse; +import org.opensaml.saml.saml2.core.StatusCode; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OpenSamlLogoutResponseResolverTests { + + @Test + public void resolveWhenAuthenticatedThenSuccess() { + OpenSamlLogoutResponseResolver resolver = new OpenSamlLogoutResponseResolver(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + HttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(Saml2RequestAttributeNames.LOGOUT_REQUEST_ID, "logout_request_id"); + String serialized = resolver.resolveLogoutResponse(request, registration).resolve().getSamlResponse(); + Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); + LogoutResponse logoutResponse = getLogoutResponse(serialized, binding); + assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS); + } + + private LogoutResponse getLogoutResponse(String saml2Response, Saml2MessageBinding binding) { + if (binding == Saml2MessageBinding.REDIRECT) { + saml2Response = Saml2Utils.samlInflate(Saml2Utils.samlDecode(saml2Response)); + } + else { + saml2Response = new String(Saml2Utils.samlDecode(saml2Response), StandardCharsets.UTF_8); + } + try { + Document document = XMLObjectProviderRegistrySupport.getParserPool() + .parse(new ByteArrayInputStream(saml2Response.getBytes(StandardCharsets.UTF_8))); + Element element = document.getDocumentElement(); + return (LogoutResponse) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element) + .unmarshall(element); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/samples/boot/saml2login/src/main/java/sample/SecurityConfig.java b/samples/boot/saml2login/src/main/java/sample/SecurityConfig.java index 434cf366681..64efc031848 100644 --- a/samples/boot/saml2login/src/main/java/sample/SecurityConfig.java +++ b/samples/boot/saml2login/src/main/java/sample/SecurityConfig.java @@ -16,12 +16,38 @@ package sample; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.joda.time.DateTime; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestHandler; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseHandler; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2AssertingPartyInitiatedLogoutSuccessHandler; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutFilter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import static org.springframework.security.config.Customizer.withDefaults; @Configuration public class SecurityConfig { @@ -30,7 +56,101 @@ RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations .fromMetadataLocation("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php") .registrationId("one") + .singleLogoutServiceLocation("/saml2/logout/one") + .singleLogoutServiceResponseLocation("/saml2/logout/one") + .signingX509Credentials((signing) -> signing.add(getSigningCredential())) .build(); return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration); } + + @Bean + SecurityFilterChain web(HttpSecurity http, LogoutSuccessHandler successHandler) throws Exception { + LogoutSuccessHandler redirect = new SimpleUrlLogoutSuccessHandler(); + http + .authorizeRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .saml2Login(withDefaults()) + .logout((logout) -> logout + .logoutRequestMatcher(new AntPathRequestMatcher("/saml2/logout/one", "GET")) + .addLogoutHandler(new OpenSamlLogoutRequestHandler()) + .addLogoutHandler(new OpenSamlLogoutResponseHandler()) + .logoutSuccessHandler((request, response, authentication) -> { + successHandler.onLogoutSuccess(request, response, authentication); + redirect.onLogoutSuccess(request, response, authentication); + }) + ) + .addFilterAfter(new Saml2RelyingPartyInitiatedLogoutFilter(requestResolver()), LogoutFilter.class); + + return http.build(); + } + + @Bean + Saml2AssertingPartyInitiatedLogoutSuccessHandler logoutSuccessHandler() { + return new Saml2AssertingPartyInitiatedLogoutSuccessHandler(responseResolver()); + } + + Saml2LogoutRequestResolver requestResolver() { + OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(); + return (request, registration, authentication) -> + delegate.resolveLogoutRequest(request, registration, authentication) + // consider this pattern for a post-processor + .request((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now())); + } + + Saml2LogoutResponseResolver responseResolver() { + OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(); + return (request, registration) -> delegate.resolveLogoutResponse(request, registration) + // consider this pattern for a post-processor + .response((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now())); + } + + private Saml2X509Credential getSigningCredential() { + String key = "-----BEGIN PRIVATE KEY-----\n" + + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" + + "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" + + "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" + + "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" + + "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" + + "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" + + "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" + + "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" + + "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" + + "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" + + "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" + + "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" + + "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" + + "INrtuLp4YHbgk1mi\n" + + "-----END PRIVATE KEY-----"; + String certificate = "-----BEGIN CERTIFICATE-----\n" + + "MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" + + "VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" + + "A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" + + "DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" + + "MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" + + "MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" + + "TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" + + "vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" + + "+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" + + "y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" + + "XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" + + "qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" + + "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + + "-----END CERTIFICATE-----"; + PrivateKey pk = RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(key.getBytes())); + X509Certificate cert = x509Certificate(certificate); + return Saml2X509Credential.signing(pk, cert); + } + + private X509Certificate x509Certificate(String source) { + try { + final CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate( + new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)) + ); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } } diff --git a/samples/boot/saml2login/src/main/resources/templates/index.html b/samples/boot/saml2login/src/main/resources/templates/index.html index b72bb58ce00..2bdf14a64d1 100644 --- a/samples/boot/saml2login/src/main/resources/templates/index.html +++ b/samples/boot/saml2login/src/main/resources/templates/index.html @@ -27,7 +27,7 @@
-
+