Use Federation Support
Closes gh-132
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
version=6.0.0-SNAPSHOT
|
version=6.1.0-SNAPSHOT
|
||||||
spring-security.version=6.0.0-SNAPSHOT
|
spring-security.version=6.1.0-SNAPSHOT
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023 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 example;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import net.shibboleth.utilities.java.support.xml.ParserPool;
|
|
||||||
import org.opensaml.core.config.ConfigurationService;
|
|
||||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
|
||||||
import org.opensaml.saml.saml2.core.Response;
|
|
||||||
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
|
|
||||||
import org.w3c.dom.Document;
|
|
||||||
import org.w3c.dom.Element;
|
|
||||||
|
|
||||||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
|
||||||
import org.springframework.security.saml2.core.Saml2Error;
|
|
||||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
|
||||||
import org.springframework.security.saml2.core.Saml2ParameterNames;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
|
||||||
|
|
||||||
public class EntityIdRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
|
||||||
|
|
||||||
static {
|
|
||||||
OpenSamlInitializationService.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ResponseUnmarshaller responseUnmarshaller;
|
|
||||||
|
|
||||||
private final ParserPool parserPool;
|
|
||||||
|
|
||||||
private final InMemoryRelyingPartyRegistrationRepository registrations;
|
|
||||||
|
|
||||||
public EntityIdRelyingPartyRegistrationResolver(InMemoryRelyingPartyRegistrationRepository registrations) {
|
|
||||||
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
|
|
||||||
this.responseUnmarshaller = (ResponseUnmarshaller) registry.getUnmarshallerFactory()
|
|
||||||
.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME);
|
|
||||||
this.parserPool = registry.getParserPool();
|
|
||||||
this.registrations = registrations;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RelyingPartyRegistration resolve(HttpServletRequest request, String relyingPartyRegistrationId) {
|
|
||||||
if (relyingPartyRegistrationId != null) {
|
|
||||||
return this.registrations.findByRegistrationId(relyingPartyRegistrationId);
|
|
||||||
}
|
|
||||||
String entityId = resolveEntityId(request);
|
|
||||||
if (entityId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (RelyingPartyRegistration registration : this.registrations) {
|
|
||||||
if (entityId.equals(registration.getAssertingPartyDetails().getEntityId())) {
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveEntityId(HttpServletRequest request) {
|
|
||||||
String saml2Response = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
|
|
||||||
if (saml2Response == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
byte[] decoded = Base64.getMimeDecoder().decode(saml2Response);
|
|
||||||
String serialized = new String(decoded, StandardCharsets.UTF_8);
|
|
||||||
return parseResponse(serialized).getIssuer().getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Response parseResponse(String serialized) {
|
|
||||||
try {
|
|
||||||
Document document = this.parserPool
|
|
||||||
.parse(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8)));
|
|
||||||
Element element = document.getDocumentElement();
|
|
||||||
return (Response) this.responseUnmarshaller.unmarshall(element);
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
Saml2Error error = new Saml2Error(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, ex.getMessage());
|
|
||||||
throw new Saml2AuthenticationException(error, ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2002-2022 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 example;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.RequestDispatcher;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Order(-102) // To run before FilterChainProxy
|
|
||||||
public class SamlExtensionUrlForwardingFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
private static final Map<String, String> urlMapping = Map.of("/saml/SSO", "/login/saml2/sso/sp",
|
|
||||||
"/saml/logout", "/logout/saml2/slo",
|
|
||||||
"/saml/SingleLogout", "/logout/saml2/slo",
|
|
||||||
"/saml/metadata", "/saml2/service-provider-metadata/sp");
|
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
private final RequestMatcher matcher = createRequestMatcher();
|
|
||||||
|
|
||||||
private RequestMatcher createRequestMatcher() {
|
|
||||||
Set<String> urls = urlMapping.keySet();
|
|
||||||
List<RequestMatcher> matchers = new LinkedList<>();
|
|
||||||
urls.forEach((url) -> matchers.add(new AntPathRequestMatcher(url)));
|
|
||||||
return new OrRequestMatcher(matchers);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
boolean match = this.matcher.matches(request);
|
|
||||||
if (!match) {
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String forwardUrl = urlMapping.get(request.getRequestURI());
|
|
||||||
RequestDispatcher dispatcher = request.getRequestDispatcher(forwardUrl);
|
|
||||||
dispatcher.forward(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -30,21 +30,13 @@ import java.util.stream.Collectors;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties;
|
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties;
|
||||||
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration;
|
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.security.config.Customizer;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||||
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
|
||||||
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -59,26 +51,14 @@ public class SecurityConfiguration {
|
|||||||
.requestMatchers("/error").permitAll()
|
.requestMatchers("/error").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.saml2Login(Customizer.withDefaults())
|
.saml2Login((saml2) -> saml2.loginProcessingUrl("/saml/SSO"))
|
||||||
.saml2Logout(Customizer.withDefaults());
|
.saml2Logout((saml2) -> saml2.logoutRequest((request) -> request.logoutUrl("/saml/logout")))
|
||||||
|
.saml2Logout((saml2) -> saml2.logoutResponse((response) -> response.logoutUrl("/saml/SingleLogout")))
|
||||||
|
.saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata"));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
Saml2AuthenticationTokenConverter usingEntityId(InMemoryRelyingPartyRegistrationRepository repository) {
|
|
||||||
var registrations = new EntityIdRelyingPartyRegistrationResolver(repository);
|
|
||||||
return new Saml2AuthenticationTokenConverter(registrations);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@Order(-101)
|
|
||||||
FilterRegistrationBean<Saml2MetadataFilter> metadata(Iterable<RelyingPartyRegistration> repository) {
|
|
||||||
var registrations = new DefaultRelyingPartyRegistrationResolver((id) -> repository.iterator().next());
|
|
||||||
var filter = new Saml2MetadataFilter(registrations, new OpenSamlMetadataResolver());
|
|
||||||
return new FilterRegistrationBean<>(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
InMemoryRelyingPartyRegistrationRepository repository(Saml2RelyingPartyProperties properties,
|
InMemoryRelyingPartyRegistrationRepository repository(Saml2RelyingPartyProperties properties,
|
||||||
@Value("classpath:credentials/rp-private.key") RSAPrivateKey key,
|
@Value("classpath:credentials/rp-private.key") RSAPrivateKey key,
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ spring:
|
|||||||
saml2:
|
saml2:
|
||||||
relyingparty:
|
relyingparty:
|
||||||
registration:
|
registration:
|
||||||
sp:
|
metadata:
|
||||||
entity-id: "http://localhost:8080/saml2/service-provider-metadata/one"
|
entity-id: "{baseUrl}/saml2/service-provider-metadata/one"
|
||||||
singlelogout:
|
singlelogout:
|
||||||
binding: POST
|
binding: POST
|
||||||
url: "http://localhost:8080/saml/logout"
|
url: "{baseUrl}/saml/logout"
|
||||||
responseUrl: "http://localhost:8080/saml/SingleLogout"
|
responseUrl: "{baseUrl}/saml/SingleLogout"
|
||||||
acs:
|
acs:
|
||||||
location: "http://localhost:8080/saml/SSO"
|
location: "{baseUrl}/saml/SSO"
|
||||||
assertingparty.metadata-uri: https://dev-05937739.okta.com/app/exk598vc9bHhwoTXM5d7/sso/saml/metadata
|
assertingparty.metadata-uri: https://dev-05937739.okta.com/app/exk598vc9bHhwoTXM5d7/sso/saml/metadata
|
||||||
|
|||||||
Reference in New Issue
Block a user