Parcourir la source

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
Josh Cummings il y a 2 mois
Parent
commit
9b724377ce
25 fichiers modifiés avec 558 ajouts et 136 suppressions
  1. 13 2
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java
  2. 13 2
      config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java
  3. 13 2
      config/src/test/java/org/springframework/security/SerializationSamples.java
  4. BIN
      config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication.serialized
  5. BIN
      config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion.serialized
  6. 54 0
      docs/modules/ROOT/pages/migration/servlet/saml2.adoc
  7. 6 2
      docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc
  8. 59 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AssertionAuthenticationMixin.java
  9. 4 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java
  10. 56 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/SimpleSaml2ResponseAssertionAccessorMixin.java
  11. 8 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java
  12. 65 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java
  13. 5 8
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java
  14. 9 1
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java
  15. 0 85
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationInfo.java
  16. 115 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertion.java
  17. 65 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ResponseAssertionAccessor.java
  18. 4 2
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java
  19. 23 8
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestResolver.java
  20. 10 4
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutRequestValidatorParametersResolver.java
  21. 10 4
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutResponseResolver.java
  22. 10 4
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java
  23. 6 4
      saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java
  24. 8 7
      saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java
  25. 2 1
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java

+ 13 - 2
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<H extends HttpSecurityBuilder<H>>
 		@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;
 		}
 
 	}

+ 13 - 2
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) {

+ 13 - 2
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,

BIN
config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication.serialized


BIN
config/src/test/resources/serialized/7.0.x/org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion.serialized


+ 54 - 0
docs/modules/ROOT/pages/migration/servlet/saml2.adoc

@@ -54,3 +54,57 @@ fun logoutResponseResolver(registrations: RelyingPartyRegistrationRepository?):
 ----
 ======
 
+== Favor `Saml2ResponseAuthenticationAccessor` over `Saml2AuthenticatedPrincipal`
+
+Spring Security 7 separates `<saml2:Assertion>` 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.

+ 6 - 2
docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc

@@ -341,8 +341,10 @@ class MyUserDetailsResponseAuthenticationConverter implements Converter<Response
 	    Saml2Authentication authentication = this.delegate.convert(responseToken); <1>
 		UserDetails principal = this.userDetailsService.loadByUsername(username); <2>
 		String saml2Response = authentication.getSaml2Response();
+		Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor(
+				saml2Response, CollectionUtils.getFirst(response.getAssertions()));
 		Collection<GrantedAuthority> 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>
     }
 
 }

+ 59 - 0
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}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * </pre>
+ *
+ * @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<? extends GrantedAuthority> authorities,
+			@JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) {
+	}
+
+}

+ 4 - 0
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);

+ 56 - 0
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}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * </pre>
+ *
+ * @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<String> sessionIndexes,
+			@JsonProperty("attributes") Map<String, List<Object>> attributes) {
+	}
+
+}

+ 8 - 0
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;

+ 65 - 0
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<? extends GrantedAuthority> authorities, String relyingPartyRegistrationId) {
+		super(assertion, assertion.getResponseValue(), authorities);
+		this.assertion = assertion;
+		this.relyingPartyRegistrationId = relyingPartyRegistrationId;
+	}
+
+	public Saml2AssertionAuthentication(Object principal, Saml2ResponseAssertionAccessor assertion,
+			Collection<? extends GrantedAuthority> 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;
+	}
+
+}

+ 5 - 8
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<String> getSessionIndexes() {
 		return Collections.emptyList();
 	}

+ 9 - 1
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<? extends GrantedAuthority> authorities) {
+		super(authorities);
+		this.principal = principal;
+		this.saml2Response = saml2Response;
+		setAuthenticated(true);
+	}
+
 	@Override
 	public Object getPrincipal() {
 		return this.principal;

+ 0 - 85
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationInfo.java

@@ -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
- *
- * <p>
- * 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<String> getSessionIndexes();
-
-	/**
-	 * Try to obtain a {@link Saml2AuthenticationInfo} instance from an
-	 * {@link Authentication}
-	 *
-	 * <p>
-	 * The result is either the {@link Authentication#getPrincipal() authenticated
-	 * principal}, the {@link Authentication} itself, or {@code null}.
-	 *
-	 * <p>
-	 * 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;
-	}
-
-}

+ 115 - 0
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<String> sessionIndexes;
+
+	private final Map<String, List<Object>> attributes;
+
+	Saml2ResponseAssertion(String responseValue, String nameId, List<String> sessionIndexes,
+			Map<String, List<Object>> 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<String> getSessionIndexes() {
+		return this.sessionIndexes;
+	}
+
+	@Override
+	public Map<String, List<Object>> 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<String> sessionIndexes = List.of();
+
+		private Map<String, List<Object>> attributes = Map.of();
+
+		Builder(String responseValue) {
+			this.responseValue = responseValue;
+		}
+
+		public Builder nameId(String nameId) {
+			this.nameId = nameId;
+			return this;
+		}
+
+		public Builder sessionIndexes(List<String> sessionIndexes) {
+			this.sessionIndexes = sessionIndexes;
+			return this;
+		}
+
+		public Builder attributes(Map<String, List<Object>> attributes) {
+			this.attributes = attributes;
+			return this;
+		}
+
+		public Saml2ResponseAssertion build() {
+			return new Saml2ResponseAssertion(this.responseValue, this.nameId, this.sessionIndexes, this.attributes);
+		}
+
+	}
+
+}

+ 65 - 0
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<String> getSessionIndexes();
+
+	/**
+	 * Get the first value of Saml2 token attribute by name
+	 * @param name the name of the attribute
+	 * @param <A> the type of the attribute
+	 * @return the first attribute value or {@code null} otherwise
+	 */
+	@Nullable default <A> A getFirstAttribute(String name) {
+		List<A> values = getAttribute(name);
+		return CollectionUtils.firstElement(values);
+	}
+
+	/**
+	 * Get the Saml2 token attribute by name
+	 * @param name the name of the attribute
+	 * @param <A> the type of the attribute
+	 * @return the attribute or {@code null} otherwise
+	 */
+	@Nullable default <A> List<A> getAttribute(String name) {
+		return (List<A>) getAttributes().get(name);
+	}
+
+	Map<String, List<Object>> getAttributes();
+
+	String getResponseValue();
+
+}

+ 4 - 2
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<Saml2Error> 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"));
 		}

+ 23 - 8
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;
 	}

+ 10 - 4
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;
 	}

+ 10 - 4
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;
 	}

+ 10 - 4
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;
 		}

+ 6 - 4
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;
 	}

+ 8 - 7
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<String, List<Object>> attributes = BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion);
-			List<String> 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<GrantedAuthority> authorities = this.grantedAuthoritiesConverter.convert(assertion);
+			return new Saml2AssertionAuthentication(principal, accessor, authorities, registrationId);
 		}
 
 		/**

+ 2 - 1
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<String, List<Object>>) null))
 			.withMessageContaining("attributes cannot be null");
 	}