From 9b724377ce9210a2b8cef13f3fa545284a4fcbec Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:24:12 -0600 Subject: [PATCH] Rework Saml2 Authentication Statement This commit separates the authentication principal, the assertion details, and the relying party tenant into separate components. This allows the principal to be completely decoupled from how Spring Security triggers and processes SLO. Specifically, it adds Saml2AssertionAuthentication, a new authentication implementation that allows an Object principal and a Saml2ResponseAssertionAccessor credential. It also moves the relying party registration id from Saml2AuthenticatedPrincipal to Saml2AssertionAuthentication. As such, Saml2AuthenticatedPrincipal is now deprecated in favor of placing its assertion components in Saml2ResponseAssertionAccessor and the relying party registration id in Saml2AssertionAuthentication. Closes gh-10820 --- .../saml2/Saml2LogoutConfigurer.java | 15 ++- .../http/Saml2LogoutBeanDefinitionParser.java | 15 ++- .../security/SerializationSamples.java | 15 ++- ...on.Saml2AssertionAuthentication.serialized | Bin 0 -> 1386 bytes ...tication.Saml2ResponseAssertion.serialized | Bin 0 -> 350 bytes .../ROOT/pages/migration/servlet/saml2.adoc | 54 ++++++++ .../servlet/saml2/login/authentication.adoc | 8 +- .../Saml2AssertionAuthenticationMixin.java | 59 +++++++++ .../saml2/jackson2/Saml2Jackson2Module.java | 4 + ...leSaml2ResponseAssertionAccessorMixin.java | 56 +++++++++ .../DefaultSaml2AuthenticatedPrincipal.java | 8 ++ .../Saml2AssertionAuthentication.java | 65 ++++++++++ .../Saml2AuthenticatedPrincipal.java | 13 +- .../authentication/Saml2Authentication.java | 10 +- .../Saml2AuthenticationInfo.java | 85 ------------- .../Saml2ResponseAssertion.java | 115 ++++++++++++++++++ .../Saml2ResponseAssertionAccessor.java | 65 ++++++++++ .../BaseOpenSamlLogoutRequestValidator.java | 6 +- .../BaseOpenSamlLogoutRequestResolver.java | 31 +++-- ...outRequestValidatorParametersResolver.java | 14 ++- .../BaseOpenSamlLogoutResponseResolver.java | 14 ++- .../logout/Saml2LogoutRequestFilter.java | 14 ++- ...outRequestValidatorParametersResolver.java | 10 +- .../OpenSaml5AuthenticationProvider.java | 15 +-- ...faultSaml2AuthenticatedPrincipalTests.java | 3 +- 25 files changed, 558 insertions(+), 136 deletions(-) create mode 100644 config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication.serialized create mode 100644 config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion.serialized create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AssertionAuthenticationMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/SimpleSaml2ResponseAssertionAccessorMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationInfo.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertion.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertionAccessor.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index f8303a81b9..fc9950d964 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -33,7 +33,9 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationInfo; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator; import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator; @@ -531,7 +533,16 @@ public final class Saml2LogoutConfigurer> @Override public boolean matches(HttpServletRequest request) { Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); - return Saml2AuthenticationInfo.fromAuthentication(authentication) != null; + if (authentication == null) { + return false; + } + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) { + return true; + } + if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor) { + return true; + } + return authentication instanceof Saml2Authentication; } } diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java index dfeed0d981..749127027c 100644 --- a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java @@ -31,7 +31,9 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationInfo; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter; @@ -236,7 +238,16 @@ final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser { @Override public boolean matches(HttpServletRequest request) { Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); - return Saml2AuthenticationInfo.fromAuthentication(authentication) != null; + if (authentication == null) { + return false; + } + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) { + return true; + } + if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor) { + return true; + } + return authentication instanceof Saml2Authentication; } public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { diff --git a/config/src/test/java/org/springframework/security/SerializationSamples.java b/config/src/test/java/org/springframework/security/SerializationSamples.java index 396828ca33..d120a6ebd0 100644 --- a/config/src/test/java/org/springframework/security/SerializationSamples.java +++ b/config/src/test/java/org/springframework/security/SerializationSamples.java @@ -170,11 +170,14 @@ import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.credentials.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; 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.Saml2PostAuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationTokens; import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications; import org.springframework.security.saml2.provider.service.authentication.TestSaml2LogoutRequests; @@ -520,8 +523,16 @@ final class SerializationSamples { generatorByClassName.put(Saml2Exception.class, (r) -> new Saml2Exception("message", new IOException("fail"))); generatorByClassName.put(DefaultSaml2AuthenticatedPrincipal.class, (r) -> TestSaml2Authentications.authentication().getPrincipal()); - generatorByClassName.put(Saml2Authentication.class, - (r) -> applyDetails(TestSaml2Authentications.authentication())); + Saml2Authentication saml2 = TestSaml2Authentications.authentication(); + generatorByClassName.put(Saml2Authentication.class, (r) -> applyDetails(saml2)); + Saml2ResponseAssertionAccessor assertion = Saml2ResponseAssertion.withResponseValue("response") + .nameId("name") + .sessionIndexes(List.of("id")) + .attributes(Map.of("key", List.of("value"))) + .build(); + generatorByClassName.put(Saml2ResponseAssertion.class, (r) -> assertion); + generatorByClassName.put(Saml2AssertionAuthentication.class, (r) -> applyDetails( + new Saml2AssertionAuthentication(assertion, authentication.getAuthorities(), "id"))); generatorByClassName.put(Saml2PostAuthenticationRequest.class, (r) -> TestSaml2PostAuthenticationRequests.create()); generatorByClassName.put(Saml2RedirectAuthenticationRequest.class, diff --git a/config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication.serialized b/config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication.serialized new file mode 100644 index 0000000000000000000000000000000000000000..61898671f849a92212fc89c67a7c8b666bbacabc GIT binary patch literal 1386 zcmb_cJ#Q015FMYLfI-d&NPLQj0uU1DPEeA{AyJ5QQIJg%K|!>>o0v`3cgOD9Ia5UN z2ht)68Y%=SfnNYZf&vL7L<7G73PeFe!R%ff$3Zd_TyeVFot^jQy_wzo1_MgMQY`DP zN+gf!%aS#4EtadU!k{U+Zo7&##6>reaf^p2XfG%t5TNr5`FwNu0t(vH^qFwV_ z?%90x=H-{lkMHLkaC|7TPLPJcCuDCIve!ZOEV9=@rge+#^&)!~xq>Q*BZXZCL4Zod za@>bw5=EP8yUL_)SFp~NmX`EPsNsOW!dlD|EUJ4|Z6p{^B`o|mMS6r7cvO5h8~L9z zA&lSwPnhUsF;`o`Kojqht-n**ZCD3~s7?2!)~k(Mu9;c`-RsA-com}$PtLAK)8UW< z`5RE$aSlTt_M88h?B}R79Lh9mYA(FXu@FR$JSK*FLez{4Ww;g6BuBk;#>nZKP`4YZ zPR&P+IONNm)dc!vz^~!c7oLCJ7;zxyLqX7rhLIgGCdL?X{_p9!Fh?ig?%SI`$86GE zU|0>N36VP3%~F<{@c~iY2)nyz5i)w zZt~g3O$Q1->|>f{uh!J2-G+Dc!7NLt&jus9mA1Irin3u}V4|5R>cdcnbrfQXw$7sU zezfbo>69wU#f_&gE**b7YMsw(R=39It?`bA{3^EX`6Hy`Ry$}IXc@3H)le{GH0&ig U?UnOqtiwZV>X;Gtuz8*Q4Sh!7+5i9m literal 0 HcmV?d00001 diff --git a/config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion.serialized b/config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion.serialized new file mode 100644 index 0000000000000000000000000000000000000000..0bc93ea8af543fd06d7917c4d55672a86718d464 GIT binary patch literal 350 zcmZXPO-jT-5QQs0BO*9DZoNYXTskY!O&}r|aB$fsWo#$YX}clFlNDIP_< zgmwTt6G1+h%8)owh?5k(#^G%3tV<~E(ojMDqabai}pYm+YPEl>vcP* z3We z(o753&>o%F413?BR_Z1z{f2J*bld)9U&yw{JGh6qfH5JF$K<+37gBfsq!x;J>Wu$y z5>O(b#BtZQCED4;^XuXM(` details from the principal. +This allows Spring Security to retrieve needed assertion details to perform Single Logout. + +This deprecates `Saml2AuthenticatedPrincipal`. +You no longer need to implement it to use `Saml2Authentication`. + +Instead, the credential implements `Saml2ResponseAssertionAccessor`, which Spring Security 7 favors when determining the appropriate action based on the authentication. + +This change is made automatically for you when using the defaults. + +If this causes you trouble when upgrading, you can publish a custom `ResponseAuhenticationConverter` to return a `Saml2Authentication` instead of returning a `Saml2AssertionAuthentication` like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +OpenSaml5AuthenticationProvider authenticationProvider() { + OpenSaml5AuthenticationProvider authenticationProvider = + new OpenSaml5AuthenticationProvider(); + ResponseAuthenticationConverter defaults = new ResponseAuthenticationConverter(); + authenticationProvider.setResponseAuthenticationConverter( + defaults.andThen((authentication) -> new Saml2Authentication( + authentication.getPrincipal(), + authentication.getSaml2Response(), + authentication.getAuthorities()))); + return authenticationProvider; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun authenticationProvider(): OpenSaml5AuthenticationProvider { + val authenticationProvider = OpenSaml5AuthenticationProvider() + val defaults = ResponseAuthenticationConverter() + authenticationProvider.setResponseAuthenticationConverter( + defaults.andThen { authentication -> + Saml2Authentication(authentication.getPrincipal(), + authentication.getSaml2Response(), + authentication.getAuthorities()) + }) + return authenticationProvider +} +---- +====== + +If you are constructing a `Saml2Authentication` instance yourself, consider changing to `Saml2AssertionAuthentication` to get the same benefit as the current default. diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc index 1c517914dc..400a66ad3a 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc @@ -341,8 +341,10 @@ class MyUserDetailsResponseAuthenticationConverter implements Converter UserDetails principal = this.userDetailsService.loadByUsername(username); <2> String saml2Response = authentication.getSaml2Response(); + Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor( + saml2Response, CollectionUtils.getFirst(response.getAssertions())); Collection authorities = principal.getAuthorities(); - return new Saml2Authentication((AuthenticatedPrincipal) userDetails, saml2Response, authorities); <3> + return new Saml2AssertionAuthentication(userDetails, assertion, authorities); <3> } } @@ -361,8 +363,10 @@ open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAu val authentication = this.delegate.convert(responseToken) <1> val principal = this.userDetailsService.loadByUsername(username) <2> val saml2Response = authentication.getSaml2Response() + val assertion = OpenSamlResponseAssertionAccessor( + saml2Response, CollectionUtils.getFirst(response.getAssertions())) val authorities = principal.getAuthorities() - return Saml2Authentication(userDetails as AuthenticatedPrincipal, saml2Response, authorities) <3> + return Saml2AssertionAuthentication(userDetails, assertion, authorities) <3> } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AssertionAuthenticationMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AssertionAuthenticationMixin.java new file mode 100644 index 0000000000..d3d3156446 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AssertionAuthenticationMixin.java @@ -0,0 +1,59 @@ +/* + * 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.jackson2; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link Saml2AssertionAuthentication}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * 
+ * + * @author Josh Cummings + * @since 7.0 + * @see Saml2Jackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true) +class Saml2AssertionAuthenticationMixin { + + @JsonCreator + Saml2AssertionAuthenticationMixin(@JsonProperty("principal") Object principal, + @JsonProperty("assertion") Saml2ResponseAssertionAccessor assertion, + @JsonProperty("authorities") Collection authorities, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java index 3d99fc2cfa..b53fcd15bc 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java @@ -22,10 +22,12 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; /** @@ -49,6 +51,8 @@ public class Saml2Jackson2Module extends SimpleModule { @Override public void setupModule(SetupContext context) { context.setMixInAnnotations(Saml2Authentication.class, Saml2AuthenticationMixin.class); + context.setMixInAnnotations(Saml2AssertionAuthentication.class, Saml2AssertionAuthenticationMixin.class); + context.setMixInAnnotations(Saml2ResponseAssertion.class, SimpleSaml2ResponseAssertionAccessorMixin.class); context.setMixInAnnotations(DefaultSaml2AuthenticatedPrincipal.class, DefaultSaml2AuthenticatedPrincipalMixin.class); context.setMixInAnnotations(Saml2LogoutRequest.class, Saml2LogoutRequestMixin.class); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/SimpleSaml2ResponseAssertionAccessorMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/SimpleSaml2ResponseAssertionAccessorMixin.java new file mode 100644 index 0000000000..7f4d02b4a0 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/SimpleSaml2ResponseAssertionAccessorMixin.java @@ -0,0 +1,56 @@ +/* + * 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.jackson2; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion; + +/** + * Jackson Mixin class helps in serialize/deserialize {@link Saml2ResponseAssertion}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * 
+ * + * @author Josh Cummings + * @since 7.0 + * @see Saml2Jackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true) +class SimpleSaml2ResponseAssertionAccessorMixin { + + @JsonCreator + SimpleSaml2ResponseAssertionAccessorMixin(@JsonProperty("responseValue") String responseValue, + @JsonProperty("nameId") String nameId, @JsonProperty("sessionIndexes") List sessionIndexes, + @JsonProperty("attributes") Map> attributes) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java index d99f640cb5..d2e9658fd0 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java @@ -30,7 +30,9 @@ import org.springframework.util.Assert; * * @author Clement Stoquart * @since 5.4 + * @deprecated Please use {@link Saml2ResponseAssertionAccessor} */ +@Deprecated public class DefaultSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPrincipal, Serializable { @Serial @@ -58,6 +60,12 @@ public class DefaultSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPri this.sessionIndexes = sessionIndexes; } + public DefaultSaml2AuthenticatedPrincipal(String name, Saml2ResponseAssertionAccessor assertion) { + this.name = name; + this.attributes = assertion.getAttributes(); + this.sessionIndexes = assertion.getSessionIndexes(); + } + @Override public String getName() { return this.name; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java new file mode 100644 index 0000000000..c38cfbfbd1 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java @@ -0,0 +1,65 @@ +/* + * 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; + +import java.io.Serial; +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; + +/** + * An authentication based off of a SAML 2.0 Assertion + * + * @author Josh Cummings + * @since 7.0 + * @see Saml2ResponseAssertionAccessor + * @see Saml2ResponseAssertion + */ +public class Saml2AssertionAuthentication extends Saml2Authentication { + + @Serial + private static final long serialVersionUID = -4194323643788693205L; + + private final Saml2ResponseAssertionAccessor assertion; + + private final String relyingPartyRegistrationId; + + public Saml2AssertionAuthentication(Saml2ResponseAssertionAccessor assertion, + Collection authorities, String relyingPartyRegistrationId) { + super(assertion, assertion.getResponseValue(), authorities); + this.assertion = assertion; + this.relyingPartyRegistrationId = relyingPartyRegistrationId; + } + + public Saml2AssertionAuthentication(Object principal, Saml2ResponseAssertionAccessor assertion, + Collection authorities, String relyingPartyRegistrationId) { + super(principal, assertion.getResponseValue(), authorities); + this.assertion = assertion; + this.relyingPartyRegistrationId = relyingPartyRegistrationId; + setAuthenticated(true); + } + + @Override + public Saml2ResponseAssertionAccessor getCredentials() { + return this.assertion; + } + + public String getRelyingPartyRegistrationId() { + return this.relyingPartyRegistrationId; + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java index 80e67376e6..6f3e32c888 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java @@ -30,8 +30,12 @@ import org.springframework.util.CollectionUtils; * * @author Clement Stoquart * @since 5.2.2 + * @deprecated Please use + * {@link Saml2AssertionAuthentication#getRelyingPartyRegistrationId()} and + * {@link Saml2ResponseAssertionAccessor} instead */ -public interface Saml2AuthenticatedPrincipal extends AuthenticatedPrincipal, Saml2AuthenticationInfo { +@Deprecated +public interface Saml2AuthenticatedPrincipal extends AuthenticatedPrincipal { /** * Get the first value of Saml2 token attribute by name @@ -72,17 +76,10 @@ public interface Saml2AuthenticatedPrincipal extends AuthenticatedPrincipal, Sam * @return the {@link RelyingPartyRegistration} identifier * @since 5.6 */ - @Override default String getRelyingPartyRegistrationId() { return null; } - @Override - default String getNameId() { - return getName(); - } - - @Override default List getSessionIndexes() { return Collections.emptyList(); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java index 2292f52a37..e628f260e8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java @@ -41,7 +41,7 @@ public class Saml2Authentication extends AbstractAuthenticationToken { @Serial private static final long serialVersionUID = 405897702378720477L; - private final AuthenticatedPrincipal principal; + private final Object principal; private final String saml2Response; @@ -61,6 +61,14 @@ public class Saml2Authentication extends AbstractAuthenticationToken { setAuthenticated(true); } + public Saml2Authentication(Object principal, String saml2Response, + Collection authorities) { + super(authorities); + this.principal = principal; + this.saml2Response = saml2Response; + setAuthenticated(true); + } + @Override public Object getPrincipal() { return this.principal; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationInfo.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationInfo.java deleted file mode 100644 index db412f026e..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationInfo.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2022 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; - -import java.util.List; - -import org.opensaml.saml.saml2.core.NameID; -import org.opensaml.saml.saml2.core.SessionIndex; - -import org.springframework.security.core.Authentication; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; - -/** - * Additional SAML 2.0 authentication information - * - *

- * SAML 2.0 Single Logout requires that the {@link Authentication#getPrincipal() - * authenticated principal} or the {@link Authentication} itself implements this - * interface. - * - * @author Christian Schuster - */ -public interface Saml2AuthenticationInfo { - - /** - * Get the {@link RelyingPartyRegistration} identifier - * @return the {@link RelyingPartyRegistration} identifier - */ - String getRelyingPartyRegistrationId(); - - /** - * Get the {@link NameID} value of the authenticated principal - * @return the {@link NameID} value of the authenticated principal - */ - String getNameId(); - - /** - * Get the {@link SessionIndex} values of the authenticated principal - * @return the {@link SessionIndex} values of the authenticated principal - */ - List getSessionIndexes(); - - /** - * Try to obtain a {@link Saml2AuthenticationInfo} instance from an - * {@link Authentication} - * - *

- * The result is either the {@link Authentication#getPrincipal() authenticated - * principal}, the {@link Authentication} itself, or {@code null}. - * - *

- * Returning {@code null} indicates that the given {@link Authentication} does not - * represent a SAML 2.0 authentication. - * @param authentication the {@link Authentication} - * @return the {@link Saml2AuthenticationInfo} or {@code null} if unavailable - */ - static Saml2AuthenticationInfo fromAuthentication(Authentication authentication) { - if (authentication == null) { - return null; - } - Object principal = authentication.getPrincipal(); - if (principal instanceof Saml2AuthenticationInfo) { - return (Saml2AuthenticationInfo) principal; - } - if (authentication instanceof Saml2AuthenticationInfo) { - return (Saml2AuthenticationInfo) authentication; - } - return null; - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertion.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertion.java new file mode 100644 index 0000000000..ea5dd2a8a9 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertion.java @@ -0,0 +1,115 @@ +/* + * 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; + +import java.io.Serial; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * An OpenSAML-based implementation of {@link Saml2ResponseAssertionAccessor} + * + * @author Josh Cummings + * @since 7.0 + */ +public class Saml2ResponseAssertion implements Saml2ResponseAssertionAccessor { + + @Serial + private static final long serialVersionUID = -7505233045395024212L; + + private final String responseValue; + + private final String nameId; + + private final List sessionIndexes; + + private final Map> attributes; + + Saml2ResponseAssertion(String responseValue, String nameId, List sessionIndexes, + Map> attributes) { + Assert.notNull(responseValue, "response value cannot be null"); + Assert.notNull(nameId, "nameId cannot be null"); + Assert.notNull(sessionIndexes, "sessionIndexes cannot be null"); + Assert.notNull(attributes, "attributes cannot be null"); + this.responseValue = responseValue; + this.nameId = nameId; + this.sessionIndexes = sessionIndexes; + this.attributes = attributes; + } + + public static Builder withResponseValue(String responseValue) { + return new Builder(responseValue); + } + + @Override + public String getNameId() { + return this.nameId; + } + + @Override + public List getSessionIndexes() { + return this.sessionIndexes; + } + + @Override + public Map> getAttributes() { + return this.attributes; + } + + @Override + public String getResponseValue() { + return this.responseValue; + } + + public static final class Builder { + + private final String responseValue; + + private String nameId; + + private List sessionIndexes = List.of(); + + private Map> attributes = Map.of(); + + Builder(String responseValue) { + this.responseValue = responseValue; + } + + public Builder nameId(String nameId) { + this.nameId = nameId; + return this; + } + + public Builder sessionIndexes(List sessionIndexes) { + this.sessionIndexes = sessionIndexes; + return this; + } + + public Builder attributes(Map> attributes) { + this.attributes = attributes; + return this; + } + + public Saml2ResponseAssertion build() { + return new Saml2ResponseAssertion(this.responseValue, this.nameId, this.sessionIndexes, this.attributes); + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertionAccessor.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertionAccessor.java new file mode 100644 index 0000000000..f137b79848 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertionAccessor.java @@ -0,0 +1,65 @@ +/* + * 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; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.CollectionUtils; + +/** + * An interface that represents key details from a SAML 2.0 Assertion + * + * @author Josh Cummings + * @since 7.0 + * @see Saml2ResponseAssertion + */ +public interface Saml2ResponseAssertionAccessor extends Serializable { + + String getNameId(); + + List getSessionIndexes(); + + /** + * Get the first value of Saml2 token attribute by name + * @param name the name of the attribute + * @param the type of the attribute + * @return the first attribute value or {@code null} otherwise + */ + @Nullable default A getFirstAttribute(String name) { + List values = getAttribute(name); + return CollectionUtils.firstElement(values); + } + + /** + * Get the Saml2 token attribute by name + * @param name the name of the attribute + * @param the type of the attribute + * @return the attribute or {@code null} otherwise + */ + @Nullable default List getAttribute(String name) { + return (List) getAttributes().get(name); + } + + Map> getAttributes(); + + String getResponseValue(); + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java index b96cb947c6..9165113684 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java @@ -27,6 +27,7 @@ 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.Saml2ResponseAssertionAccessor; 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; @@ -142,8 +143,9 @@ class BaseOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator } private void validateNameId(NameID nameId, Authentication authentication, Collection errors) { - String name = nameId.getValue(); - if (!name.equals(authentication.getName())) { + String name = (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor assertion) + ? assertion.getNameId() : authentication.getName(); + if (!nameId.getValue().equals(name)) { errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, "Failed to match subject in LogoutRequest with currently logged in user")); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestResolver.java index 1f0e99db32..def1ab7afe 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestResolver.java @@ -42,7 +42,9 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.Authentication; import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2ParameterNames; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationInfo; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; @@ -148,17 +150,25 @@ final class BaseOpenSamlLogoutRequestResolver implements Saml2LogoutRequestResol logoutRequest.setIssuer(issuer); NameID nameId = this.nameIdBuilder.buildObject(); logoutRequest.setNameID(nameId); - Saml2AuthenticationInfo info = Saml2AuthenticationInfo.fromAuthentication(authentication); - if (info != null) { + if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor info) { nameId.setValue(info.getNameId()); + } + else { + nameId.setValue(authentication.getName()); + } + if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor info) { for (String index : info.getSessionIndexes()) { SessionIndex sessionIndex = this.sessionIndexBuilder.buildObject(); sessionIndex.setValue(index); logoutRequest.getSessionIndexes().add(sessionIndex); } } - else { - nameId.setValue(authentication.getName()); + else if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal info) { + for (String index : info.getSessionIndexes()) { + SessionIndex sessionIndex = this.sessionIndexBuilder.buildObject(); + sessionIndex.setValue(index); + logoutRequest.getSessionIndexes().add(sessionIndex); + } } logoutRequest.setIssueInstant(Instant.now(this.clock)); this.parametersConsumer @@ -194,9 +204,14 @@ final class BaseOpenSamlLogoutRequestResolver implements Saml2LogoutRequestResol if (this.logger.isTraceEnabled()) { this.logger.trace("Attempting to resolve registrationId from " + authentication); } - Saml2AuthenticationInfo info = Saml2AuthenticationInfo.fromAuthentication(authentication); - if (info != null) { - return info.getRelyingPartyRegistrationId(); + if (authentication == null) { + return null; + } + if (authentication instanceof Saml2AssertionAuthentication response) { + return response.getRelyingPartyRegistrationId(); + } + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal principal) { + return principal.getRelyingPartyRegistrationId(); } return null; } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestValidatorParametersResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestValidatorParametersResolver.java index 975f83c930..3fc54e88bf 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestValidatorParametersResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestValidatorParametersResolver.java @@ -24,8 +24,9 @@ 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.Saml2AssertionAuthentication; +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.Saml2AuthenticationInfo; 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; @@ -130,9 +131,14 @@ final class BaseOpenSamlLogoutRequestValidatorParametersResolver if (registrationId != null) { return registrationId; } - Saml2AuthenticationInfo info = Saml2AuthenticationInfo.fromAuthentication(authentication); - if (info != null) { - return info.getRelyingPartyRegistrationId(); + if (authentication == null) { + return null; + } + if (authentication instanceof Saml2AssertionAuthentication saml2) { + return saml2.getRelyingPartyRegistrationId(); + } + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal saml2) { + return saml2.getRelyingPartyRegistrationId(); } return null; } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutResponseResolver.java index 4fe929e372..48dd0bc47a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutResponseResolver.java @@ -46,8 +46,9 @@ 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.Saml2ParameterNames; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +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.Saml2AuthenticationInfo; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; @@ -217,9 +218,14 @@ final class BaseOpenSamlLogoutResponseResolver implements Saml2LogoutResponseRes if (this.logger.isTraceEnabled()) { this.logger.trace("Attempting to resolve registrationId from " + authentication); } - Saml2AuthenticationInfo info = Saml2AuthenticationInfo.fromAuthentication(authentication); - if (info != null) { - return info.getRelyingPartyRegistrationId(); + if (authentication == null) { + return null; + } + if (authentication instanceof Saml2AssertionAuthentication saml2) { + return saml2.getRelyingPartyRegistrationId(); + } + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal saml2) { + return saml2.getRelyingPartyRegistrationId(); } return null; } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java index 18169605ca..29fe0921c5 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java @@ -33,8 +33,9 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.core.Saml2ErrorCodes; import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +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.Saml2AuthenticationInfo; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters; @@ -329,9 +330,14 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { if (registrationId != null) { return registrationId; } - Saml2AuthenticationInfo info = Saml2AuthenticationInfo.fromAuthentication(authentication); - if (info != null) { - return info.getRelyingPartyRegistrationId(); + if (authentication == null) { + return null; + } + if (authentication instanceof Saml2AssertionAuthentication saml2) { + return saml2.getRelyingPartyRegistrationId(); + } + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal saml2) { + return saml2.getRelyingPartyRegistrationId(); } return null; } diff --git a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java index ce8bc94300..aae95e28c7 100644 --- a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java +++ b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java @@ -28,8 +28,8 @@ 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.Saml2AuthenticationInfo; 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; @@ -144,9 +144,11 @@ public final class OpenSamlLogoutRequestValidatorParametersResolver if (registrationId != null) { return registrationId; } - Saml2AuthenticationInfo info = Saml2AuthenticationInfo.fromAuthentication(authentication); - if (info != null) { - return info.getRelyingPartyRegistrationId(); + if (authentication == null) { + return null; + } + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal principal) { + return principal.getRelyingPartyRegistrationId(); } return null; } diff --git a/saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java b/saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java index d2dbb5ba0c..6d49c6ef2f 100644 --- a/saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java @@ -893,14 +893,15 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv Saml2AuthenticationToken token = responseToken.token; Assertion assertion = CollectionUtils.firstElement(response.getAssertions()); String username = this.principalNameConverter.convert(assertion); - Map> attributes = BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion); - List sessionIndexes = BaseOpenSamlAuthenticationProvider.getSessionIndexes(assertion); - DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes, - sessionIndexes); String registrationId = responseToken.token.getRelyingPartyRegistration().getRegistrationId(); - principal.setRelyingPartyRegistrationId(registrationId); - return new Saml2Authentication(principal, token.getSaml2Response(), - this.grantedAuthoritiesConverter.convert(assertion)); + Saml2ResponseAssertionAccessor accessor = Saml2ResponseAssertion.withResponseValue(token.getSaml2Response()) + .nameId(authenticatedPrincipal(assertion)) + .sessionIndexes(BaseOpenSamlAuthenticationProvider.getSessionIndexes(assertion)) + .attributes(BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion)) + .build(); + Saml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, accessor); + Collection authorities = this.grantedAuthoritiesConverter.convert(assertion); + return new Saml2AssertionAuthentication(principal, accessor, authorities, registrationId); } /** diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java index 4ea06059a6..75ab69d108 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java @@ -48,7 +48,8 @@ public class DefaultSaml2AuthenticatedPrincipalTests { @Test public void createDefaultSaml2AuthenticatedPrincipalWhenAttributesNullThenException() { - assertThatIllegalArgumentException().isThrownBy(() -> new DefaultSaml2AuthenticatedPrincipal("user", null)) + assertThatIllegalArgumentException() + .isThrownBy(() -> new DefaultSaml2AuthenticatedPrincipal("user", (Map>) null)) .withMessageContaining("attributes cannot be null"); }