Skip to content

Commit de62adc

Browse files
committed
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
1 parent 5a6d5fa commit de62adc

File tree

25 files changed

+578
-137
lines changed

25 files changed

+578
-137
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
3434
import org.springframework.security.core.Authentication;
3535
import org.springframework.security.core.context.SecurityContextHolderStrategy;
36-
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationInfo;
36+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
37+
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
38+
import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor;
3739
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator;
3840
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator;
3941
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator;
@@ -531,7 +533,16 @@ private static class Saml2RequestMatcher implements RequestMatcher {
531533
@Override
532534
public boolean matches(HttpServletRequest request) {
533535
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
534-
return Saml2AuthenticationInfo.fromAuthentication(authentication) != null;
536+
if (authentication == null) {
537+
return false;
538+
}
539+
if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) {
540+
return true;
541+
}
542+
if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor) {
543+
return true;
544+
}
545+
return authentication instanceof Saml2Authentication;
535546
}
536547

537548
}

config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import org.springframework.security.core.Authentication;
3232
import org.springframework.security.core.context.SecurityContextHolder;
3333
import org.springframework.security.core.context.SecurityContextHolderStrategy;
34-
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationInfo;
34+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
35+
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
36+
import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor;
3537
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
3638
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
3739
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
@@ -236,7 +238,16 @@ public static class Saml2RequestMatcher implements RequestMatcher {
236238
@Override
237239
public boolean matches(HttpServletRequest request) {
238240
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
239-
return Saml2AuthenticationInfo.fromAuthentication(authentication) != null;
241+
if (authentication == null) {
242+
return false;
243+
}
244+
if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) {
245+
return true;
246+
}
247+
if (authentication.getCredentials() instanceof Saml2ResponseAssertionAccessor) {
248+
return true;
249+
}
250+
return authentication instanceof Saml2Authentication;
240251
}
241252

242253
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {

config/src/test/java/org/springframework/security/SerializationSamples.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,15 @@
170170
import org.springframework.security.saml2.core.Saml2X509Credential;
171171
import org.springframework.security.saml2.credentials.TestSaml2X509Credentials;
172172
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
173+
import org.springframework.security.saml2.provider.service.authentication.OpenSamlResponseAssertionAccessor;
174+
import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication;
173175
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
174176
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
175177
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
176178
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
177179
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
180+
import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor;
181+
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
178182
import org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationTokens;
179183
import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications;
180184
import org.springframework.security.saml2.provider.service.authentication.TestSaml2LogoutRequests;
@@ -520,8 +524,13 @@ final class SerializationSamples {
520524
generatorByClassName.put(Saml2Exception.class, (r) -> new Saml2Exception("message", new IOException("fail")));
521525
generatorByClassName.put(DefaultSaml2AuthenticatedPrincipal.class,
522526
(r) -> TestSaml2Authentications.authentication().getPrincipal());
523-
generatorByClassName.put(Saml2Authentication.class,
524-
(r) -> applyDetails(TestSaml2Authentications.authentication()));
527+
Saml2Authentication saml2 = TestSaml2Authentications.authentication();
528+
generatorByClassName.put(Saml2Authentication.class, (r) -> applyDetails(saml2));
529+
Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor("response",
530+
TestOpenSamlObjects.assertion());
531+
generatorByClassName.put(OpenSamlResponseAssertionAccessor.class, (r) -> assertion);
532+
generatorByClassName.put(Saml2AssertionAuthentication.class, (r) -> applyDetails(
533+
new Saml2AssertionAuthentication(assertion, authentication.getAuthorities(), "id")));
525534
generatorByClassName.put(Saml2PostAuthenticationRequest.class,
526535
(r) -> TestSaml2PostAuthenticationRequests.create());
527536
generatorByClassName.put(Saml2RedirectAuthenticationRequest.class,

docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,10 @@ class MyUserDetailsResponseAuthenticationConverter implements Converter<Response
341341
Saml2Authentication authentication = this.delegate.convert(responseToken); <1>
342342
UserDetails principal = this.userDetailsService.loadByUsername(username); <2>
343343
String saml2Response = authentication.getSaml2Response();
344+
Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor(
345+
saml2Response, CollectionUtils.getFirst(response.getAssertions()));
344346
Collection<GrantedAuthority> authorities = principal.getAuthorities();
345-
return new Saml2Authentication((AuthenticatedPrincipal) userDetails, saml2Response, authorities); <3>
347+
return new Saml2AssertionAuthentication(userDetails, assertion, authorities); <3>
346348
}
347349
348350
}
@@ -361,8 +363,10 @@ open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAu
361363
val authentication = this.delegate.convert(responseToken) <1>
362364
val principal = this.userDetailsService.loadByUsername(username) <2>
363365
val saml2Response = authentication.getSaml2Response()
366+
val assertion = OpenSamlResponseAssertionAccessor(
367+
saml2Response, CollectionUtils.getFirst(response.getAssertions()))
364368
val authorities = principal.getAuthorities()
365-
return Saml2Authentication(userDetails as AuthenticatedPrincipal, saml2Response, authorities) <3>
369+
return Saml2AssertionAuthentication(userDetails, assertion, authorities) <3>
366370
}
367371
368372
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.jackson2;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
23+
import com.fasterxml.jackson.annotation.JsonCreator;
24+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
25+
import com.fasterxml.jackson.annotation.JsonProperty;
26+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
27+
28+
import org.springframework.security.jackson2.SecurityJackson2Modules;
29+
import org.springframework.security.saml2.provider.service.authentication.OpenSamlResponseAssertionAccessor;
30+
import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor;
31+
32+
/**
33+
* Jackson Mixin class helps in serialize/deserialize
34+
* {@link OpenSamlResponseAssertionAccessor}.
35+
*
36+
* <pre>
37+
* ObjectMapper mapper = new ObjectMapper();
38+
* mapper.registerModule(new Saml2Jackson2Module());
39+
* </pre>
40+
*
41+
* @author Josh Cummings
42+
* @since 7.0
43+
* @see Saml2Jackson2Module
44+
* @see SecurityJackson2Modules
45+
*/
46+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
47+
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
48+
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
49+
@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true)
50+
class OpenSamlResponseAssertionAccessorMixin {
51+
52+
@JsonCreator
53+
OpenSamlResponseAssertionAccessorMixin(@JsonProperty("responseValue") String responseValue,
54+
@JsonProperty("nameId") String nameId, @JsonProperty("sessionIndexes") List<String> sessionIndexes,
55+
@JsonProperty("attributes") Map<String, List<Object>> attributes) {
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.jackson2;
18+
19+
import java.util.Collection;
20+
21+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
22+
import com.fasterxml.jackson.annotation.JsonCreator;
23+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
24+
import com.fasterxml.jackson.annotation.JsonProperty;
25+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
26+
27+
import org.springframework.security.core.GrantedAuthority;
28+
import org.springframework.security.jackson2.SecurityJackson2Modules;
29+
import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication;
30+
import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor;
31+
32+
/**
33+
* Jackson Mixin class helps in serialize/deserialize
34+
* {@link Saml2AssertionAuthentication}.
35+
*
36+
* <pre>
37+
* ObjectMapper mapper = new ObjectMapper();
38+
* mapper.registerModule(new Saml2Jackson2Module());
39+
* </pre>
40+
*
41+
* @author Josh Cummings
42+
* @since 7.0
43+
* @see Saml2Jackson2Module
44+
* @see SecurityJackson2Modules
45+
*/
46+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
47+
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
48+
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
49+
@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true)
50+
class Saml2AssertionAuthenticationMixin {
51+
52+
@JsonCreator
53+
Saml2AssertionAuthenticationMixin(@JsonProperty("principal") Object principal,
54+
@JsonProperty("assertion") Saml2ResponseAssertionAccessor assertion,
55+
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
56+
@JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) {
57+
}
58+
59+
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import org.springframework.security.jackson2.SecurityJackson2Modules;
2323
import org.springframework.security.saml2.core.Saml2Error;
2424
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
25+
import org.springframework.security.saml2.provider.service.authentication.OpenSamlResponseAssertionAccessor;
26+
import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication;
2527
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
2628
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
2729
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
@@ -49,6 +51,9 @@ public Saml2Jackson2Module() {
4951
@Override
5052
public void setupModule(SetupContext context) {
5153
context.setMixInAnnotations(Saml2Authentication.class, Saml2AuthenticationMixin.class);
54+
context.setMixInAnnotations(Saml2AssertionAuthentication.class, Saml2AssertionAuthenticationMixin.class);
55+
context.setMixInAnnotations(OpenSamlResponseAssertionAccessor.class,
56+
OpenSamlResponseAssertionAccessorMixin.class);
5257
context.setMixInAnnotations(DefaultSaml2AuthenticatedPrincipal.class,
5358
DefaultSaml2AuthenticatedPrincipalMixin.class);
5459
context.setMixInAnnotations(Saml2LogoutRequest.class, Saml2LogoutRequestMixin.class);

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,24 @@
3030
*
3131
* @author Clement Stoquart
3232
* @since 5.4
33+
* @deprecated Please use {@link Saml2ResponseAssertionAccessor}
3334
*/
35+
@Deprecated
3436
public class DefaultSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPrincipal, Serializable {
3537

3638
@Serial
3739
private static final long serialVersionUID = -7601324133433139825L;
3840

3941
private final String name;
4042

43+
private final String nameId;
44+
4145
private final Map<String, List<Object>> attributes;
4246

4347
private final List<String> sessionIndexes;
4448

49+
private final String responseValue;
50+
4551
private String registrationId;
4652

4753
public DefaultSaml2AuthenticatedPrincipal(String name, Map<String, List<Object>> attributes) {
@@ -54,25 +60,50 @@ public DefaultSaml2AuthenticatedPrincipal(String name, Map<String, List<Object>>
5460
Assert.notNull(attributes, "attributes cannot be null");
5561
Assert.notNull(sessionIndexes, "sessionIndexes cannot be null");
5662
this.name = name;
63+
this.nameId = name;
5764
this.attributes = attributes;
5865
this.sessionIndexes = sessionIndexes;
66+
this.responseValue = null;
67+
}
68+
69+
public DefaultSaml2AuthenticatedPrincipal(String name, Saml2ResponseAssertionAccessor assertion) {
70+
this.name = name;
71+
this.nameId = assertion.getNameId();
72+
this.attributes = assertion.getAttributes();
73+
this.sessionIndexes = assertion.getSessionIndexes();
74+
this.responseValue = assertion.getResponseValue();
5975
}
6076

6177
@Override
6278
public String getName() {
6379
return this.name;
6480
}
6581

82+
@Override
83+
public String getNameId() {
84+
return this.nameId;
85+
}
86+
6687
@Override
6788
public Map<String, List<Object>> getAttributes() {
6889
return this.attributes;
6990
}
7091

92+
@Override
93+
public String getResponseValue() {
94+
return this.responseValue;
95+
}
96+
7197
@Override
7298
public List<String> getSessionIndexes() {
7399
return this.sessionIndexes;
74100
}
75101

102+
/**
103+
* @deprecated Please use
104+
* {@link Saml2AssertionAuthentication#getRelyingPartyRegistrationId} instead
105+
*/
106+
@Deprecated
76107
@Override
77108
public String getRelyingPartyRegistrationId() {
78109
return this.registrationId;

0 commit comments

Comments
 (0)