Remove Deprecated OpenSaml Components

Closes gh-17306
This commit is contained in:
Josh Cummings 2025-07-09 14:04:21 -06:00
parent 571b6fe4a8
commit da182a2d7c
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
11 changed files with 0 additions and 2007 deletions

View File

@ -43,7 +43,6 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.OpenSaml4AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.OpenSaml5AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.OpenSamlAuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter;
@ -416,9 +415,6 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
}
AuthenticationConverter authenticationConverterBean = getBeanOrNull(http,
Saml2AuthenticationTokenConverter.class);
if (authenticationConverterBean == null) {
authenticationConverterBean = getBeanOrNull(http, OpenSamlAuthenticationTokenConverter.class);
}
if (authenticationConverterBean != null) {
return authenticationConverterBean;
}

View File

@ -1,164 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.util.Collection;
import java.util.function.Consumer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.springframework.security.core.Authentication;
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.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters;
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
/**
* A {@link Saml2LogoutRequestValidator} that authenticates a SAML 2.0 Logout Requests
* received from a SAML 2.0 Asserting Party using OpenSAML.
*
* @author Josh Cummings
* @since 5.6
* @deprecated Please use the version-specific {@link Saml2LogoutRequestValidator} such as
* {@code OpenSaml4LogoutRequestValidator}
*/
@Deprecated
public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
static {
OpenSamlInitializationService.initialize();
}
private final OpenSamlOperations saml = new OpenSaml4Template();
/**
* Constructs a {@link OpenSamlLogoutRequestValidator}
*/
public OpenSamlLogoutRequestValidator() {
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) {
Saml2LogoutRequest request = parameters.getLogoutRequest();
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
Authentication authentication = parameters.getAuthentication();
LogoutRequest logoutRequest = this.saml.deserialize(Saml2Utils.withEncoded(request.getSamlRequest())
.inflate(request.getBinding() == Saml2MessageBinding.REDIRECT)
.decode());
return Saml2LogoutValidatorResult.withErrors()
.errors(verifySignature(request, logoutRequest, registration))
.errors(validateRequest(logoutRequest, registration, authentication))
.build();
}
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest,
RelyingPartyRegistration registration) {
AssertingPartyMetadata details = registration.getAssertingPartyMetadata();
Collection<Saml2X509Credential> credentials = details.getVerificationX509Credentials();
VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId());
return (errors) -> {
if (logoutRequest.isSigned()) {
errors.addAll(verify.verify(logoutRequest));
}
else {
RedirectParameters params = new RedirectParameters(request.getParameters(),
request.getParametersQuery(), logoutRequest);
errors.addAll(verify.verify(params));
}
};
}
private Consumer<Collection<Saml2Error>> validateRequest(LogoutRequest request,
RelyingPartyRegistration registration, Authentication authentication) {
return (errors) -> {
validateIssuer(request, registration).accept(errors);
validateDestination(request, registration).accept(errors);
validateSubject(request, registration, authentication).accept(errors);
};
}
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutRequest request,
RelyingPartyRegistration registration) {
return (errors) -> {
if (request.getIssuer() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutRequest"));
return;
}
String issuer = request.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
errors
.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
};
}
private Consumer<Collection<Saml2Error>> validateDestination(LogoutRequest request,
RelyingPartyRegistration registration) {
return (errors) -> {
if (request.getDestination() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutRequest"));
return;
}
String destination = request.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceLocation())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
};
}
private Consumer<Collection<Saml2Error>> validateSubject(LogoutRequest request,
RelyingPartyRegistration registration, Authentication authentication) {
return (errors) -> {
if (authentication == null) {
return;
}
NameID nameId = getNameId(request, registration);
if (nameId == null) {
errors
.add(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest"));
return;
}
validateNameId(nameId, authentication, errors);
};
}
private NameID getNameId(LogoutRequest request, RelyingPartyRegistration registration) {
this.saml.withDecryptionKeys(registration.getDecryptionX509Credentials()).decrypt(request);
return request.getNameID();
}
private void validateNameId(NameID nameId, Authentication authentication, Collection<Saml2Error> errors) {
String name = nameId.getValue();
if (!name.equals(authentication.getName())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST,
"Failed to match subject in LogoutRequest with currently logged in user"));
}
}
}

View File

@ -1,176 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.util.Collection;
import java.util.function.Consumer;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.impl.LogoutResponseUnmarshaller;
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.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters;
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
/**
* A {@link Saml2LogoutResponseValidator} that authenticates a SAML 2.0 Logout Responses
* received from a SAML 2.0 Asserting Party using OpenSAML.
*
* @author Josh Cummings
* @since 5.6
* @deprecated Please use the version-specific {@link Saml2LogoutResponseValidator}
* instead such as {@code OpenSaml4LogoutResponseValidator}
*/
@Deprecated
public class OpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
static {
OpenSamlInitializationService.initialize();
}
private final XMLObjectProviderRegistry registry;
private final LogoutResponseUnmarshaller unmarshaller;
private final OpenSamlOperations saml = new OpenSaml4Template();
/**
* Constructs a {@link OpenSamlLogoutRequestValidator}
*/
public OpenSamlLogoutResponseValidator() {
this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.unmarshaller = (LogoutResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) {
Saml2LogoutResponse response = parameters.getLogoutResponse();
Saml2LogoutRequest request = parameters.getLogoutRequest();
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
LogoutResponse logoutResponse = this.saml.deserialize(Saml2Utils.withEncoded(response.getSamlResponse())
.inflate(response.getBinding() == Saml2MessageBinding.REDIRECT)
.decode());
return Saml2LogoutValidatorResult.withErrors()
.errors(verifySignature(response, logoutResponse, registration))
.errors(validateRequest(logoutResponse, registration))
.errors(validateLogoutRequest(logoutResponse, request.getId()))
.build();
}
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutResponse response,
LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
return (errors) -> {
AssertingPartyMetadata details = registration.getAssertingPartyMetadata();
Collection<Saml2X509Credential> credentials = details.getVerificationX509Credentials();
VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId());
if (logoutResponse.isSigned()) {
errors.addAll(verify.verify(logoutResponse));
}
else {
RedirectParameters params = new RedirectParameters(response.getParameters(),
response.getParametersQuery(), logoutResponse);
errors.addAll(verify.verify(params));
}
};
}
private Consumer<Collection<Saml2Error>> validateRequest(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
validateIssuer(response, registration).accept(errors);
validateDestination(response, registration).accept(errors);
validateStatus(response).accept(errors);
};
}
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
if (response.getIssuer() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
return;
}
String issuer = response.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
errors
.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
};
}
private Consumer<Collection<Saml2Error>> validateDestination(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
if (response.getDestination() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutResponse"));
return;
}
String destination = response.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
};
}
private Consumer<Collection<Saml2Error>> validateStatus(LogoutResponse response) {
return (errors) -> {
if (response.getStatus() == null) {
return;
}
if (response.getStatus().getStatusCode() == null) {
return;
}
if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) {
return;
}
if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) {
return;
}
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed"));
};
}
private Consumer<Collection<Saml2Error>> validateLogoutRequest(LogoutResponse response, String id) {
return (errors) -> {
if (response.getInResponseTo() == null) {
return;
}
if (response.getInResponseTo().equals(id)) {
return;
}
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE,
"LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest"));
};
}
}

View File

@ -1,255 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.metadata;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.X509Certificate;
import org.opensaml.xmlsec.signature.X509Data;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.util.Assert;
/**
* Resolves the SAML 2.0 Relying Party Metadata for a given
* {@link RelyingPartyRegistration} using the OpenSAML API.
*
* @author Jakub Kubrynski
* @author Josh Cummings
* @since 5.4
* @deprecated Please use version-specific {@link Saml2MetadataResolver} instead, for
* example {@code OpenSaml4MetadataResolver}
*/
@Deprecated
public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
static {
OpenSamlInitializationService.initialize();
}
private final Log logger = LogFactory.getLog(this.getClass());
private OpenSamlOperations saml = new OpenSaml4Template();
private Consumer<EntityDescriptorParameters> entityDescriptorCustomizer = (parameters) -> {
};
private boolean usePrettyPrint = true;
private boolean signMetadata = false;
public OpenSamlMetadataResolver() {
}
OpenSamlMetadataResolver(OpenSamlOperations saml) {
this.saml = saml;
}
@Override
public String resolve(RelyingPartyRegistration relyingPartyRegistration) {
EntityDescriptor entityDescriptor = entityDescriptor(relyingPartyRegistration);
return serialize(entityDescriptor);
}
public String resolve(Iterable<RelyingPartyRegistration> relyingPartyRegistrations) {
Collection<EntityDescriptor> entityDescriptors = new ArrayList<>();
for (RelyingPartyRegistration registration : relyingPartyRegistrations) {
EntityDescriptor entityDescriptor = entityDescriptor(registration);
entityDescriptors.add(entityDescriptor);
}
if (entityDescriptors.size() == 1) {
return serialize(entityDescriptors.iterator().next());
}
EntitiesDescriptor entities = this.saml.build(EntitiesDescriptor.DEFAULT_ELEMENT_NAME);
entities.getEntityDescriptors().addAll(entityDescriptors);
return serialize(entities);
}
private EntityDescriptor entityDescriptor(RelyingPartyRegistration registration) {
EntityDescriptor entityDescriptor = this.saml.build(EntityDescriptor.DEFAULT_ELEMENT_NAME);
entityDescriptor.setEntityID(registration.getEntityId());
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration);
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
this.entityDescriptorCustomizer.accept(new EntityDescriptorParameters(entityDescriptor, registration));
if (this.signMetadata) {
return this.saml.withSigningKeys(registration.getSigningX509Credentials())
.algorithms(registration.getAssertingPartyMetadata().getSigningAlgorithms())
.sign(entityDescriptor);
}
else {
this.logger.trace("Did not sign metadata since `signMetadata` is `false`");
}
return entityDescriptor;
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link EntityDescriptor}
* @param entityDescriptorCustomizer a consumer that accepts an
* {@link EntityDescriptorParameters}
* @since 5.7
*/
public void setEntityDescriptorCustomizer(Consumer<EntityDescriptorParameters> entityDescriptorCustomizer) {
Assert.notNull(entityDescriptorCustomizer, "entityDescriptorCustomizer cannot be null");
this.entityDescriptorCustomizer = entityDescriptorCustomizer;
}
/**
* Configure whether to pretty-print the metadata XML. This can be helpful when
* signing the metadata payload.
*
* @since 6.2
**/
public void setUsePrettyPrint(boolean usePrettyPrint) {
this.usePrettyPrint = usePrettyPrint;
}
private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) {
SPSSODescriptor spSsoDescriptor = this.saml.build(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
spSsoDescriptor.getKeyDescriptors()
.addAll(buildKeys(registration.getSigningX509Credentials(), UsageType.SIGNING));
spSsoDescriptor.getKeyDescriptors()
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
if (registration.getSingleLogoutServiceLocation() != null) {
for (Saml2MessageBinding binding : registration.getSingleLogoutServiceBindings()) {
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration, binding));
}
}
if (registration.getNameIdFormat() != null) {
spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration));
}
return spSsoDescriptor;
}
private List<KeyDescriptor> buildKeys(Collection<Saml2X509Credential> credentials, UsageType usageType) {
List<KeyDescriptor> list = new ArrayList<>();
for (Saml2X509Credential credential : credentials) {
KeyDescriptor keyDescriptor = buildKeyDescriptor(usageType, credential.getCertificate());
list.add(keyDescriptor);
}
return list;
}
private KeyDescriptor buildKeyDescriptor(UsageType usageType, java.security.cert.X509Certificate certificate) {
KeyDescriptor keyDescriptor = this.saml.build(KeyDescriptor.DEFAULT_ELEMENT_NAME);
KeyInfo keyInfo = this.saml.build(KeyInfo.DEFAULT_ELEMENT_NAME);
X509Certificate x509Certificate = this.saml.build(X509Certificate.DEFAULT_ELEMENT_NAME);
X509Data x509Data = this.saml.build(X509Data.DEFAULT_ELEMENT_NAME);
try {
x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded())));
}
catch (CertificateEncodingException ex) {
throw new Saml2Exception("Cannot encode certificate " + certificate.toString());
}
x509Data.getX509Certificates().add(x509Certificate);
keyInfo.getX509Datas().add(x509Data);
keyDescriptor.setUse(usageType);
keyDescriptor.setKeyInfo(keyInfo);
return keyDescriptor;
}
private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration) {
AssertionConsumerService assertionConsumerService = this.saml
.build(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
assertionConsumerService.setLocation(registration.getAssertionConsumerServiceLocation());
assertionConsumerService.setBinding(registration.getAssertionConsumerServiceBinding().getUrn());
assertionConsumerService.setIndex(1);
return assertionConsumerService;
}
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration,
Saml2MessageBinding binding) {
SingleLogoutService singleLogoutService = this.saml.build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation());
singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
singleLogoutService.setBinding(binding.getUrn());
return singleLogoutService;
}
private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) {
NameIDFormat nameIdFormat = this.saml.build(NameIDFormat.DEFAULT_ELEMENT_NAME);
nameIdFormat.setURI(registration.getNameIdFormat());
return nameIdFormat;
}
private String serialize(EntityDescriptor entityDescriptor) {
return this.saml.serialize(entityDescriptor).prettyPrint(this.usePrettyPrint).serialize();
}
private String serialize(EntitiesDescriptor entities) {
return this.saml.serialize(entities).prettyPrint(this.usePrettyPrint).serialize();
}
/**
* Configure whether to sign the metadata, defaults to {@code false}.
*
* @since 6.4
*/
public void setSignMetadata(boolean signMetadata) {
this.signMetadata = signMetadata;
}
/**
* A tuple containing an OpenSAML {@link EntityDescriptor} and its associated
* {@link RelyingPartyRegistration}
*
* @since 5.7
*/
public static final class EntityDescriptorParameters {
private final EntityDescriptor entityDescriptor;
private final RelyingPartyRegistration registration;
public EntityDescriptorParameters(EntityDescriptor entityDescriptor, RelyingPartyRegistration registration) {
this.entityDescriptor = entityDescriptor;
this.registration = registration;
}
public EntityDescriptor getEntityDescriptor() {
return this.entityDescriptor;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
}
}

View File

@ -1,203 +0,0 @@
/*
* Copyright 2002-2025 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;
import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest;
import org.opensaml.saml.saml2.core.Response;
import org.springframework.http.HttpMethod;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
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.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.pathPattern;
/**
* An {@link AuthenticationConverter} that generates a {@link Saml2AuthenticationToken}
* appropriate for authenticated a SAML 2.0 Assertion against an
* {@link org.springframework.security.authentication.AuthenticationManager}.
*
* @author Josh Cummings
* @since 6.1
* @deprecated Please use a version-specific SAML 2.0 {@link AuthenticationConverter}
* instead such as {@code OpenSaml4AuthenticationTokenConverter}
*/
@Deprecated
public final class OpenSamlAuthenticationTokenConverter implements AuthenticationConverter {
static {
OpenSamlInitializationService.initialize();
}
private final OpenSamlOperations saml = new OpenSaml4Template();
private final RelyingPartyRegistrationRepository registrations;
private RequestMatcher requestMatcher = new OrRequestMatcher(pathPattern("/login/saml2/sso/{registrationId}"),
pathPattern("/login/saml2/sso"));
private Function<HttpServletRequest, AbstractSaml2AuthenticationRequest> loader;
/**
* Constructs a {@link OpenSamlAuthenticationTokenConverter} given a repository for
* {@link RelyingPartyRegistration}s
* @param registrations the repository for {@link RelyingPartyRegistration}s
* {@link RelyingPartyRegistration}s
*/
public OpenSamlAuthenticationTokenConverter(RelyingPartyRegistrationRepository registrations) {
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
this.registrations = registrations;
this.loader = new HttpSessionSaml2AuthenticationRequestRepository()::loadAuthenticationRequest;
}
/**
* Resolve an authentication request from the given {@link HttpServletRequest}.
*
* <p>
* First uses the configured {@link RequestMatcher} to deduce whether an
* authentication request is being made and optionally for which
* {@code registrationId}.
*
* <p>
* If there is an associated {@code <saml2:AuthnRequest>}, then the
* {@code registrationId} is looked up and used.
*
* <p>
* If a {@code registrationId} is found in the request, then it is looked up and used.
* In that case, if none is found a {@link Saml2AuthenticationException} is thrown.
*
* <p>
* Finally, if no {@code registrationId} is found in the request, then the code
* attempts to resolve the {@link RelyingPartyRegistration} from the SAML Response's
* Issuer.
* @param request the HTTP request
* @return the {@link Saml2AuthenticationToken} authentication request
* @throws Saml2AuthenticationException if the {@link RequestMatcher} specifies a
* non-existent {@code registrationId}
*/
@Override
public Saml2AuthenticationToken convert(HttpServletRequest request) {
String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
if (serialized == null) {
return null;
}
RequestMatcher.MatchResult result = this.requestMatcher.matcher(request);
if (!result.isMatch()) {
return null;
}
Saml2AuthenticationToken token = tokenByAuthenticationRequest(request);
if (token == null) {
token = tokenByRegistrationId(request, result);
}
if (token == null) {
token = tokenByEntityId(request);
}
return token;
}
private Saml2AuthenticationToken tokenByAuthenticationRequest(HttpServletRequest request) {
AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request);
if (authenticationRequest == null) {
return null;
}
String registrationId = authenticationRequest.getRelyingPartyRegistrationId();
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
return tokenByRegistration(request, registration, authenticationRequest);
}
private Saml2AuthenticationToken tokenByRegistrationId(HttpServletRequest request,
RequestMatcher.MatchResult result) {
String registrationId = result.getVariables().get("registrationId");
if (registrationId == null) {
return null;
}
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
return tokenByRegistration(request, registration, null);
}
private Saml2AuthenticationToken tokenByEntityId(HttpServletRequest request) {
Response response = this.saml.deserialize(decode(request));
String issuer = response.getIssuer().getValue();
RelyingPartyRegistration registration = this.registrations.findUniqueByAssertingPartyEntityId(issuer);
return tokenByRegistration(request, registration, null);
}
private Saml2AuthenticationToken tokenByRegistration(HttpServletRequest request,
RelyingPartyRegistration registration, AbstractSaml2AuthenticationRequest authenticationRequest) {
if (registration == null) {
return null;
}
String decoded = decode(request);
UriResolver resolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
registration = registration.mutate()
.entityId(resolver.resolve(registration.getEntityId()))
.assertionConsumerServiceLocation(resolver.resolve(registration.getAssertionConsumerServiceLocation()))
.build();
return new Saml2AuthenticationToken(registration, decoded, authenticationRequest);
}
/**
* Use the given {@link Saml2AuthenticationRequestRepository} to load authentication
* request.
* @param authenticationRequestRepository the
* {@link Saml2AuthenticationRequestRepository} to use
*/
public void setAuthenticationRequestRepository(
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository) {
Assert.notNull(authenticationRequestRepository, "authenticationRequestRepository cannot be null");
this.loader = authenticationRequestRepository::loadAuthenticationRequest;
}
/**
* Use the given {@link RequestMatcher} to match the request.
* @param requestMatcher the {@link RequestMatcher} to use
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
private AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
return this.loader.apply(request);
}
private String decode(HttpServletRequest request) {
String encoded = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
try {
return Saml2Utils.withEncoded(encoded)
.requireBase64(true)
.inflate(HttpMethod.GET.matches(request.getMethod()))
.decode();
}
catch (Exception ex) {
throw new Saml2AuthenticationException(Saml2Error.invalidResponse(ex.getMessage()), ex);
}
}
}

View File

@ -1,214 +0,0 @@
/*
* Copyright 2002-2025 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 jakarta.servlet.http.HttpServletRequest;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters;
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.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.pathPattern;
/**
* An OpenSAML-based implementation of
* {@link Saml2LogoutRequestValidatorParametersResolver}
*
* @deprecated Please use a version-specific
* {@link Saml2LogoutRequestValidatorParametersResolver} such as
* {@code OpenSaml4LogoutRequestValidatorParametersResolver}
*/
@Deprecated
public final class OpenSamlLogoutRequestValidatorParametersResolver
implements Saml2LogoutRequestValidatorParametersResolver {
static {
OpenSamlInitializationService.initialize();
}
private RequestMatcher requestMatcher = new OrRequestMatcher(pathPattern("/logout/saml2/slo/{registrationId}"),
pathPattern("/logout/saml2/slo"));
private final OpenSamlOperations saml = new OpenSaml4Template();
private final RelyingPartyRegistrationRepository registrations;
private final XMLObjectProviderRegistry registry;
private final LogoutRequestUnmarshaller unmarshaller;
/**
* Constructs a {@link OpenSamlLogoutRequestValidatorParametersResolver}
*/
public OpenSamlLogoutRequestValidatorParametersResolver(RelyingPartyRegistrationRepository registrations) {
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME);
this.registrations = registrations;
}
/**
* Construct the parameters necessary for validating an asserting party's
* {@code <saml2:LogoutRequest>} based on the given {@link HttpServletRequest}
*
* <p>
* Uses the configured {@link RequestMatcher} to identify the processing request,
* including looking for any indicated {@code registrationId}.
*
* <p>
* If a {@code registrationId} is found in the request, it will attempt to use that,
* erroring if no {@link RelyingPartyRegistration} is found.
*
* <p>
* If no {@code registrationId} is found in the request, it will look for a currently
* logged-in user and use the associated {@code registrationId}.
*
* <p>
* In the event that neither the URL nor any logged in user could determine a
* {@code registrationId}, this code then will try and derive a
* {@link RelyingPartyRegistration} given the {@code <saml2:LogoutRequest>}'s
* {@code Issuer} value.
* @param request the HTTP request
* @return a {@link Saml2LogoutRequestValidatorParameters} instance, or {@code null}
* if one could not be resolved
* @throws Saml2AuthenticationException if the {@link RequestMatcher} specifies a
* non-existent {@code registrationId}
*/
@Override
public Saml2LogoutRequestValidatorParameters resolve(HttpServletRequest request, Authentication authentication) {
if (request.getParameter(Saml2ParameterNames.SAML_REQUEST) == null) {
return null;
}
RequestMatcher.MatchResult result = this.requestMatcher.matcher(request);
if (!result.isMatch()) {
return null;
}
String registrationId = getRegistrationId(result, authentication);
if (registrationId == null) {
return logoutRequestByEntityId(request, authentication);
}
return logoutRequestById(request, authentication, registrationId);
}
/**
* The request matcher to use to identify a request to process a
* {@code <saml2:LogoutRequest>}. By default, checks for {@code /logout/saml2/slo} and
* {@code /logout/saml2/slo/{registrationId}}.
*
* <p>
* Generally speaking, the URL does not need to have a {@code registrationId} in it
* since either it can be looked up from the active logged in user or it can be
* derived through the {@code Issuer} in the {@code <saml2:LogoutRequest>}.
* @param requestMatcher the {@link RequestMatcher} to use
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
private String getRegistrationId(RequestMatcher.MatchResult result, Authentication authentication) {
String registrationId = result.getVariables().get("registrationId");
if (registrationId != null) {
return registrationId;
}
if (authentication == null) {
return null;
}
if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal principal) {
return principal.getRelyingPartyRegistrationId();
}
return null;
}
private Saml2LogoutRequestValidatorParameters logoutRequestById(HttpServletRequest request,
Authentication authentication, String registrationId) {
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
if (registration == null) {
throw new Saml2AuthenticationException(
Saml2Error.relyingPartyRegistrationNotFound("registration not found"));
}
return logoutRequestByRegistration(request, registration, authentication);
}
private Saml2LogoutRequestValidatorParameters logoutRequestByEntityId(HttpServletRequest request,
Authentication authentication) {
String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST);
LogoutRequest logoutRequest = this.saml
.deserialize(org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2Utils
.withEncoded(serialized)
.inflate(HttpMethod.GET.matches(request.getMethod()))
.decode());
String issuer = logoutRequest.getIssuer().getValue();
RelyingPartyRegistration registration = this.registrations.findUniqueByAssertingPartyEntityId(issuer);
return logoutRequestByRegistration(request, registration, authentication);
}
private Saml2LogoutRequestValidatorParameters logoutRequestByRegistration(HttpServletRequest request,
RelyingPartyRegistration registration, Authentication authentication) {
if (registration == null) {
return null;
}
Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request);
registration = fromRequest(request, registration);
String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST);
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(serialized)
.relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE))
.binding(saml2MessageBinding)
.location(registration.getSingleLogoutServiceLocation())
.parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG,
request.getParameter(Saml2ParameterNames.SIG_ALG)))
.parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE,
request.getParameter(Saml2ParameterNames.SIGNATURE)))
.parametersQuery((params) -> request.getQueryString())
.build();
return new Saml2LogoutRequestValidatorParameters(logoutRequest, registration, authentication);
}
private RelyingPartyRegistration fromRequest(HttpServletRequest request, RelyingPartyRegistration registration) {
RelyingPartyRegistrationPlaceholderResolvers.UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers
.uriResolver(request, registration);
String entityId = uriResolver.resolve(registration.getEntityId());
String logoutLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation());
String logoutResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation());
return registration.mutate()
.entityId(entityId)
.singleLogoutServiceLocation(logoutLocation)
.singleLogoutServiceResponseLocation(logoutResponseLocation)
.build();
}
}

View File

@ -1,223 +0,0 @@
/*
* Copyright 2002-2025 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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
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.authentication.logout.OpenSamlOperations.SignatureConfigurer;
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;
/**
* Tests for {@link OpenSamlLogoutRequestValidator}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutRequestValidatorTests {
private final OpenSamlOperations saml = new OpenSaml4Template();
private final OpenSamlLogoutRequestValidator manager = new OpenSamlLogoutRequestValidator();
@Test
public void handleWhenPostBindingThenValidates() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
@Test
public void handleWhenNameIdIsEncryptedIdPostThenValidates() {
RelyingPartyRegistration registration = decrypting(encrypting(registration())).build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequestNameIdInEncryptedId(registration);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse();
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = registration()
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
.build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
Saml2LogoutRequest request = redirect(logoutRequest, registration,
this.saml.withSigningKeys(registration.getSigningX509Credentials()));
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getIssuer().setValue("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedUserThenInvalidRequestError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getNameID().setValue("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_REQUEST);
}
@Test
public void handleWhenMissingUserThenSubjectNotFoundError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setNameID(null);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.SUBJECT_NOT_FOUND);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setDestination("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
}
// gh-10923
@Test
public void handleWhenLogoutResponseHasLineBreaksThenHandles() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
sign(logoutRequest, registration);
String encoded = new StringBuffer(
Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8)))
.insert(10, "\r\n")
.toString();
Saml2LogoutRequest request = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(encoded)
.build();
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
}
private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) {
return builder
.decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential()));
}
private RelyingPartyRegistration.Builder encrypting(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyMetadata((party) -> party
.encryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyEncryptingCredential())));
}
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyMetadata((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
}
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
}
private Authentication authentication(RelyingPartyRegistration registration) {
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
return new Saml2Authentication(principal, "response", new ArrayList<>());
}
private Saml2LogoutRequest post(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
return Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8)))
.build();
}
private Saml2LogoutRequest redirect(LogoutRequest logoutRequest, RelyingPartyRegistration registration,
SignatureConfigurer configurer) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest)));
Map<String, String> parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_REQUEST, serialized));
return Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(serialized)
.parameters((params) -> params.putAll(parameters))
.build();
}
private void sign(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
TestOpenSamlObjects.signed(logoutRequest, registration.getSigningX509Credentials().iterator().next(),
registration.getAssertingPartyMetadata().getEntityId());
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}

View File

@ -1,190 +0,0 @@
/*
* Copyright 2002-2025 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.nio.charset.StandardCharsets;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.SignatureConfigurer;
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;
/**
* Tests for {@link OpenSamlLogoutResponseValidator}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutResponseValidatorTests {
private final OpenSamlOperations saml = new OpenSaml4Template();
private final OpenSamlLogoutResponseValidator manager = new OpenSamlLogoutResponseValidator();
@Test
public void handleWhenAuthenticatedThenHandles() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = signing(verifying(registration()))
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
.build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
Saml2LogoutResponse response = redirect(logoutResponse, registration,
this.saml.withSigningKeys(registration.getSigningX509Credentials()));
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getIssuer().setValue("wrong");
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.setDestination("wrong");
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
}
@Test
public void handleWhenStatusNotSuccessThenInvalidResponseError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getStatus().getStatusCode().setValue(StatusCode.UNKNOWN_PRINCIPAL);
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE);
}
// gh-10923
@Test
public void handleWhenLogoutResponseHasLineBreaksThenHandles() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
sign(logoutResponse, registration);
String encoded = new StringBuilder(
Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8)))
.insert(10, "\r\n")
.toString();
Saml2LogoutResponse response = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(encoded)
.build();
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyMetadata((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
}
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyMetadata((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
}
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
}
private Saml2LogoutResponse post(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
return Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8)))
.build();
}
private Saml2LogoutResponse redirect(LogoutResponse logoutResponse, RelyingPartyRegistration registration,
SignatureConfigurer configurer) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse)));
Map<String, String> parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_RESPONSE, serialized));
return Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(serialized)
.parameters((params) -> params.putAll(parameters))
.build();
}
private void sign(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
TestOpenSamlObjects.signed(logoutResponse, registration.getSigningX509Credentials().iterator().next(),
registration.getAssertingPartyMetadata().getEntityId());
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}

View File

@ -1,185 +0,0 @@
/*
* Copyright 2002-2025 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.metadata;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
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;
/**
* Tests for {@link OpenSamlMetadataResolver}
*/
public class OpenSamlMetadataResolverTests {
@Test
public void resolveWhenRelyingPartyThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
public void resolveWhenRelyingPartyAndSignMetadataSetThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
openSamlMetadataResolver.setSignMetadata(true);
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
.contains("Reference URI=\"\"")
.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
.contains("DigestValue")
.contains("SignatureValue");
}
@Test
public void resolveWhenRelyingPartyNoCredentialsThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
.assertingPartyMetadata((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.doesNotContain("<md:KeyDescriptor use=\"signing\">")
.doesNotContain("<md:KeyDescriptor use=\"encryption\">")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
public void resolveWhenRelyingPartyNameIDFormatThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.nameIdFormat("format")
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:NameIDFormat>format</md:NameIDFormat>");
}
@Test
public void resolveWhenRelyingPartyNoLogoutThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.singleLogoutServiceLocation(null)
.nameIdFormat("format")
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).doesNotContain("ResponseLocation");
}
@Test
public void resolveWhenEntityDescriptorCustomizerThenUses() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.entityId("originalEntityId")
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
openSamlMetadataResolver.setEntityDescriptorCustomizer(
(parameters) -> parameters.getEntityDescriptor().setEntityID("overriddenEntityId"));
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).contains("<md:EntityDescriptor").contains("entityID=\"overriddenEntityId\"");
}
@Test
public void resolveIterableWhenRelyingPartiesThenMetadataMatches() {
RelyingPartyRegistration one = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
RelyingPartyRegistration two = TestRelyingPartyRegistrations.full()
.entityId("two")
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
String metadata = openSamlMetadataResolver.resolve(List.of(one, two));
assertThat(metadata).contains("<md:EntitiesDescriptor")
.contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("entityID=\"two\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
public void resolveIterableWhenRelyingPartiesAndSignMetadataSetThenMetadataMatches() {
RelyingPartyRegistration one = TestRelyingPartyRegistrations.full()
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
RelyingPartyRegistration two = TestRelyingPartyRegistrations.full()
.entityId("two")
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
.build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
openSamlMetadataResolver.setSignMetadata(true);
String metadata = openSamlMetadataResolver.resolve(List.of(one, two));
assertThat(metadata).contains("<md:EntitiesDescriptor")
.contains("<md:EntityDescriptor")
.contains("entityID=\"rp-entity-id\"")
.contains("entityID=\"two\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
.contains("Reference URI=\"\"")
.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
.contains("DigestValue")
.contains("SignatureValue");
}
}

View File

@ -1,243 +0,0 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.common.SignableSAMLObject;
import org.opensaml.saml.saml2.core.Response;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2Utils;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
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.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.web.servlet.TestMockHttpServletRequests;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.UriUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenSamlAuthenticationTokenConverter}
*/
@ExtendWith(MockitoExtension.class)
public final class OpenSamlAuthenticationTokenConverterTests {
@Mock
RelyingPartyRegistrationRepository registrations;
private final OpenSamlOperations saml = new OpenSaml4Template();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
@Test
public void convertWhenSamlResponseThenToken() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE,
Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8)));
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
}
@Test
public void convertWhenSamlResponseInvalidBase64ThenSaml2AuthenticationException() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "invalid");
assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> converter.convert(request))
.withCauseInstanceOf(IllegalArgumentException.class)
.satisfies(
(ex) -> assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE))
.satisfies(
(ex) -> assertThat(ex.getSaml2Error().getDescription()).isEqualTo("Failed to decode SAMLResponse"));
}
@Test
public void convertWhenNoSamlResponseThenNull() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
assertThat(converter.convert(request)).isNull();
}
@Test
public void convertWhenNoMatchingRequestThenNull() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "ignored");
assertThat(converter.convert(request)).isNull();
}
@Test
public void convertWhenNoRelyingPartyRegistrationThenNull() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
String response = Saml2Utils.samlEncode(serialize(signed(response())).getBytes(StandardCharsets.UTF_8));
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, response);
assertThat(converter.convert(request)).isNull();
}
@Test
public void convertWhenGetRequestThenInflates() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = get("/login/saml2/sso/" + this.registration.getRegistrationId());
byte[] deflated = Saml2Utils.samlDeflate("response");
String encoded = Saml2Utils.samlEncode(deflated);
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded);
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
}
@Test
public void convertWhenGetRequestInvalidDeflatedThenSaml2AuthenticationException() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = get("/login/saml2/sso/" + this.registration.getRegistrationId());
byte[] invalidDeflated = "invalid".getBytes();
String encoded = Saml2Utils.samlEncode(invalidDeflated);
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded);
assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> converter.convert(request))
.withRootCauseInstanceOf(IOException.class)
.satisfies(
(ex) -> assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE))
.satisfies((ex) -> assertThat(ex.getSaml2Error().getDescription()).isEqualTo("Unable to inflate string"));
}
@Test
public void convertWhenUsingSamlUtilsBase64ThenXmlIsValid() throws Exception {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, getSsoCircleEncodedXml());
Saml2AuthenticationToken token = converter.convert(request);
validateSsoCircleXml(token.getSaml2Response());
}
@Test
public void convertWhenSavedAuthenticationRequestThenToken() {
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = mock(
Saml2AuthenticationRequestRepository.class);
AbstractSaml2AuthenticationRequest authenticationRequest = mock(AbstractSaml2AuthenticationRequest.class);
given(authenticationRequest.getRelyingPartyRegistrationId()).willReturn(this.registration.getRegistrationId());
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
converter.setAuthenticationRequestRepository(authenticationRequestRepository);
given(this.registrations.findByRegistrationId(any())).willReturn(this.registration);
given(authenticationRequestRepository.loadAuthenticationRequest(any(HttpServletRequest.class)))
.willReturn(authenticationRequest);
MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId());
request.setParameter(Saml2ParameterNames.SAML_RESPONSE,
Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8)));
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
assertThat(token.getAuthenticationRequest()).isEqualTo(authenticationRequest);
}
@Test
public void convertWhenMatchingNoRegistrationIdThenLooksUpByAssertingEntityId() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
String response = serialize(signed(response()));
String encoded = Saml2Utils.samlEncode(response.getBytes(StandardCharsets.UTF_8));
given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID))
.willReturn(this.registration);
MockHttpServletRequest request = post("/login/saml2/sso");
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded);
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo(response);
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
.isEqualTo(this.registration.getRegistrationId());
}
@Test
public void constructorWhenResolverIsNullThenIllegalArgument() {
assertThatIllegalArgumentException().isThrownBy(() -> new Saml2AuthenticationTokenConverter(null));
}
@Test
public void setAuthenticationRequestRepositoryWhenNullThenIllegalArgument() {
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> converter.setAuthenticationRequestRepository(null));
}
private void validateSsoCircleXml(String xml) {
assertThat(xml).contains("InResponseTo=\"ARQ9a73ead-7dcf-45a8-89eb-26f3c9900c36\"")
.contains(" ID=\"s246d157446618e90e43fb79bdd4d9e9e19cf2c7c4\"")
.contains("<saml:Issuer>https://idp.ssocircle.com</saml:Issuer>");
}
private String getSsoCircleEncodedXml() throws IOException {
ClassPathResource resource = new ClassPathResource("saml2-response-sso-circle.encoded");
String response = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
return UriUtils.decode(response, StandardCharsets.UTF_8);
}
private MockHttpServletRequest post(String uri) {
return TestMockHttpServletRequests.post(uri).build();
}
private MockHttpServletRequest get(String uri) {
return TestMockHttpServletRequests.get(uri).build();
}
private <T extends SignableSAMLObject> T signed(T toSign) {
TestOpenSamlObjects.signed(toSign, TestSaml2X509Credentials.assertingPartySigningCredential(),
TestOpenSamlObjects.RELYING_PARTY_ENTITY_ID);
return toSign;
}
private Response response() {
Response response = TestOpenSamlObjects.response();
response.setIssueInstant(Instant.now());
return response;
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}

View File

@ -1,150 +0,0 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensaml.core.xml.XMLObject;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters;
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.TestRelyingPartyRegistrations;
import org.springframework.security.web.servlet.TestMockHttpServletRequests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
public final class OpenSamlLogoutRequestValidatorParametersResolverTests {
@Mock
RelyingPartyRegistrationRepository registrations;
private final OpenSamlOperations saml = new OpenSaml4Template();
private RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
private OpenSamlLogoutRequestValidatorParametersResolver resolver;
@BeforeEach
void setup() {
this.resolver = new OpenSamlLogoutRequestValidatorParametersResolver(this.registrations);
}
@Test
void saml2LogoutRegistrationIdResolveWhenMatchesThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo/" + registrationId);
Authentication authentication = new TestingAuthenticationToken("user", "pass");
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, authentication);
assertThat(parameters.getAuthentication()).isEqualTo(authentication);
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request");
}
@Test
void saml2LogoutRegistrationIdWhenUnauthenticatedThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo/" + registrationId);
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null);
assertThat(parameters.getAuthentication()).isNull();
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request");
}
@Test
void saml2LogoutResolveWhenAuthenticatedThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo");
Authentication authentication = TestSaml2Authentications.authentication();
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, authentication);
assertThat(parameters.getAuthentication()).isEqualTo(authentication);
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request");
}
@Test
void saml2LogoutResolveWhenUnauthenticatedThenParameters() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = post("/logout/saml2/slo");
String logoutRequest = serialize(TestOpenSamlObjects.logoutRequest());
String encoded = Saml2Utils.samlEncode(logoutRequest.getBytes(StandardCharsets.UTF_8));
request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded);
given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID))
.willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null);
assertThat(parameters.getAuthentication()).isNull();
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo(encoded);
}
@Test
void saml2LogoutResolveWhenUnauthenticatedGetRequestThenInflates() {
String registrationId = this.registration.getRegistrationId();
MockHttpServletRequest request = get("/logout/saml2/slo");
String logoutRequest = serialize(TestOpenSamlObjects.logoutRequest());
String encoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(logoutRequest));
request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded);
given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID))
.willReturn(this.registration);
Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null);
assertThat(parameters.getAuthentication()).isNull();
assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId);
assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo(encoded);
}
@Test
void saml2LogoutRegistrationIdResolveWhenNoMatchingRegistrationIdThenSaml2Exception() {
MockHttpServletRequest request = post("/logout/saml2/slo/id");
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
assertThatExceptionOfType(Saml2AuthenticationException.class)
.isThrownBy(() -> this.resolver.resolve(request, null));
}
private MockHttpServletRequest post(String uri) {
return TestMockHttpServletRequests.post(uri).build();
}
private MockHttpServletRequest get(String uri) {
return TestMockHttpServletRequests.get(uri).build();
}
private String serialize(XMLObject object) {
return this.saml.serialize(object).serialize();
}
}