Skip to content

Commit 416859e

Browse files
committed
Use OpenSAML API in authentication.logout
Issue gh-11658
1 parent 94431d1 commit 416859e

18 files changed

+1596
-646
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
import org.springframework.security.core.Authentication;
3232
import org.springframework.security.core.context.SecurityContextHolderStrategy;
3333
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
34-
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator;
35-
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator;
34+
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator;
35+
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator;
3636
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
3737
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
3838
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
@@ -398,7 +398,7 @@ public Saml2LogoutConfigurer<H> and() {
398398

399399
private Saml2LogoutRequestValidator logoutRequestValidator() {
400400
if (this.logoutRequestValidator == null) {
401-
return new OpenSamlLogoutRequestValidator();
401+
return new OpenSaml4LogoutRequestValidator();
402402
}
403403
return this.logoutRequestValidator;
404404
}
@@ -474,7 +474,7 @@ public Saml2LogoutConfigurer<H> and() {
474474

475475
private Saml2LogoutResponseValidator logoutResponseValidator() {
476476
if (this.logoutResponseValidator == null) {
477-
return new OpenSamlLogoutResponseValidator();
477+
return new OpenSaml4LogoutResponseValidator();
478478
}
479479
return this.logoutResponseValidator;
480480
}

saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
apply plugin: 'io.spring.convention.spring-module'
22

3+
sourceSets.configureEach { set ->
4+
if (!set.name.containsIgnoreCase("main")) {
5+
return
6+
}
7+
def from = copySpec {
8+
from("$projectDir/src/$set.name/java/org/springframework/security/saml2/internal")
9+
}
10+
11+
copy {
12+
into "$projectDir/src/$set.name/java/org/springframework/security/saml2/provider/service/authentication/logout"
13+
filter { line -> line.replaceAll(".saml2.internal", ".saml2.provider.service.authentication.logout") }
14+
with from
15+
}
16+
}
17+
318
dependencies {
419
management platform(project(":spring-security-dependencies"))
520
api project(':spring-security-web')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2002-2024 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.provider.service.authentication.logout;
18+
19+
import java.util.Collection;
20+
import java.util.function.Consumer;
21+
22+
import org.opensaml.saml.saml2.core.LogoutRequest;
23+
import org.opensaml.saml.saml2.core.NameID;
24+
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
27+
import org.springframework.security.saml2.core.Saml2Error;
28+
import org.springframework.security.saml2.core.Saml2ErrorCodes;
29+
import org.springframework.security.saml2.core.Saml2X509Credential;
30+
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer;
31+
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters;
32+
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
33+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
34+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
35+
36+
class BaseOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
37+
38+
static {
39+
OpenSamlInitializationService.initialize();
40+
}
41+
42+
private final OpenSamlOperations saml;
43+
44+
BaseOpenSamlLogoutRequestValidator(OpenSamlOperations saml) {
45+
this.saml = saml;
46+
}
47+
48+
/**
49+
* {@inheritDoc}
50+
*/
51+
@Override
52+
public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) {
53+
Saml2LogoutRequest request = parameters.getLogoutRequest();
54+
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
55+
Authentication authentication = parameters.getAuthentication();
56+
LogoutRequest logoutRequest = this.saml.deserialize(Saml2Utils.withEncoded(request.getSamlRequest())
57+
.inflate(request.getBinding() == Saml2MessageBinding.REDIRECT)
58+
.decode());
59+
return Saml2LogoutValidatorResult.withErrors()
60+
.errors(verifySignature(request, logoutRequest, registration))
61+
.errors(validateRequest(logoutRequest, registration, authentication))
62+
.build();
63+
}
64+
65+
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest,
66+
RelyingPartyRegistration registration) {
67+
AssertingPartyMetadata details = registration.getAssertingPartyMetadata();
68+
Collection<Saml2X509Credential> credentials = details.getVerificationX509Credentials();
69+
VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId());
70+
return (errors) -> {
71+
if (logoutRequest.isSigned()) {
72+
errors.addAll(verify.verify(logoutRequest));
73+
}
74+
else {
75+
RedirectParameters params = new RedirectParameters(request.getParameters(),
76+
request.getParametersQuery(), logoutRequest);
77+
errors.addAll(verify.verify(params));
78+
}
79+
};
80+
}
81+
82+
private Consumer<Collection<Saml2Error>> validateRequest(LogoutRequest request,
83+
RelyingPartyRegistration registration, Authentication authentication) {
84+
return (errors) -> {
85+
validateIssuer(request, registration).accept(errors);
86+
validateDestination(request, registration).accept(errors);
87+
validateSubject(request, registration, authentication).accept(errors);
88+
};
89+
}
90+
91+
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutRequest request,
92+
RelyingPartyRegistration registration) {
93+
return (errors) -> {
94+
if (request.getIssuer() == null) {
95+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutRequest"));
96+
return;
97+
}
98+
String issuer = request.getIssuer().getValue();
99+
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
100+
errors
101+
.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
102+
}
103+
};
104+
}
105+
106+
private Consumer<Collection<Saml2Error>> validateDestination(LogoutRequest request,
107+
RelyingPartyRegistration registration) {
108+
return (errors) -> {
109+
if (request.getDestination() == null) {
110+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
111+
"Failed to find destination in LogoutRequest"));
112+
return;
113+
}
114+
String destination = request.getDestination();
115+
if (!destination.equals(registration.getSingleLogoutServiceLocation())) {
116+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
117+
"Failed to match destination to configured destination"));
118+
}
119+
};
120+
}
121+
122+
private Consumer<Collection<Saml2Error>> validateSubject(LogoutRequest request,
123+
RelyingPartyRegistration registration, Authentication authentication) {
124+
return (errors) -> {
125+
if (authentication == null) {
126+
return;
127+
}
128+
NameID nameId = getNameId(request, registration);
129+
if (nameId == null) {
130+
errors
131+
.add(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest"));
132+
return;
133+
}
134+
135+
validateNameId(nameId, authentication, errors);
136+
};
137+
}
138+
139+
private NameID getNameId(LogoutRequest request, RelyingPartyRegistration registration) {
140+
this.saml.withDecryptionKeys(registration.getDecryptionX509Credentials()).decrypt(request);
141+
return request.getNameID();
142+
}
143+
144+
private void validateNameId(NameID nameId, Authentication authentication, Collection<Saml2Error> errors) {
145+
String name = nameId.getValue();
146+
if (!name.equals(authentication.getName())) {
147+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST,
148+
"Failed to match subject in LogoutRequest with currently logged in user"));
149+
}
150+
}
151+
152+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2002-2024 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.provider.service.authentication.logout;
18+
19+
import java.util.Collection;
20+
import java.util.function.Consumer;
21+
22+
import org.opensaml.saml.saml2.core.LogoutResponse;
23+
import org.opensaml.saml.saml2.core.StatusCode;
24+
25+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
26+
import org.springframework.security.saml2.core.Saml2Error;
27+
import org.springframework.security.saml2.core.Saml2ErrorCodes;
28+
import org.springframework.security.saml2.core.Saml2X509Credential;
29+
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer;
30+
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters;
31+
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
32+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
33+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
34+
35+
class BaseOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
36+
37+
static {
38+
OpenSamlInitializationService.initialize();
39+
}
40+
41+
private final OpenSamlOperations saml;
42+
43+
BaseOpenSamlLogoutResponseValidator(OpenSamlOperations saml) {
44+
this.saml = saml;
45+
}
46+
47+
@Override
48+
public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) {
49+
Saml2LogoutResponse response = parameters.getLogoutResponse();
50+
Saml2LogoutRequest request = parameters.getLogoutRequest();
51+
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
52+
LogoutResponse logoutResponse = this.saml.deserialize(Saml2Utils.withEncoded(response.getSamlResponse())
53+
.inflate(response.getBinding() == Saml2MessageBinding.REDIRECT)
54+
.decode());
55+
return Saml2LogoutValidatorResult.withErrors()
56+
.errors(verifySignature(response, logoutResponse, registration))
57+
.errors(validateRequest(logoutResponse, registration))
58+
.errors(validateLogoutRequest(logoutResponse, request.getId()))
59+
.build();
60+
}
61+
62+
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutResponse response,
63+
LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
64+
return (errors) -> {
65+
AssertingPartyMetadata details = registration.getAssertingPartyMetadata();
66+
Collection<Saml2X509Credential> credentials = details.getVerificationX509Credentials();
67+
VerificationConfigurer verify = this.saml.withVerificationKeys(credentials)
68+
.entityId(details.getEntityId())
69+
.entityId(details.getEntityId());
70+
if (logoutResponse.isSigned()) {
71+
errors.addAll(verify.verify(logoutResponse));
72+
}
73+
else {
74+
RedirectParameters params = new RedirectParameters(response.getParameters(),
75+
response.getParametersQuery(), logoutResponse);
76+
errors.addAll(verify.verify(params));
77+
}
78+
};
79+
}
80+
81+
private Consumer<Collection<Saml2Error>> validateRequest(LogoutResponse response,
82+
RelyingPartyRegistration registration) {
83+
return (errors) -> {
84+
validateIssuer(response, registration).accept(errors);
85+
validateDestination(response, registration).accept(errors);
86+
validateStatus(response).accept(errors);
87+
};
88+
}
89+
90+
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutResponse response,
91+
RelyingPartyRegistration registration) {
92+
return (errors) -> {
93+
if (response.getIssuer() == null) {
94+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
95+
return;
96+
}
97+
String issuer = response.getIssuer().getValue();
98+
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
99+
errors
100+
.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
101+
}
102+
};
103+
}
104+
105+
private Consumer<Collection<Saml2Error>> validateDestination(LogoutResponse response,
106+
RelyingPartyRegistration registration) {
107+
return (errors) -> {
108+
if (response.getDestination() == null) {
109+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
110+
"Failed to find destination in LogoutResponse"));
111+
return;
112+
}
113+
String destination = response.getDestination();
114+
if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) {
115+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
116+
"Failed to match destination to configured destination"));
117+
}
118+
};
119+
}
120+
121+
private Consumer<Collection<Saml2Error>> validateStatus(LogoutResponse response) {
122+
return (errors) -> {
123+
if (response.getStatus() == null) {
124+
return;
125+
}
126+
if (response.getStatus().getStatusCode() == null) {
127+
return;
128+
}
129+
if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) {
130+
return;
131+
}
132+
if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) {
133+
return;
134+
}
135+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed"));
136+
};
137+
}
138+
139+
private Consumer<Collection<Saml2Error>> validateLogoutRequest(LogoutResponse response, String id) {
140+
return (errors) -> {
141+
if (response.getInResponseTo() == null) {
142+
return;
143+
}
144+
if (response.getInResponseTo().equals(id)) {
145+
return;
146+
}
147+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE,
148+
"LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest"));
149+
};
150+
}
151+
152+
}

0 commit comments

Comments
 (0)