Skip to content

Commit dbdf04f

Browse files
committed
SAML Response Reads EntityId
Closes gh-10243
1 parent 3f2816f commit dbdf04f

File tree

4 files changed

+450
-52
lines changed

4 files changed

+450
-52
lines changed

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

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,11 @@ You can configure this in a number of ways including:
1313

1414
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
1515

16-
[[relyingpartyregistrationresolver-apply]]
17-
== Changing `RelyingPartyRegistration` Lookup
18-
19-
`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`].
20-
21-
To apply a `RelyingPartyRegistrationResolver` when processing `<saml2:Response>` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so:
22-
23-
====
24-
.Java
25-
[source,java,role="primary"]
26-
----
27-
@Bean
28-
Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) {
29-
return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations));
30-
}
31-
----
32-
33-
.Kotlin
34-
[source,kotlin,role="secondary"]
35-
----
36-
@Bean
37-
fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter {
38-
return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations));
39-
}
40-
----
41-
====
16+
[[saml2-response-processing-endpoint]]
17+
== Changing the SAML Response Processing Endpoint
4218

43-
Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default.
44-
If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata:
19+
The default endpoint is `+/login/saml2/sso/{registrationId}+`.
20+
You can change this in the DSL and in the associated metadata like so:
4521

4622
====
4723
.Java
@@ -82,13 +58,55 @@ and:
8258
.Java
8359
[source,java,role="primary"]
8460
----
85-
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
61+
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
62+
----
63+
64+
.Kotlin
65+
[source,kotlin,role="secondary"]
66+
----
67+
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
68+
----
69+
====
70+
71+
[[relyingpartyregistrationresolver-apply]]
72+
== Changing `RelyingPartyRegistration` lookup
73+
74+
By default, this converter will match against any associated `<saml2:AuthnRequest>` or any `registrationId` it finds in the URL.
75+
Or, if it cannot find one in either of those cases, then it attempts to look it up by the `<saml2:Response#Issuer>` element.
76+
77+
There are a number of circumstances where you might need something more sophisticated, like if you are supporting `ARTIFACT` binding.
78+
In those cases, you can customize lookup through a custom `AuthenticationConverter`, which you can customize like so:
79+
80+
====
81+
.Java
82+
[source,java,role="primary"]
83+
----
84+
@Bean
85+
SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
86+
http
87+
// ...
88+
.saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
89+
// ...
90+
91+
return http.build();
92+
}
8693
----
8794
8895
.Kotlin
8996
[source,kotlin,role="secondary"]
9097
----
91-
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
98+
@Bean
99+
fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain {
100+
http {
101+
// ...
102+
.saml2Login {
103+
authenticationConverter = converter
104+
}
105+
// ...
106+
}
107+
108+
return http.build()
109+
}
92110
----
93111
====
94112

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverter.java

Lines changed: 142 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.saml2.provider.service.web;
1818

19+
import java.io.ByteArrayInputStream;
1920
import java.io.ByteArrayOutputStream;
2021
import java.nio.charset.StandardCharsets;
2122
import java.util.Arrays;
@@ -25,16 +26,31 @@
2526
import java.util.zip.InflaterOutputStream;
2627

2728
import jakarta.servlet.http.HttpServletRequest;
29+
import net.shibboleth.utilities.java.support.xml.ParserPool;
30+
import org.opensaml.core.config.ConfigurationService;
31+
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
32+
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
33+
import org.opensaml.saml.saml2.core.Response;
34+
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
35+
import org.w3c.dom.Document;
36+
import org.w3c.dom.Element;
2837

2938
import org.springframework.http.HttpMethod;
39+
import org.springframework.security.saml2.Saml2Exception;
40+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
3041
import org.springframework.security.saml2.core.Saml2Error;
3142
import org.springframework.security.saml2.core.Saml2ErrorCodes;
3243
import org.springframework.security.saml2.core.Saml2ParameterNames;
3344
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
3445
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
3546
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
3647
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
48+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
49+
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
3750
import org.springframework.security.web.authentication.AuthenticationConverter;
51+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
52+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
53+
import org.springframework.security.web.util.matcher.RequestMatcher;
3854
import org.springframework.util.Assert;
3955

4056
/**
@@ -43,49 +59,134 @@
4359
* {@link org.springframework.security.authentication.AuthenticationManager}.
4460
*
4561
* @author Josh Cummings
46-
* @since 5.4
62+
* @since 6.1
4763
*/
48-
public final class Saml2AuthenticationTokenConverter implements AuthenticationConverter {
64+
public final class OpenSamlAuthenticationTokenConverter implements AuthenticationConverter {
65+
66+
static {
67+
OpenSamlInitializationService.initialize();
68+
}
4969

5070
// MimeDecoder allows extra line-breaks as well as other non-alphabet values.
5171
// This matches the behaviour of the commons-codec decoder.
5272
private static final Base64.Decoder BASE64 = Base64.getMimeDecoder();
5373

5474
private static final Base64Checker BASE_64_CHECKER = new Base64Checker();
5575

56-
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
76+
private final RelyingPartyRegistrationRepository registrations;
77+
78+
private RequestMatcher requestMatcher = new OrRequestMatcher(
79+
new AntPathRequestMatcher("/login/saml2/sso/{registrationId}"),
80+
new AntPathRequestMatcher("/login/saml2/sso"));
81+
82+
private final ParserPool parserPool;
83+
84+
private final ResponseUnmarshaller unmarshaller;
5785

5886
private Function<HttpServletRequest, AbstractSaml2AuthenticationRequest> loader;
5987

6088
/**
61-
* Constructs a {@link Saml2AuthenticationTokenConverter} given a strategy for
62-
* resolving {@link RelyingPartyRegistration}s
63-
* @param relyingPartyRegistrationResolver the strategy for resolving
89+
* Constructs a {@link OpenSamlAuthenticationTokenConverter} given a repository for
90+
* {@link RelyingPartyRegistration}s
91+
* @param registrations the repository for {@link RelyingPartyRegistration}s
6492
* {@link RelyingPartyRegistration}s
6593
*/
66-
public Saml2AuthenticationTokenConverter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
67-
Assert.notNull(relyingPartyRegistrationResolver, "relyingPartyRegistrationResolver cannot be null");
68-
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
94+
public OpenSamlAuthenticationTokenConverter(RelyingPartyRegistrationRepository registrations) {
95+
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
96+
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
97+
this.parserPool = registry.getParserPool();
98+
this.unmarshaller = (ResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
99+
.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME);
100+
this.registrations = registrations;
69101
this.loader = new HttpSessionSaml2AuthenticationRequestRepository()::loadAuthenticationRequest;
70102
}
71103

104+
/**
105+
* Resolve an authentication request from the given {@link HttpServletRequest}.
106+
*
107+
* <p>
108+
* First uses the configured {@link RequestMatcher} to deduce whether an
109+
* authentication request is being made and optionally for which
110+
* {@code registrationId}.
111+
*
112+
* <p>
113+
* If there is an associated {@code <saml2:AuthnRequest>}, then the
114+
* {@code registrationId} is looked up and used.
115+
*
116+
* <p>
117+
* If a {@code registrationId} is found in the request, then it is looked up and used.
118+
* In that case, if none is found a {@link Saml2AuthenticationException} is thrown.
119+
*
120+
* <p>
121+
* Finally, if no {@code registrationId} is found in the request, then the code
122+
* attempts to resolve the {@link RelyingPartyRegistration} from the SAML Response's
123+
* Issuer.
124+
* @param request the HTTP request
125+
* @return the {@link Saml2AuthenticationToken} authentication request
126+
* @throws Saml2AuthenticationException if the {@link RequestMatcher} specifies a
127+
* non-existent {@code registrationId}
128+
*/
72129
@Override
73130
public Saml2AuthenticationToken convert(HttpServletRequest request) {
131+
String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
132+
if (serialized == null) {
133+
return null;
134+
}
135+
RequestMatcher.MatchResult result = this.requestMatcher.matcher(request);
136+
if (!result.isMatch()) {
137+
return null;
138+
}
139+
Saml2AuthenticationToken token = tokenByAuthenticationRequest(request);
140+
if (token == null) {
141+
token = tokenByRegistrationId(request, result);
142+
}
143+
if (token == null) {
144+
token = tokenByEntityId(request);
145+
}
146+
return token;
147+
}
148+
149+
private Saml2AuthenticationToken tokenByAuthenticationRequest(HttpServletRequest request) {
74150
AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request);
75-
String relyingPartyRegistrationId = (authenticationRequest != null)
76-
? authenticationRequest.getRelyingPartyRegistrationId() : null;
77-
RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationResolver.resolve(request,
78-
relyingPartyRegistrationId);
79-
if (relyingPartyRegistration == null) {
151+
if (authenticationRequest == null) {
152+
return null;
153+
}
154+
String registrationId = authenticationRequest.getRelyingPartyRegistrationId();
155+
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
156+
return tokenByRegistration(request, registration, authenticationRequest);
157+
}
158+
159+
private Saml2AuthenticationToken tokenByRegistrationId(HttpServletRequest request,
160+
RequestMatcher.MatchResult result) {
161+
String registrationId = result.getVariables().get("registrationId");
162+
if (registrationId == null) {
80163
return null;
81164
}
82-
String saml2Response = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
83-
if (saml2Response == null) {
165+
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
166+
return tokenByRegistration(request, registration, null);
167+
}
168+
169+
private Saml2AuthenticationToken tokenByEntityId(HttpServletRequest request) {
170+
String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
171+
String decoded = new String(samlDecode(serialized), StandardCharsets.UTF_8);
172+
Response response = parse(decoded);
173+
String issuer = response.getIssuer().getValue();
174+
RelyingPartyRegistration registration = this.registrations.findUniqueByAssertingPartyEntityId(issuer);
175+
return tokenByRegistration(request, registration, null);
176+
}
177+
178+
private Saml2AuthenticationToken tokenByRegistration(HttpServletRequest request,
179+
RelyingPartyRegistration registration, AbstractSaml2AuthenticationRequest authenticationRequest) {
180+
if (registration == null) {
84181
return null;
85182
}
86-
byte[] b = samlDecode(saml2Response);
87-
saml2Response = inflateIfRequired(request, b);
88-
return new Saml2AuthenticationToken(relyingPartyRegistration, saml2Response, authenticationRequest);
183+
String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
184+
String decoded = inflateIfRequired(request, samlDecode(serialized));
185+
UriResolver resolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
186+
registration = registration.mutate().entityId(resolver.resolve(registration.getEntityId()))
187+
.assertionConsumerServiceLocation(resolver.resolve(registration.getAssertionConsumerServiceLocation()))
188+
.build();
189+
return new Saml2AuthenticationToken(registration, decoded, authenticationRequest);
89190
}
90191

91192
/**
@@ -100,6 +201,15 @@ public void setAuthenticationRequestRepository(
100201
this.loader = authenticationRequestRepository::loadAuthenticationRequest;
101202
}
102203

204+
/**
205+
* Use the given {@link RequestMatcher} to match the request.
206+
* @param requestMatcher the {@link RequestMatcher} to use
207+
*/
208+
public void setRequestMatcher(RequestMatcher requestMatcher) {
209+
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
210+
this.requestMatcher = requestMatcher;
211+
}
212+
103213
private AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
104214
return this.loader.apply(request);
105215
}
@@ -136,6 +246,18 @@ private String samlInflate(byte[] b) {
136246
}
137247
}
138248

249+
private Response parse(String request) throws Saml2Exception {
250+
try {
251+
Document document = this.parserPool
252+
.parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8)));
253+
Element element = document.getDocumentElement();
254+
return (Response) this.unmarshaller.unmarshall(element);
255+
}
256+
catch (Exception ex) {
257+
throw new Saml2Exception("Failed to deserialize LogoutRequest", ex);
258+
}
259+
}
260+
139261
static class Base64Checker {
140262

141263
private static final int[] values = genValueMapping();

saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,15 @@ public final class TestOpenSamlObjects {
105105

106106
public static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
107107

108-
private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
108+
public static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
109109

110110
private static SecretKey SECRET_KEY = new SecretKeySpec(
111111
Base64.getDecoder().decode("shOnwNMoCv88HKMEa91+FlYoD5RNvzMTAL5LGxZKIFk="), "AES");
112112

113113
private TestOpenSamlObjects() {
114114
}
115115

116-
static Response response() {
116+
public static Response response() {
117117
return response(DESTINATION, ASSERTING_PARTY_ENTITY_ID);
118118
}
119119

0 commit comments

Comments
 (0)