Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
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;
Expand Down Expand Up @@ -534,7 +536,13 @@ public boolean matches(HttpServletRequest request) {
if (authentication == null) {
return false;
}
return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal;
if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) {
return true;
}
if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor) {
return true;
}
return authentication instanceof Saml2Authentication;
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
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;
Expand Down Expand Up @@ -239,7 +241,13 @@ public boolean matches(HttpServletRequest request) {
if (authentication == null) {
return false;
}
return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal;
if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) {
return true;
}
if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor) {
return true;
}
return authentication instanceof Saml2Authentication;
}

public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,14 @@
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;
Expand Down Expand Up @@ -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,
Expand Down
Binary file not shown.
Binary file not shown.
54 changes: 54 additions & 0 deletions docs/modules/ROOT/pages/migration/servlet/saml2.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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>
}
}
Expand All @@ -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>
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
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;

/**
Expand All @@ -49,6 +51,8 @@ public Saml2Jackson2Module() {
@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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
*
* @author Clement Stoquart
* @since 5.4
* @deprecated Please use {@link Saml2ResponseAssertionAccessor}
*/
@Deprecated
public class DefaultSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPrincipal, Serializable {

@Serial
Expand Down Expand Up @@ -58,6 +60,12 @@ public DefaultSaml2AuthenticatedPrincipal(String name, Map<String, List<Object>>
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Loading