diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java index 6467a928e3..5b390fe19b 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java +++ b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java @@ -31,7 +31,6 @@ import org.springframework.security.authentication.AccountStatusUserDetailsCheck import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.SpringSecurityMessageSource; diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/ServiceAuthenticationDetails.java b/cas/src/main/java/org/springframework/security/cas/authentication/ServiceAuthenticationDetails.java new file mode 100644 index 0000000000..d35c2d7732 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/authentication/ServiceAuthenticationDetails.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011-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.cas.authentication; + +import java.io.Serializable; + +import org.springframework.security.cas.ServiceProperties; +import org.springframework.security.core.Authentication; + +/** + * In order for the {@link CasAuthenticationProvider} to provide the correct service url + * to authenticate the ticket, the returned value of {@link Authentication#getDetails()} + * should implement this interface when tickets can be sent to any URL rather than only + * {@link ServiceProperties#getService()}. + * + * @author Rob Winch + * @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource + */ +public interface ServiceAuthenticationDetails extends Serializable { + + /** + * Gets the absolute service url (i.e. https://example.com/service/). + * @return the service url. Cannot be null. + */ + String getServiceUrl(); + +} diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java index 9c39d66544..91d6a34d66 100644 --- a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java +++ b/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023 the original author or authors. + * Copyright 2011-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. @@ -16,22 +16,24 @@ package org.springframework.security.cas.web.authentication; -import java.io.Serializable; - import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.cas.authentication.CasAuthenticationProvider; import org.springframework.security.core.Authentication; /** - * In order for the {@link CasAuthenticationProvider} to provide the correct service url - * to authenticate the ticket, the returned value of {@link Authentication#getDetails()} - * should implement this interface when tickets can be sent to any URL rather than only - * {@link ServiceProperties#getService()}. + * In order for the + * {@link org.springframework.security.cas.authentication.CasAuthenticationProvider} to + * provide the correct service url to authenticate the ticket, the returned value of + * {@link Authentication#getDetails()} should implement this interface when tickets can be + * sent to any URL rather than only {@link ServiceProperties#getService()}. * * @author Rob Winch * @see ServiceAuthenticationDetailsSource + * @deprecated Please use + * org.springframework.security.cas.authentication.ServiceAuthenticationDetails */ -public interface ServiceAuthenticationDetails extends Serializable { +@Deprecated +public interface ServiceAuthenticationDetails + extends org.springframework.security.cas.authentication.ServiceAuthenticationDetails { /** * Gets the absolute service url (i.e. https://example.com/service/). diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java index 42153be417..a37b7743e1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -16,25 +16,9 @@ package org.springframework.security.saml2.provider.service.metadata; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.UUID; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.security.saml2.Saml2Exception; 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; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; /** * An implementation of {@link Saml2MetadataResponseResolver} that identifies which @@ -42,140 +26,23 @@ import org.springframework.util.Assert; * * @author Josh Cummings * @since 6.1 + * @deprecated Please use + * {@link org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver} */ -public final class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver { - - private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml"; - - private RequestMatcher matcher = new OrRequestMatcher( - new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"), - new AntPathRequestMatcher("/saml2/metadata/{registrationId}"), - new AntPathRequestMatcher("/saml2/metadata")); - - private String filename = DEFAULT_METADATA_FILENAME; - - private final RelyingPartyRegistrationRepository registrations; - - private final Saml2MetadataResolver metadata; +@Deprecated +public final class RequestMatcherMetadataResponseResolver extends + org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver { /** - * Construct a {@link RequestMatcherMetadataResponseResolver} + * Construct a + * {@link org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver} * @param registrations the source for relying party metadata * @param metadata the strategy for converting {@link RelyingPartyRegistration}s into * metadata */ public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations, Saml2MetadataResolver metadata) { - Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null"); - Assert.notNull(metadata, "saml2MetadataResolver cannot be null"); - this.registrations = registrations; - this.metadata = metadata; - } - - /** - * Construct and serialize a relying party's SAML 2.0 metadata based on the given - * {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify - * the metadata request, including looking for any indicated {@code registrationId}. - * - *

- * If a {@code registrationId} is found in the request, it will attempt to use that, - * erroring if no {@link RelyingPartyRegistration} is found. - * - *

- * If no {@code registrationId} is found in the request, it will attempt to show all - * {@link RelyingPartyRegistration}s in an {@code }. To - * exercise this functionality, the provided - * {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}. - * @param request the HTTP request - * @return a {@link Saml2MetadataResponse} instance - * @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent - * {@code registrationId} - */ - @Override - public Saml2MetadataResponse resolve(HttpServletRequest request) { - RequestMatcher.MatchResult result = this.matcher.matcher(request); - if (!result.isMatch()) { - return null; - } - String registrationId = result.getVariables().get("registrationId"); - Saml2MetadataResponse response = responseByRegistrationId(request, registrationId); - if (response != null) { - return response; - } - if (this.registrations instanceof Iterable) { - Iterable registrations = (Iterable) this.registrations; - return responseByIterable(request, registrations); - } - return null; - } - - private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) { - if (registrationId == null) { - return null; - } - RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId); - if (registration == null) { - throw new Saml2Exception("registration not found"); - } - return responseByIterable(request, Collections.singleton(registration)); - } - - private Saml2MetadataResponse responseByIterable(HttpServletRequest request, - Iterable registrations) { - Map results = new LinkedHashMap<>(); - for (RelyingPartyRegistration registration : registrations) { - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); - results.computeIfAbsent(entityId, (e) -> { - String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation()); - String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation()); - String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation()); - return registration.mutate() - .entityId(entityId) - .assertionConsumerServiceLocation(ssoLocation) - .singleLogoutServiceLocation(sloLocation) - .singleLogoutServiceResponseLocation(sloResponseLocation) - .build(); - }); - } - String metadata = this.metadata.resolve(results.values()); - String value = (results.size() == 1) ? results.values().iterator().next().getRegistrationId() - : UUID.randomUUID().toString(); - String fileName = this.filename.replace("{registrationId}", value); - try { - String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()); - return new Saml2MetadataResponse(metadata, encodedFileName); - } - catch (UnsupportedEncodingException ex) { - throw new Saml2Exception(ex); - } - } - - /** - * Use this {@link RequestMatcher} to identity which requests to generate metadata - * for. By default, matches {@code /saml2/metadata}, - * {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata}, - * and {@code /saml2/service-provider-metadata/{registrationId}} - * @param requestMatcher the {@link RequestMatcher} to use - */ - public void setRequestMatcher(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "requestMatcher cannot be empty"); - this.matcher = requestMatcher; - } - - /** - * Sets the metadata filename template. If it contains the {@code {registrationId}} - * placeholder, it will be resolved as a random UUID if there are multiple - * {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the - * {@link RelyingPartyRegistration}'s id. - * - *

- * The default value is {@code saml-{registrationId}-metadata.xml} - * @param metadataFilename metadata filename, must contain a {registrationId} - */ - public void setMetadataFilename(String metadataFilename) { - Assert.hasText(metadataFilename, "metadataFilename cannot be empty"); - this.filename = metadataFilename; + super(registrations, metadata); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/metadata/RequestMatcherMetadataResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/metadata/RequestMatcherMetadataResponseResolver.java new file mode 100644 index 0000000000..96850d3661 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/metadata/RequestMatcherMetadataResponseResolver.java @@ -0,0 +1,185 @@ +/* + * 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.web.metadata; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver; +import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse; +import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; +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; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * An implementation of {@link Saml2MetadataResponseResolver} that identifies which + * {@link RelyingPartyRegistration}s to use with a {@link RequestMatcher} + * + * @author Josh Cummings + * @since 6.1 + */ +public class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver { + + private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml"; + + private RequestMatcher matcher = new OrRequestMatcher( + new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"), + new AntPathRequestMatcher("/saml2/metadata/{registrationId}"), + new AntPathRequestMatcher("/saml2/metadata")); + + private String filename = DEFAULT_METADATA_FILENAME; + + private final RelyingPartyRegistrationRepository registrations; + + private final Saml2MetadataResolver metadata; + + /** + * Construct a + * {@link org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver} + * @param registrations the source for relying party metadata + * @param metadata the strategy for converting {@link RelyingPartyRegistration}s into + * metadata + */ + public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations, + Saml2MetadataResolver metadata) { + Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null"); + Assert.notNull(metadata, "saml2MetadataResolver cannot be null"); + this.registrations = registrations; + this.metadata = metadata; + } + + /** + * Construct and serialize a relying party's SAML 2.0 metadata based on the given + * {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify + * the metadata request, including looking for any indicated {@code registrationId}. + * + *

+ * If a {@code registrationId} is found in the request, it will attempt to use that, + * erroring if no {@link RelyingPartyRegistration} is found. + * + *

+ * If no {@code registrationId} is found in the request, it will attempt to show all + * {@link RelyingPartyRegistration}s in an {@code }. To + * exercise this functionality, the provided + * {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}. + * @param request the HTTP request + * @return a {@link Saml2MetadataResponse} instance + * @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent + * {@code registrationId} + */ + @Override + public Saml2MetadataResponse resolve(HttpServletRequest request) { + RequestMatcher.MatchResult result = this.matcher.matcher(request); + if (!result.isMatch()) { + return null; + } + String registrationId = result.getVariables().get("registrationId"); + Saml2MetadataResponse response = responseByRegistrationId(request, registrationId); + if (response != null) { + return response; + } + if (this.registrations instanceof Iterable) { + Iterable registrations = (Iterable) this.registrations; + return responseByIterable(request, registrations); + } + return null; + } + + private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) { + if (registrationId == null) { + return null; + } + RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId); + if (registration == null) { + throw new Saml2Exception("registration not found"); + } + return responseByIterable(request, Collections.singleton(registration)); + } + + private Saml2MetadataResponse responseByIterable(HttpServletRequest request, + Iterable registrations) { + Map results = new LinkedHashMap<>(); + for (RelyingPartyRegistration registration : registrations) { + RelyingPartyRegistrationPlaceholderResolvers.UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers + .uriResolver(request, registration); + String entityId = uriResolver.resolve(registration.getEntityId()); + results.computeIfAbsent(entityId, (e) -> { + String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation()); + String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation()); + String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation()); + return registration.mutate() + .entityId(entityId) + .assertionConsumerServiceLocation(ssoLocation) + .singleLogoutServiceLocation(sloLocation) + .singleLogoutServiceResponseLocation(sloResponseLocation) + .build(); + }); + } + String metadata = this.metadata.resolve(results.values()); + String value = (results.size() == 1) ? results.values().iterator().next().getRegistrationId() + : UUID.randomUUID().toString(); + String fileName = this.filename.replace("{registrationId}", value); + try { + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()); + return new Saml2MetadataResponse(metadata, encodedFileName); + } + catch (UnsupportedEncodingException ex) { + throw new Saml2Exception(ex); + } + } + + /** + * Use this {@link RequestMatcher} to identity which requests to generate metadata + * for. By default, matches {@code /saml2/metadata}, + * {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata}, + * and {@code /saml2/service-provider-metadata/{registrationId}} + * @param requestMatcher the {@link RequestMatcher} to use + */ + public void setRequestMatcher(RequestMatcher requestMatcher) { + Assert.notNull(requestMatcher, "requestMatcher cannot be empty"); + this.matcher = requestMatcher; + } + + /** + * Sets the metadata filename template. If it contains the {@code {registrationId}} + * placeholder, it will be resolved as a random UUID if there are multiple + * {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the + * {@link RelyingPartyRegistration}'s id. + * + *

+ * The default value is {@code saml-{registrationId}-metadata.xml} + * @param metadataFilename metadata filename, must contain a {registrationId} + */ + public void setMetadataFilename(String metadataFilename) { + Assert.hasText(metadataFilename, "metadataFilename cannot be empty"); + this.filename = metadataFilename; + } + +}