Skip to content

Commit 785123e

Browse files
committed
Add Saml2MetadataResponseResolver
Closes gh-12846
1 parent 7678523 commit 785123e

File tree

8 files changed

+465
-39
lines changed

8 files changed

+465
-39
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2002-2023 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.metadata;
18+
19+
import java.io.UnsupportedEncodingException;
20+
import java.net.URLEncoder;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.ArrayList;
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.LinkedHashMap;
26+
import java.util.Map;
27+
import java.util.UUID;
28+
29+
import jakarta.servlet.http.HttpServletRequest;
30+
31+
import org.springframework.security.saml2.Saml2Exception;
32+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
33+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
34+
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
35+
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
36+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
37+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
38+
import org.springframework.security.web.util.matcher.RequestMatcher;
39+
import org.springframework.util.Assert;
40+
41+
/**
42+
* An implementation of {@link Saml2MetadataResponseResolver} that identifies which
43+
* {@link RelyingPartyRegistration}s to use with a {@link RequestMatcher}
44+
*
45+
* @author Josh Cummings
46+
* @since 6.1
47+
*/
48+
public final class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver {
49+
50+
private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml";
51+
52+
private RequestMatcher matcher = new OrRequestMatcher(
53+
new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"),
54+
new AntPathRequestMatcher("/saml2/metadata/{registrationId}"),
55+
new AntPathRequestMatcher("/saml2/metadata"));
56+
57+
private String filename = DEFAULT_METADATA_FILENAME;
58+
59+
private final RelyingPartyRegistrationRepository registrations;
60+
61+
private final Saml2MetadataResolver metadata;
62+
63+
/**
64+
* Construct a {@link RequestMatcherMetadataResponseResolver}
65+
* @param registrations the source for relying party metadata
66+
* @param metadata the strategy for converting {@link RelyingPartyRegistration}s into
67+
* metadata
68+
*/
69+
public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations,
70+
Saml2MetadataResolver metadata) {
71+
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
72+
Assert.notNull(metadata, "saml2MetadataResolver cannot be null");
73+
this.registrations = registrations;
74+
this.metadata = metadata;
75+
}
76+
77+
/**
78+
* Construct and serialize a relying party's SAML 2.0 metadata based on the given
79+
* {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify
80+
* the metadata request, including looking for any indicated {@code registrationId}.
81+
*
82+
* <p>
83+
* If a {@code registrationId} is found in the request, it will attempt to use that,
84+
* erroring if no {@link RelyingPartyRegistration} is found.
85+
*
86+
* <p>
87+
* If no {@code registrationId} is found in the request, it will attempt to show all
88+
* {@link RelyingPartyRegistration}s in an {@code <md:EntitiesDescriptor>}. To
89+
* exercise this functionality, the provided
90+
* {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}.
91+
* @param request the HTTP request
92+
* @return a {@link Saml2MetadataResponse} instance
93+
* @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent
94+
* {@code registrationId}
95+
*/
96+
@Override
97+
public Saml2MetadataResponse resolve(HttpServletRequest request) {
98+
RequestMatcher.MatchResult result = this.matcher.matcher(request);
99+
if (!result.isMatch()) {
100+
return null;
101+
}
102+
String registrationId = result.getVariables().get("registrationId");
103+
Saml2MetadataResponse response = responseByRegistrationId(request, registrationId);
104+
if (response != null) {
105+
return response;
106+
}
107+
if (this.registrations instanceof Iterable<?>) {
108+
Iterable<RelyingPartyRegistration> registrations = (Iterable<RelyingPartyRegistration>) this.registrations;
109+
return responseByIterable(request, registrations);
110+
}
111+
return null;
112+
}
113+
114+
private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) {
115+
if (registrationId == null) {
116+
return null;
117+
}
118+
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
119+
if (registration == null) {
120+
throw new Saml2Exception("registration not found");
121+
}
122+
return responseByIterable(request, Collections.singleton(registration));
123+
}
124+
125+
private Saml2MetadataResponse responseByIterable(HttpServletRequest request,
126+
Iterable<RelyingPartyRegistration> registrations) {
127+
Map<String, RelyingPartyRegistration> results = new LinkedHashMap<>();
128+
for (RelyingPartyRegistration registration : registrations) {
129+
results.put(registration.getEntityId(), registration);
130+
}
131+
Collection<RelyingPartyRegistration> resolved = new ArrayList<>();
132+
for (RelyingPartyRegistration registration : results.values()) {
133+
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
134+
String entityId = uriResolver.resolve(registration.getEntityId());
135+
String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation());
136+
String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation());
137+
String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation());
138+
resolved.add(registration.mutate().entityId(entityId).assertionConsumerServiceLocation(ssoLocation)
139+
.singleLogoutServiceLocation(sloLocation).singleLogoutServiceResponseLocation(sloResponseLocation)
140+
.build());
141+
}
142+
String metadata = this.metadata.resolve(resolved);
143+
String value = (resolved.size() == 1) ? resolved.iterator().next().getRegistrationId()
144+
: UUID.randomUUID().toString();
145+
String fileName = this.filename.replace("{registrationId}", value);
146+
try {
147+
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
148+
return new Saml2MetadataResponse(metadata, encodedFileName);
149+
}
150+
catch (UnsupportedEncodingException ex) {
151+
throw new Saml2Exception(ex);
152+
}
153+
}
154+
155+
/**
156+
* Use this {@link RequestMatcher} to identity which requests to generate metadata
157+
* for. By default, matches {@code /saml2/metadata},
158+
* {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata},
159+
* and {@code /saml2/service-provider-metadata/{registrationId}}
160+
* @param requestMatcher the {@link RequestMatcher} to use
161+
*/
162+
public void setRequestMatcher(RequestMatcher requestMatcher) {
163+
Assert.notNull(requestMatcher, "requestMatcher cannot be empty");
164+
this.matcher = requestMatcher;
165+
}
166+
167+
/**
168+
* Sets the metadata filename template. If it contains the {@code {registrationId}}
169+
* placeholder, it will be resolved as a random UUID if there are multiple
170+
* {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the
171+
* {@link RelyingPartyRegistration}'s id.
172+
*
173+
* <p>
174+
* The default value is {@code saml-{registrationId}-metadata.xml}
175+
* @param metadataFilename metadata filename, must contain a {registrationId}
176+
*/
177+
public void setMetadataFilename(String metadataFilename) {
178+
Assert.hasText(metadataFilename, "metadataFilename cannot be empty");
179+
this.filename = metadataFilename;
180+
}
181+
182+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2002-2023 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.metadata;
18+
19+
public class Saml2MetadataResponse {
20+
21+
private final String metadata;
22+
23+
private final String fileName;
24+
25+
public Saml2MetadataResponse(String metadata, String fileName) {
26+
this.metadata = metadata;
27+
this.fileName = fileName;
28+
}
29+
30+
public String getMetadata() {
31+
return this.metadata;
32+
}
33+
34+
public String getFileName() {
35+
return this.fileName;
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2002-2023 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.metadata;
18+
19+
import jakarta.servlet.http.HttpServletRequest;
20+
21+
/**
22+
* Resolves Relying Party SAML 2.0 Metadata given details from the
23+
* {@link HttpServletRequest}.
24+
*
25+
* @author Josh Cummings
26+
* @since 6.1
27+
*/
28+
public interface Saml2MetadataResponseResolver {
29+
30+
/**
31+
* Construct and serialize a relying party's SAML 2.0 metadata based on the given
32+
* {@link HttpServletRequest}
33+
* @param request the HTTP request
34+
* @return a {@link Saml2MetadataResponse} instance
35+
*/
36+
Saml2MetadataResponse resolve(HttpServletRequest request);
37+
38+
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ public Saml2AuthenticationToken convert(HttpServletRequest request) {
9393
* request.
9494
* @param authenticationRequestRepository the
9595
* {@link Saml2AuthenticationRequestRepository} to use
96-
* @since 5.6
9796
*/
9897
public void setAuthenticationRequestRepository(
9998
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository) {

0 commit comments

Comments
 (0)