sessionAuthenticationFailureHandler(
* {@link HttpServletResponse#encodeRedirectURL(String)} or
* {@link HttpServletResponse#encodeURL(String)}, otherwise disallows HTTP sessions to
* be included in the URL. This prevents leaking information to external domains.
+ *
+ * This is achieved by guarding {@link HttpServletResponse#encodeURL} and
+ * {@link HttpServletResponse#encodeRedirectURL} invocations. Any code that also
+ * overrides either of these two methods, like
+ * {@link org.springframework.web.servlet.resource.ResourceUrlEncodingFilter}, needs
+ * to come after the security filter chain or risk being skipped.
* @param enableSessionUrlRewriting true if should allow the JSESSIONID to be
* rewritten into the URLs, else false (default)
* @return the {@link SessionManagementConfigurer} for further customization
@@ -370,6 +378,12 @@ public void configure(H http) {
concurrentSessionFilter = postProcess(concurrentSessionFilter);
http.addFilter(concurrentSessionFilter);
}
+ if (!this.enableSessionUrlRewriting) {
+ http.addFilter(new DisableEncodeUrlFilter());
+ }
+ if (this.sessionPolicy == SessionCreationPolicy.ALWAYS) {
+ http.addFilter(new ForceEagerSessionCreationFilter());
+ }
}
private ConcurrentSessionFilter createConcurrencyFilter(H http) {
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java
index 93e1b092506..30de7141b42 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * 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.
@@ -18,15 +18,19 @@
import javax.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
@@ -141,7 +145,9 @@ public X509Configurer userDetailsService(UserDetailsService userDetailsServic
/**
* Specifies the {@link AuthenticationUserDetailsService} to use. If not specified,
* the shared {@link UserDetailsService} will be used to create a
- * {@link UserDetailsByNameServiceWrapper}.
+ * {@link UserDetailsByNameServiceWrapper}. If a {@link SecurityFilterChain} bean is
+ * used instead of the {@link WebSecurityConfigurerAdapter}, then the
+ * {@link UserDetailsService} bean will be used by default.
* @param authenticationUserDetailsService the
* {@link AuthenticationUserDetailsService} to use
* @return the {@link X509Configurer} for further customizations
@@ -200,9 +206,30 @@ private X509AuthenticationFilter getFilter(AuthenticationManager authenticationM
private AuthenticationUserDetailsService getAuthenticationUserDetailsService(
H http) {
if (this.authenticationUserDetailsService == null) {
- userDetailsService(http.getSharedObject(UserDetailsService.class));
+ userDetailsService(getSharedOrBean(http, UserDetailsService.class));
}
return this.authenticationUserDetailsService;
}
+ private C getSharedOrBean(H http, Class type) {
+ C shared = http.getSharedObject(type);
+ if (shared != null) {
+ return shared;
+ }
+ return getBeanOrNull(type);
+ }
+
+ private T getBeanOrNull(Class type) {
+ ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
+ if (context == null) {
+ return null;
+ }
+ try {
+ return context.getBean(type);
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ return null;
+ }
+ }
+
}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java
index 5bdb85f3eb1..be1707e3946 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -16,6 +16,7 @@
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -73,12 +74,14 @@
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
/**
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Login, which leverages the OAuth 2.0
@@ -503,14 +506,28 @@ private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLogin
new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
+ RequestMatcher formLoginNotEnabled = getFormLoginNotEnabledRequestMatcher(http);
LinkedHashMap entryPoints = new LinkedHashMap<>();
- entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)),
- new LoginUrlAuthenticationEntryPoint(providerLoginPage));
+ entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher),
+ formLoginNotEnabled), new LoginUrlAuthenticationEntryPoint(providerLoginPage));
DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint());
return loginEntryPoint;
}
+ private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {
+ DefaultLoginPageGeneratingFilter defaultLoginPageGeneratingFilter = http
+ .getSharedObject(DefaultLoginPageGeneratingFilter.class);
+ Field formLoginEnabledField = (defaultLoginPageGeneratingFilter != null)
+ ? ReflectionUtils.findField(DefaultLoginPageGeneratingFilter.class, "formLoginEnabled") : null;
+ if (formLoginEnabledField != null) {
+ ReflectionUtils.makeAccessible(formLoginEnabledField);
+ return (request) -> Boolean.FALSE
+ .equals(ReflectionUtils.getField(formLoginEnabledField, defaultLoginPageGeneratingFilter));
+ }
+ return AnyRequestMatcher.INSTANCE;
+ }
+
/**
* Configuration options for the Authorization Server's Authorization Endpoint.
*/
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
index 02a99fdb2a4..0f267915973 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
@@ -18,6 +18,9 @@
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
@@ -25,6 +28,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.MediaType;
+import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
@@ -51,6 +55,9 @@
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.access.AccessDeniedHandlerImpl;
+import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
+import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
@@ -153,12 +160,19 @@ public final class OAuth2ResourceServerConfigurer(createAccessDeniedHandlers()), new BearerTokenAccessDeniedHandler());
private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher();
+ private static Map, AccessDeniedHandler> createAccessDeniedHandlers() {
+ Map, AccessDeniedHandler> handlers = new HashMap<>();
+ handlers.put(CsrfException.class, new AccessDeniedHandlerImpl());
+ return handlers;
+ }
+
public OAuth2ResourceServerConfigurer(ApplicationContext context) {
Assert.notNull(context, "context cannot be null");
this.context = context;
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java
index aa1ddb29af6..a4cfb815dc8 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -19,8 +19,6 @@
import java.util.LinkedHashMap;
import java.util.Map;
-import javax.servlet.Filter;
-
import org.opensaml.core.Version;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -34,8 +32,6 @@
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
-import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
-import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
@@ -50,12 +46,14 @@
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
+import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -113,11 +111,15 @@
public final class Saml2LoginConfigurer>
extends AbstractAuthenticationFilterConfigurer, Saml2WebSsoAuthenticationFilter> {
+ private static final String OPEN_SAML_4_VERSION = "4";
+
private String loginPage;
- private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
+ private String authenticationRequestUri = "/saml2/authenticate/{registrationId}";
- private AuthenticationRequestEndpointConfig authenticationRequestEndpoint = new AuthenticationRequestEndpointConfig();
+ private Saml2AuthenticationRequestResolver authenticationRequestResolver;
+
+ private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
@@ -176,6 +178,20 @@ public Saml2LoginConfigurer loginPage(String loginPage) {
return this;
}
+ /**
+ * Use this {@link Saml2AuthenticationRequestResolver} for generating SAML 2.0
+ * Authentication Requests.
+ * @param authenticationRequestResolver
+ * @return the {@link Saml2LoginConfigurer} for further configuration
+ * @since 5.7
+ */
+ public Saml2LoginConfigurer authenticationRequestResolver(
+ Saml2AuthenticationRequestResolver authenticationRequestResolver) {
+ Assert.notNull(authenticationRequestResolver, "authenticationRequestResolver cannot be null");
+ this.authenticationRequestResolver = authenticationRequestResolver;
+ return this;
+ }
+
/**
* Specifies the URL to validate the credentials. If specified a custom URL, consider
* specifying a custom {@link AuthenticationConverter} via
@@ -200,7 +216,7 @@ protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingU
/**
* {@inheritDoc}
- *
+ *
* Initializes this filter chain for SAML 2 Login. The following actions are taken:
*
* - The WebSSO endpoint has CSRF disabled, typically {@code /login/saml2/sso}
@@ -226,8 +242,8 @@ public void init(B http) throws Exception {
super.init(http);
}
else {
- Map providerUrlMap = getIdentityProviderUrlMap(
- this.authenticationRequestEndpoint.filterProcessingUrl, this.relyingPartyRegistrationRepository);
+ Map providerUrlMap = getIdentityProviderUrlMap(this.authenticationRequestUri,
+ this.relyingPartyRegistrationRepository);
boolean singleProvider = providerUrlMap.size() == 1;
if (singleProvider) {
// Setup auto-redirect to provider login page
@@ -247,14 +263,16 @@ public void init(B http) throws Exception {
/**
* {@inheritDoc}
- *
+ *
* During the {@code configure} phase, a
* {@link Saml2WebSsoAuthenticationRequestFilter} is added to handle SAML 2.0
* AuthNRequest redirects
*/
@Override
public void configure(B http) throws Exception {
- http.addFilter(this.authenticationRequestEndpoint.build(http));
+ Saml2WebSsoAuthenticationRequestFilter filter = getAuthenticationRequestFilter(http);
+ filter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
+ http.addFilter(postProcess(filter));
super.configure(http);
if (this.authenticationManager == null) {
registerDefaultAuthenticationProvider(http);
@@ -264,6 +282,11 @@ public void configure(B http) throws Exception {
}
}
+ private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(B http) {
+ RelyingPartyRegistrationRepository registrations = relyingPartyRegistrationRepository(http);
+ return new DefaultRelyingPartyRegistrationResolver(registrations);
+ }
+
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(B http) {
if (this.relyingPartyRegistrationRepository == null) {
this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class);
@@ -276,6 +299,44 @@ private void setAuthenticationRequestRepository(B http,
saml2WebSsoAuthenticationFilter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
}
+ private Saml2WebSsoAuthenticationRequestFilter getAuthenticationRequestFilter(B http) {
+ Saml2AuthenticationRequestResolver authenticationRequestResolver = getAuthenticationRequestResolver(http);
+ if (authenticationRequestResolver != null) {
+ return new Saml2WebSsoAuthenticationRequestFilter(authenticationRequestResolver);
+ }
+ return new Saml2WebSsoAuthenticationRequestFilter(getAuthenticationRequestContextResolver(http),
+ getAuthenticationRequestFactory(http));
+ }
+
+ private Saml2AuthenticationRequestResolver getAuthenticationRequestResolver(B http) {
+ if (this.authenticationRequestResolver != null) {
+ return this.authenticationRequestResolver;
+ }
+ return getBeanOrNull(http, Saml2AuthenticationRequestResolver.class);
+ }
+
+ private Saml2AuthenticationRequestFactory getAuthenticationRequestFactory(B http) {
+ Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
+ if (resolver != null) {
+ return resolver;
+ }
+ if (version().startsWith("4")) {
+ return OpenSaml4LoginSupportFactory.getAuthenticationRequestFactory();
+ }
+ return new OpenSamlAuthenticationRequestFactory();
+ }
+
+ private Saml2AuthenticationRequestContextResolver getAuthenticationRequestContextResolver(B http) {
+ Saml2AuthenticationRequestContextResolver resolver = getBeanOrNull(http,
+ Saml2AuthenticationRequestContextResolver.class);
+ if (resolver != null) {
+ return resolver;
+ }
+ RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(
+ this.relyingPartyRegistrationRepository);
+ return new DefaultSaml2AuthenticationRequestContextResolver(registrationResolver);
+ }
+
private AuthenticationConverter getAuthenticationConverter(B http) {
if (this.authenticationConverter != null) {
return this.authenticationConverter;
@@ -292,18 +353,9 @@ private AuthenticationConverter getAuthenticationConverter(B http) {
return authenticationConverterBean;
}
- private String version() {
- String version = Version.getVersion();
- if (version != null) {
- return version;
- }
- return Version.class.getModule().getDescriptor().version().map(Object::toString)
- .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
- }
-
private void registerDefaultAuthenticationProvider(B http) {
if (version().startsWith("4")) {
- http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
+ http.authenticationProvider(postProcess(OpenSaml4LoginSupportFactory.getAuthenticationProvider()));
}
else {
http.authenticationProvider(postProcess(new OpenSamlAuthenticationProvider()));
@@ -325,8 +377,8 @@ private void initDefaultLoginFilter(B http) {
return;
}
loginPageGeneratingFilter.setSaml2LoginEnabled(true);
- loginPageGeneratingFilter.setSaml2AuthenticationUrlToProviderName(this.getIdentityProviderUrlMap(
- this.authenticationRequestEndpoint.filterProcessingUrl, this.relyingPartyRegistrationRepository));
+ loginPageGeneratingFilter.setSaml2AuthenticationUrlToProviderName(
+ this.getIdentityProviderUrlMap(this.authenticationRequestUri, this.relyingPartyRegistrationRepository));
loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage());
loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl());
}
@@ -353,6 +405,19 @@ private Saml2AuthenticationRequestRepository
return repository;
}
+ private String version() {
+ String version = Version.getVersion();
+ if (StringUtils.hasText(version)) {
+ return version;
+ }
+ boolean openSaml4ClassPresent = ClassUtils
+ .isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
+ if (openSaml4ClassPresent) {
+ return OPEN_SAML_4_VERSION;
+ }
+ throw new IllegalStateException("cannot determine OpenSAML version");
+ }
+
private C getSharedOrBean(B http, Class clazz) {
C shared = http.getSharedObject(clazz);
if (shared != null) {
@@ -380,44 +445,31 @@ private void setSharedObject(B http, Class clazz, C object) {
}
}
- private final class AuthenticationRequestEndpointConfig {
-
- private String filterProcessingUrl = "/saml2/authenticate/{registrationId}";
-
- private AuthenticationRequestEndpointConfig() {
- }
-
- private Filter build(B http) {
- Saml2AuthenticationRequestFactory authenticationRequestResolver = getResolver(http);
- Saml2AuthenticationRequestContextResolver contextResolver = getContextResolver(http);
- Saml2AuthenticationRequestRepository repository = getAuthenticationRequestRepository(
- http);
- Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(contextResolver,
- authenticationRequestResolver);
- filter.setAuthenticationRequestRepository(repository);
- return postProcess(filter);
- }
+ private static class OpenSaml4LoginSupportFactory {
- private Saml2AuthenticationRequestFactory getResolver(B http) {
- Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
- if (resolver == null) {
- if (version().startsWith("4")) {
- return new OpenSaml4AuthenticationRequestFactory();
- }
- return new OpenSamlAuthenticationRequestFactory();
+ private static Saml2AuthenticationRequestFactory getAuthenticationRequestFactory() {
+ try {
+ Class> authenticationRequestFactory = ClassUtils.forName(
+ "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory",
+ OpenSaml4LoginSupportFactory.class.getClassLoader());
+ return (Saml2AuthenticationRequestFactory) authenticationRequestFactory.getDeclaredConstructor()
+ .newInstance();
+ }
+ catch (ReflectiveOperationException ex) {
+ throw new IllegalStateException("Could not instantiate OpenSaml4AuthenticationRequestFactory", ex);
}
- return resolver;
}
- private Saml2AuthenticationRequestContextResolver getContextResolver(B http) {
- Saml2AuthenticationRequestContextResolver resolver = getBeanOrNull(http,
- Saml2AuthenticationRequestContextResolver.class);
- if (resolver == null) {
- RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver(
- Saml2LoginConfigurer.this.relyingPartyRegistrationRepository);
- return new DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver);
+ private static AuthenticationProvider getAuthenticationProvider() {
+ try {
+ Class> authenticationProvider = ClassUtils.forName(
+ "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider",
+ OpenSaml4LoginSupportFactory.class.getClassLoader());
+ return (AuthenticationProvider) authenticationProvider.getDeclaredConstructor().newInstance();
+ }
+ catch (ReflectiveOperationException ex) {
+ throw new IllegalStateException("Could not instantiate OpenSaml4AuthenticationProvider", ex);
}
- return resolver;
}
}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java
index 45bd549c01c..5a974231ae8 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -47,8 +47,6 @@
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver;
-import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver;
-import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
@@ -67,6 +65,8 @@
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
/**
* Adds SAML 2.0 logout support.
@@ -113,6 +113,8 @@
public final class Saml2LogoutConfigurer>
extends AbstractHttpConfigurer, H> {
+ private static final String OPEN_SAML_4_VERSION = "4";
+
private ApplicationContext context;
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
@@ -148,7 +150,7 @@ public Saml2LogoutConfigurer(ApplicationContext context) {
*
* The Relying Party triggers logout by POSTing to the endpoint. The Asserting Party
* triggers logout based on what is specified by
- * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
+ * {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @param logoutUrl the URL that will invoke logout
* @return the {@link LogoutConfigurer} for further customizations
* @see LogoutConfigurer#logoutUrl(String)
@@ -304,6 +306,19 @@ private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver(
return this.logoutResponseConfigurer.logoutResponseResolver(relyingPartyRegistrationResolver);
}
+ private String version() {
+ String version = Version.getVersion();
+ if (StringUtils.hasText(version)) {
+ return version;
+ }
+ boolean openSaml4ClassPresent = ClassUtils
+ .isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
+ if (openSaml4ClassPresent) {
+ return OPEN_SAML_4_VERSION;
+ }
+ throw new IllegalStateException("cannot determine OpenSAML version");
+ }
+
private C getBeanOrNull(Class clazz) {
if (this.context == null) {
return null;
@@ -314,15 +329,6 @@ private C getBeanOrNull(Class clazz) {
return this.context.getBean(clazz);
}
- private String version() {
- String version = Version.getVersion();
- if (version != null) {
- return version;
- }
- return Version.class.getModule().getDescriptor().version().map(Object::toString)
- .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
- }
-
/**
* A configurer for SAML 2.0 LogoutRequest components
*/
@@ -344,7 +350,7 @@ public final class LogoutRequestConfigurer {
*
*
* The Asserting Party should use whatever HTTP method specified in
- * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
+ * {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @param logoutUrl the URL that will receive the SAML 2.0 Logout Request
* @return the {@link LogoutRequestConfigurer} for further customizations
* @see Saml2LogoutConfigurer#logoutUrl(String)
@@ -403,7 +409,7 @@ private Saml2LogoutRequestResolver logoutRequestResolver(
return this.logoutRequestResolver;
}
if (version().startsWith("4")) {
- return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver);
+ return OpenSaml4LogoutSupportFactory.getLogoutRequestResolver(relyingPartyRegistrationResolver);
}
return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver);
}
@@ -426,7 +432,7 @@ public final class LogoutResponseConfigurer {
*
*
* The Asserting Party should use whatever HTTP method specified in
- * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
+ * {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @param logoutUrl the URL that will receive the SAML 2.0 Logout Response
* @return the {@link LogoutResponseConfigurer} for further customizations
* @see Saml2LogoutConfigurer#logoutUrl(String)
@@ -471,13 +477,13 @@ private Saml2LogoutResponseValidator logoutResponseValidator() {
private Saml2LogoutResponseResolver logoutResponseResolver(
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
- if (this.logoutResponseResolver == null) {
- if (version().startsWith("4")) {
- return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver);
- }
- return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
+ if (this.logoutResponseResolver != null) {
+ return this.logoutResponseResolver;
}
- return this.logoutResponseResolver;
+ if (version().startsWith("4")) {
+ return OpenSaml4LogoutSupportFactory.getLogoutResponseResolver(relyingPartyRegistrationResolver);
+ }
+ return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
}
}
@@ -520,4 +526,38 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut
}
+ private static class OpenSaml4LogoutSupportFactory {
+
+ private static Saml2LogoutResponseResolver getLogoutResponseResolver(
+ RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+ try {
+ Class> logoutResponseResolver = ClassUtils.forName(
+ "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver",
+ OpenSaml4LogoutSupportFactory.class.getClassLoader());
+ return (Saml2LogoutResponseResolver) logoutResponseResolver
+ .getDeclaredConstructor(RelyingPartyRegistrationResolver.class)
+ .newInstance(relyingPartyRegistrationResolver);
+ }
+ catch (ReflectiveOperationException ex) {
+ throw new IllegalStateException("Could not instantiate OpenSaml4LogoutResponseResolver", ex);
+ }
+ }
+
+ private static Saml2LogoutRequestResolver getLogoutRequestResolver(
+ RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+ try {
+ Class> logoutRequestResolver = ClassUtils.forName(
+ "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver",
+ OpenSaml4LogoutSupportFactory.class.getClassLoader());
+ return (Saml2LogoutRequestResolver) logoutRequestResolver
+ .getDeclaredConstructor(RelyingPartyRegistrationResolver.class)
+ .newInstance(relyingPartyRegistrationResolver);
+ }
+ catch (ReflectiveOperationException ex) {
+ throw new IllegalStateException("Could not instantiate OpenSaml4LogoutRequestResolver", ex);
+ }
+ }
+
+ }
+
}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java
index eee7e34f368..dda9e7bea80 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 the original author or authors.
+ * 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.
@@ -28,6 +28,7 @@
import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
+import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
@@ -43,7 +44,9 @@
*
* @author Rob Winch
* @since 4.0
+ * @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead
*/
+@Deprecated
public class MessageSecurityMetadataSourceRegistry {
private static final String permitAll = "permitAll";
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java
index 5a56f256506..e72f9a5b4bf 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * 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.
@@ -22,6 +22,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
@@ -41,6 +42,7 @@
* This {@code Configuration} is imported by {@link EnableWebFluxSecurity}
*
* @author Rob Winch
+ * @author Alavudin Kuttikkattil
* @since 5.1
*/
final class ReactiveOAuth2ClientImportSelector implements ImportSelector {
@@ -64,14 +66,12 @@ static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigur
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
+ private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
+
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
- if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) {
- ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
- .builder().authorizationCode().refreshToken().clientCredentials().password().build();
- DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
- this.clientRegistrationRepository, getAuthorizedClientRepository());
- authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+ ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
+ if (authorizedClientManager != null) {
configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager));
}
}
@@ -93,6 +93,13 @@ void setAuthorizedClientService(List auth
}
}
+ @Autowired(required = false)
+ void setAuthorizedClientManager(List authorizedClientManager) {
+ if (authorizedClientManager.size() == 1) {
+ this.authorizedClientManager = authorizedClientManager.get(0);
+ }
+ }
+
private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
if (this.authorizedClientRepository != null) {
return this.authorizedClientRepository;
@@ -103,6 +110,23 @@ private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
return null;
}
+ private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() {
+ if (this.authorizedClientManager != null) {
+ return this.authorizedClientManager;
+ }
+ ReactiveOAuth2AuthorizedClientManager authorizedClientManager = null;
+ if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) {
+ ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
+ .builder().authorizationCode().refreshToken().clientCredentials().password().build();
+ DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
+ this.clientRegistrationRepository, getAuthorizedClientRepository());
+ defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+ authorizedClientManager = defaultReactiveOAuth2AuthorizedClientManager;
+ }
+
+ return authorizedClientManager;
+ }
+
}
}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java
index 60139cd59aa..4d2dc99ede7 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * 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.
@@ -81,9 +81,12 @@
*
* @author Rob Winch
* @since 4.0
+ * @see WebSocketMessageBrokerSecurityConfiguration
+ * @deprecated Use {@link EnableWebSocketSecurity} instead
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
@Import(ObjectPostProcessorConfiguration.class)
+@Deprecated
public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer
implements SmartInitializingSingleton {
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java
new file mode 100644
index 00000000000..e80671aa50c
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java
@@ -0,0 +1,58 @@
+/*
+ * 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 org.springframework.security.config.annotation.web.socket;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+
+/**
+ * Allows configuring WebSocket Authorization.
+ *
+ *
+ * For example:
+ *
+ *
+ *
+ * @Configuration
+ * @EnableWebSocketSecurity
+ * public class WebSocketSecurityConfig {
+ *
+ * @Bean
+ * AuthorizationManager<Message<?>> (MessageMatcherDelegatingAuthorizationManager.Builder messages) {
+ * messages.simpDestMatchers("/user/queue/errors").permitAll()
+ * .simpDestMatchers("/admin/**").hasRole("ADMIN")
+ * .anyMessage().authenticated();
+ * return messages.build();
+ * }
+ * }
+ *
+ *
+ * @author Josh Cummings
+ * @since 5.8
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+@Import(WebSocketMessageBrokerSecurityConfiguration.class)
+public @interface EnableWebSocketSecurity {
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/MessageMatcherAuthorizationManagerConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/MessageMatcherAuthorizationManagerConfiguration.java
new file mode 100644
index 00000000000..930ce773539
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/MessageMatcherAuthorizationManagerConfiguration.java
@@ -0,0 +1,38 @@
+/*
+ * 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 org.springframework.security.config.annotation.web.socket;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Scope;
+import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
+import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
+import org.springframework.util.AntPathMatcher;
+
+final class MessageMatcherAuthorizationManagerConfiguration {
+
+ @Bean
+ @Scope("prototype")
+ MessageMatcherDelegatingAuthorizationManager.Builder messageAuthorizationManagerBuilder(
+ ApplicationContext context) {
+ return MessageMatcherDelegatingAuthorizationManager.builder().simpDestPathMatcher(
+ () -> (context.getBeanNamesForType(SimpAnnotationMethodMessageHandler.class).length > 0)
+ ? context.getBean(SimpAnnotationMethodMessageHandler.class).getPathMatcher()
+ : new AntPathMatcher());
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java
new file mode 100644
index 00000000000..8c5db34fb5a
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java
@@ -0,0 +1,145 @@
+/*
+ * 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 org.springframework.security.config.annotation.web.socket;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.SmartInitializingSingleton;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
+import org.springframework.messaging.simp.config.ChannelRegistration;
+import org.springframework.messaging.support.ChannelInterceptor;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
+import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
+import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
+import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
+import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
+import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
+import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
+import org.springframework.util.Assert;
+import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
+import org.springframework.web.socket.sockjs.SockJsService;
+import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler;
+import org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsService;
+
+@Order(Ordered.HIGHEST_PRECEDENCE + 100)
+@Import(MessageMatcherAuthorizationManagerConfiguration.class)
+final class WebSocketMessageBrokerSecurityConfiguration
+ implements WebSocketMessageBrokerConfigurer, SmartInitializingSingleton {
+
+ private static final String SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME = "stompWebSocketHandlerMapping";
+
+ private MessageMatcherDelegatingAuthorizationManager b;
+
+ private static final AuthorizationManager> ANY_MESSAGE_AUTHENTICATED = MessageMatcherDelegatingAuthorizationManager
+ .builder().anyMessage().authenticated().build();
+
+ private final ChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor();
+
+ private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
+
+ private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor(
+ ANY_MESSAGE_AUTHENTICATED);
+
+ private ApplicationContext context;
+
+ WebSocketMessageBrokerSecurityConfiguration(ApplicationContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public void addArgumentResolvers(List argumentResolvers) {
+ argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
+ }
+
+ @Override
+ public void configureClientInboundChannel(ChannelRegistration registration) {
+ this.authorizationChannelInterceptor
+ .setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context));
+ registration.interceptors(this.securityContextChannelInterceptor, this.csrfChannelInterceptor,
+ this.authorizationChannelInterceptor);
+ }
+
+ @Autowired(required = false)
+ void setAuthorizationManager(AuthorizationManager> authorizationManager) {
+ this.authorizationChannelInterceptor = new AuthorizationChannelInterceptor(authorizationManager);
+ }
+
+ @Override
+ public void afterSingletonsInstantiated() {
+ SimpleUrlHandlerMapping mapping = getBeanOrNull(SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME,
+ SimpleUrlHandlerMapping.class);
+ if (mapping == null) {
+ return;
+ }
+ configureCsrf(mapping);
+ }
+
+ private T getBeanOrNull(String name, Class type) {
+ Map beans = this.context.getBeansOfType(type);
+ return beans.get(name);
+ }
+
+ private void configureCsrf(SimpleUrlHandlerMapping mapping) {
+ Map mappings = mapping.getHandlerMap();
+ for (Object object : mappings.values()) {
+ if (object instanceof SockJsHttpRequestHandler) {
+ setHandshakeInterceptors((SockJsHttpRequestHandler) object);
+ }
+ else if (object instanceof WebSocketHttpRequestHandler) {
+ setHandshakeInterceptors((WebSocketHttpRequestHandler) object);
+ }
+ else {
+ throw new IllegalStateException(
+ "Bean " + SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME + " is expected to contain mappings to either a "
+ + "SockJsHttpRequestHandler or a WebSocketHttpRequestHandler but got " + object);
+ }
+ }
+ }
+
+ private void setHandshakeInterceptors(SockJsHttpRequestHandler handler) {
+ SockJsService sockJsService = handler.getSockJsService();
+ Assert.state(sockJsService instanceof TransportHandlingSockJsService,
+ () -> "sockJsService must be instance of TransportHandlingSockJsService got " + sockJsService);
+ TransportHandlingSockJsService transportHandlingSockJsService = (TransportHandlingSockJsService) sockJsService;
+ List handshakeInterceptors = transportHandlingSockJsService.getHandshakeInterceptors();
+ List interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1);
+ interceptorsToSet.add(new CsrfTokenHandshakeInterceptor());
+ interceptorsToSet.addAll(handshakeInterceptors);
+ transportHandlingSockJsService.setHandshakeInterceptors(interceptorsToSet);
+ }
+
+ private void setHandshakeInterceptors(WebSocketHttpRequestHandler handler) {
+ List handshakeInterceptors = handler.getHandshakeInterceptors();
+ List interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1);
+ interceptorsToSet.add(new CsrfTokenHandshakeInterceptor());
+ interceptorsToSet.addAll(handshakeInterceptors);
+ handler.setHandshakeInterceptors(interceptorsToSet);
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java
index a4d79280ac8..33bdcb923b5 100644
--- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java
@@ -102,7 +102,9 @@ public BeanDefinition parse(Element element, ParserContext pc) {
pc.getRegistry().registerAlias(id, alias);
pc.getReaderContext().fireAliasRegistered(id, alias, pc.extractSource(element));
}
- if (!BeanIds.AUTHENTICATION_MANAGER.equals(id)) {
+ if (!BeanIds.AUTHENTICATION_MANAGER.equals(id)
+ && !pc.getRegistry().containsBeanDefinition(BeanIds.AUTHENTICATION_MANAGER)
+ && !pc.getRegistry().isAlias(BeanIds.AUTHENTICATION_MANAGER)) {
pc.getRegistry().registerAlias(id, BeanIds.AUTHENTICATION_MANAGER);
pc.getReaderContext().fireAliasRegistered(id, BeanIds.AUTHENTICATION_MANAGER, pc.extractSource(element));
}
diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java
index d3c0ce32f4b..1150200f051 100644
--- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java
+++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * 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.
@@ -164,6 +164,8 @@ final class AuthenticationConfigBuilder {
@SuppressWarnings("rawtypes")
private ManagedList logoutHandlers;
+ private BeanMetadataElement logoutSuccessHandler;
+
private BeanDefinition loginPageGenerationFilter;
private BeanDefinition logoutPageGenerationFilter;
@@ -202,6 +204,20 @@ final class AuthenticationConfigBuilder {
private BeanDefinition oauth2LoginLinks;
+ private BeanDefinition saml2AuthenticationUrlToProviderName;
+
+ private BeanDefinition saml2AuthorizationRequestFilter;
+
+ private String saml2AuthenticationFilterId;
+
+ private String saml2AuthenticationRequestFilterId;
+
+ private String saml2LogoutFilterId;
+
+ private String saml2LogoutRequestFilterId;
+
+ private String saml2LogoutResponseFilterId;
+
private boolean oauth2ClientEnabled;
private BeanDefinition authorizationRequestRedirectFilter;
@@ -220,8 +236,8 @@ final class AuthenticationConfigBuilder {
AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc,
SessionCreationPolicy sessionPolicy, BeanReference requestCache, BeanReference authenticationManager,
- BeanReference sessionStrategy, BeanReference portMapper, BeanReference portResolver,
- BeanMetadataElement csrfLogoutHandler) {
+ BeanReference authenticationFilterSecurityContextRepositoryRef, BeanReference sessionStrategy,
+ BeanReference portMapper, BeanReference portResolver, BeanMetadataElement csrfLogoutHandler) {
this.httpElt = element;
this.pc = pc;
this.requestCache = requestCache;
@@ -235,12 +251,16 @@ final class AuthenticationConfigBuilder {
createRememberMeFilter(authenticationManager);
createBasicFilter(authenticationManager);
createBearerTokenAuthenticationFilter(authenticationManager);
- createFormLoginFilter(sessionStrategy, authenticationManager);
- createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager);
- createOpenIDLoginFilter(sessionStrategy, authenticationManager);
+ createFormLoginFilter(sessionStrategy, authenticationManager, authenticationFilterSecurityContextRepositoryRef);
+ createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager,
+ authenticationFilterSecurityContextRepositoryRef);
+ createOpenIDLoginFilter(sessionStrategy, authenticationManager,
+ authenticationFilterSecurityContextRepositoryRef);
+ createSaml2LoginFilter(authenticationManager, authenticationFilterSecurityContextRepositoryRef);
createX509Filter(authenticationManager);
createJeeFilter(authenticationManager);
createLogoutFilter();
+ createSaml2LogoutFilter();
createLoginPageFilterIfNeeded();
createUserDetailsServiceFactory();
createExceptionTranslationFilter();
@@ -272,7 +292,8 @@ private void createRememberMeProvider(String key) {
this.rememberMeProviderRef = new RuntimeBeanReference(id);
}
- void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
+ void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authManager,
+ BeanReference authenticationFilterSecurityContextRepositoryRef) {
Element formLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.FORM_LOGIN);
RootBeanDefinition formFilter = null;
if (formLoginElt != null || this.autoConfig) {
@@ -288,6 +309,10 @@ void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authMana
if (formFilter != null) {
formFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
formFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+ if (authenticationFilterSecurityContextRepositoryRef != null) {
+ formFilter.getPropertyValues().addPropertyValue("securityContextRepository",
+ authenticationFilterSecurityContextRepositoryRef);
+ }
// Id is required by login page filter
this.formFilterId = this.pc.getReaderContext().generateBeanName(formFilter);
this.pc.registerBeanComponent(new BeanComponentDefinition(formFilter, this.formFilterId));
@@ -296,13 +321,15 @@ void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authMana
}
void createOAuth2ClientFilters(BeanReference sessionStrategy, BeanReference requestCache,
- BeanReference authenticationManager) {
- createOAuth2LoginFilter(sessionStrategy, authenticationManager);
- createOAuth2ClientFilter(requestCache, authenticationManager);
+ BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef) {
+ createOAuth2LoginFilter(sessionStrategy, authenticationManager,
+ authenticationFilterSecurityContextRepositoryRef);
+ createOAuth2ClientFilter(requestCache, authenticationManager, authenticationFilterSecurityContextRepositoryRef);
registerOAuth2ClientPostProcessors();
}
- void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
+ void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager,
+ BeanReference authenticationFilterSecurityContextRepositoryRef) {
Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN);
if (oauth2LoginElt == null) {
return;
@@ -314,6 +341,10 @@ void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authMa
BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+ if (authenticationFilterSecurityContextRepositoryRef != null) {
+ oauth2LoginFilterBean.getPropertyValues().addPropertyValue("securityContextRepository",
+ authenticationFilterSecurityContextRepositoryRef);
+ }
// retrieve the other bean result
BeanDefinition oauth2LoginAuthProvider = parser.getOAuth2LoginAuthenticationProvider();
@@ -343,14 +374,15 @@ void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authMa
this.oauth2LoginOidcAuthenticationProviderRef = new RuntimeBeanReference(oauth2LoginOidcAuthProviderId);
}
- void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenticationManager) {
+ void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenticationManager,
+ BeanReference authenticationFilterSecurityContextRepositoryRef) {
Element oauth2ClientElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_CLIENT);
if (oauth2ClientElt == null) {
return;
}
this.oauth2ClientEnabled = true;
OAuth2ClientBeanDefinitionParser parser = new OAuth2ClientBeanDefinitionParser(requestCache,
- authenticationManager);
+ authenticationManager, authenticationFilterSecurityContextRepositoryRef);
parser.parse(oauth2ClientElt, this.pc);
BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
@@ -395,7 +427,8 @@ private void registerOAuth2ClientPostProcessors() {
}
}
- void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
+ void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager,
+ BeanReference authenticationFilterSecurityContextRepositoryRef) {
Element openIDLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OPENID_LOGIN);
RootBeanDefinition openIDFilter = null;
if (openIDLoginElt != null) {
@@ -404,6 +437,10 @@ void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authMa
if (openIDFilter != null) {
openIDFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
openIDFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+ if (authenticationFilterSecurityContextRepositoryRef != null) {
+ openIDFilter.getPropertyValues().addPropertyValue("securityContextRepository",
+ authenticationFilterSecurityContextRepositoryRef);
+ }
// Required by login page filter
this.openIDFilterId = this.pc.getReaderContext().generateBeanName(openIDFilter);
this.pc.registerBeanComponent(new BeanComponentDefinition(openIDFilter, this.openIDFilterId));
@@ -412,6 +449,31 @@ void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authMa
}
}
+ private void createSaml2LoginFilter(BeanReference authenticationManager,
+ BeanReference authenticationFilterSecurityContextRepositoryRef) {
+ Element saml2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGIN);
+ if (saml2LoginElt == null) {
+ return;
+ }
+ Saml2LoginBeanDefinitionParser parser = new Saml2LoginBeanDefinitionParser(this.csrfIgnoreRequestMatchers,
+ this.portMapper, this.portResolver, this.requestCache, this.allowSessionCreation, authenticationManager,
+ authenticationFilterSecurityContextRepositoryRef, this.authenticationProviders,
+ this.defaultEntryPointMappings);
+ BeanDefinition saml2WebSsoAuthenticationFilter = parser.parse(saml2LoginElt, this.pc);
+ this.saml2AuthorizationRequestFilter = parser.getSaml2WebSsoAuthenticationRequestFilter();
+
+ this.saml2AuthenticationFilterId = this.pc.getReaderContext().generateBeanName(saml2WebSsoAuthenticationFilter);
+ this.saml2AuthenticationRequestFilterId = this.pc.getReaderContext()
+ .generateBeanName(this.saml2AuthorizationRequestFilter);
+ this.saml2AuthenticationUrlToProviderName = parser.getSaml2AuthenticationUrlToProviderName();
+
+ // register the component
+ this.pc.registerBeanComponent(
+ new BeanComponentDefinition(saml2WebSsoAuthenticationFilter, this.saml2AuthenticationFilterId));
+ this.pc.registerBeanComponent(new BeanComponentDefinition(this.saml2AuthorizationRequestFilter,
+ this.saml2AuthenticationRequestFilterId));
+ }
+
/**
* Parses OpenID 1.0 and 2.0 - related parts of configuration xmls
* @param sessionStrategy sessionStrategy
@@ -666,6 +728,12 @@ void createLoginPageFilterIfNeeded() {
loginPageFilter.addPropertyValue("Oauth2LoginEnabled", true);
loginPageFilter.addPropertyValue("Oauth2AuthenticationUrlToClientName", this.oauth2LoginLinks);
}
+ if (this.saml2AuthenticationFilterId != null) {
+ loginPageFilter.addConstructorArgReference(this.saml2AuthenticationFilterId);
+ loginPageFilter.addPropertyValue("saml2LoginEnabled", true);
+ loginPageFilter.addPropertyValue("saml2AuthenticationUrlToProviderName",
+ this.saml2AuthenticationUrlToProviderName);
+ }
this.loginPageGenerationFilter = loginPageFilter.getBeanDefinition();
this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition();
}
@@ -682,7 +750,31 @@ void createLogoutFilter() {
this.rememberMeServicesId, this.csrfLogoutHandler);
this.logoutFilter = logoutParser.parse(logoutElt, this.pc);
this.logoutHandlers = logoutParser.getLogoutHandlers();
+ this.logoutSuccessHandler = logoutParser.getLogoutSuccessHandler();
+ }
+ }
+
+ private void createSaml2LogoutFilter() {
+ Element saml2LogoutElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGOUT);
+ if (saml2LogoutElt == null) {
+ return;
}
+ Saml2LogoutBeanDefinitionParser parser = new Saml2LogoutBeanDefinitionParser(this.logoutHandlers,
+ this.logoutSuccessHandler);
+ parser.parse(saml2LogoutElt, this.pc);
+ BeanDefinition saml2LogoutFilter = parser.getLogoutFilter();
+ BeanDefinition saml2LogoutRequestFilter = parser.getLogoutRequestFilter();
+ BeanDefinition saml2LogoutResponseFilter = parser.getLogoutResponseFilter();
+ this.saml2LogoutFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutFilter);
+ this.saml2LogoutRequestFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutRequestFilter);
+ this.saml2LogoutResponseFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutResponseFilter);
+
+ // register the component
+ this.pc.registerBeanComponent(new BeanComponentDefinition(saml2LogoutFilter, this.saml2LogoutFilterId));
+ this.pc.registerBeanComponent(
+ new BeanComponentDefinition(saml2LogoutRequestFilter, this.saml2LogoutRequestFilterId));
+ this.pc.registerBeanComponent(
+ new BeanComponentDefinition(saml2LogoutResponseFilter, this.saml2LogoutResponseFilterId));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -840,7 +932,8 @@ private BeanMetadataElement selectEntryPoint() {
if (formLoginElt != null && this.oauth2LoginEntryPoint != null) {
return this.formEntryPoint;
}
- // If form login was enabled through auto-config, and Oauth2 login was not
+ // If form login was enabled through auto-config, and Oauth2 login & Saml2
+ // login was not
// enabled then use form login
if (this.oauth2LoginEntryPoint == null) {
return this.formEntryPoint;
@@ -923,6 +1016,20 @@ List getFilters() {
filters.add(new OrderDecorator(this.authorizationCodeGrantFilter,
SecurityFilters.OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER));
}
+ if (this.saml2AuthenticationFilterId != null) {
+ filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationFilterId),
+ SecurityFilters.SAML2_AUTHENTICATION_FILTER));
+ filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationRequestFilterId),
+ SecurityFilters.SAML2_AUTHENTICATION_REQUEST_FILTER));
+ }
+ if (this.saml2LogoutFilterId != null) {
+ filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutFilterId),
+ SecurityFilters.SAML2_LOGOUT_FILTER));
+ filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutRequestFilterId),
+ SecurityFilters.SAML2_LOGOUT_REQUEST_FILTER));
+ filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutResponseFilterId),
+ SecurityFilters.SAML2_LOGOUT_RESPONSE_FILTER));
+ }
filters.add(new OrderDecorator(this.etf, SecurityFilters.EXCEPTION_TRANSLATION_FILTER));
return filters;
}
diff --git a/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java
new file mode 100644
index 00000000000..e3ac6c915ae
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2002-2016 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 org.springframework.security.config.http;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.parsing.BeanComponentDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.ManagedMap;
+import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.beans.factory.xml.XmlReaderContext;
+import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.config.Elements;
+import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler;
+import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
+import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
+import org.springframework.security.web.util.matcher.AnyRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
+
+class AuthorizationFilterParser implements BeanDefinitionParser {
+
+ private static final String ATT_USE_EXPRESSIONS = "use-expressions";
+
+ private static final String ATT_HTTP_METHOD = "method";
+
+ private static final String ATT_PATTERN = "pattern";
+
+ private static final String ATT_ACCESS = "access";
+
+ private static final String ATT_SERVLET_PATH = "servlet-path";
+
+ private String authorizationManagerRef;
+
+ @Override
+ public BeanDefinition parse(Element element, ParserContext parserContext) {
+ if (!isUseExpressions(element)) {
+ parserContext.getReaderContext().error("AuthorizationManager must be used with `use-expressions=\"true\"",
+ element);
+ return null;
+ }
+ this.authorizationManagerRef = createAuthorizationManager(element, parserContext);
+ BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.rootBeanDefinition(AuthorizationFilter.class);
+ filterBuilder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
+ BeanDefinition filter = filterBuilder.addConstructorArgReference(this.authorizationManagerRef)
+ .getBeanDefinition();
+ String id = element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE);
+ if (StringUtils.hasText(id)) {
+ parserContext.registerComponent(new BeanComponentDefinition(filter, id));
+ parserContext.getRegistry().registerBeanDefinition(id, filter);
+ }
+ return filter;
+ }
+
+ String getAuthorizationManagerRef() {
+ return this.authorizationManagerRef;
+ }
+
+ private String createAuthorizationManager(Element element, ParserContext parserContext) {
+ XmlReaderContext context = parserContext.getReaderContext();
+ String authorizationManagerRef = element.getAttribute("authorization-manager-ref");
+ if (StringUtils.hasText(authorizationManagerRef)) {
+ return authorizationManagerRef;
+ }
+ Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
+ String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null;
+ if (expressionHandlerRef == null) {
+ expressionHandlerRef = registerDefaultExpressionHandler(parserContext);
+ }
+ MatcherType matcherType = MatcherType.fromElement(element);
+ ManagedMap matcherToExpression = new ManagedMap<>();
+ List interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_URL);
+ for (Element interceptMessage : interceptMessages) {
+ String accessExpression = interceptMessage.getAttribute(ATT_ACCESS);
+ BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder
+ .rootBeanDefinition(WebExpressionAuthorizationManager.class);
+ authorizationManager.addPropertyReference("expressionHandler", expressionHandlerRef);
+ authorizationManager.addConstructorArgValue(accessExpression);
+ BeanMetadataElement matcher = createMatcher(matcherType, interceptMessage, parserContext);
+ matcherToExpression.put(matcher, authorizationManager.getBeanDefinition());
+ }
+ BeanDefinitionBuilder mds = BeanDefinitionBuilder
+ .rootBeanDefinition(RequestMatcherDelegatingAuthorizationManagerFactory.class);
+ mds.setFactoryMethod("createRequestMatcherDelegatingAuthorizationManager");
+ mds.addConstructorArgValue(matcherToExpression);
+ return context.registerWithGeneratedName(mds.getBeanDefinition());
+ }
+
+ private BeanMetadataElement createMatcher(MatcherType matcherType, Element urlElt, ParserContext parserContext) {
+ String path = urlElt.getAttribute(ATT_PATTERN);
+ String matcherRef = urlElt.getAttribute(HttpSecurityBeanDefinitionParser.ATT_REQUEST_MATCHER_REF);
+ boolean hasMatcherRef = StringUtils.hasText(matcherRef);
+ if (!hasMatcherRef && !StringUtils.hasText(path)) {
+ parserContext.getReaderContext().error("path attribute cannot be empty or null", urlElt);
+ }
+ String method = urlElt.getAttribute(ATT_HTTP_METHOD);
+ if (!StringUtils.hasText(method)) {
+ method = null;
+ }
+ String servletPath = urlElt.getAttribute(ATT_SERVLET_PATH);
+ if (!StringUtils.hasText(servletPath)) {
+ servletPath = null;
+ }
+ else if (!MatcherType.mvc.equals(matcherType)) {
+ parserContext.getReaderContext().error(
+ ATT_SERVLET_PATH + " is not applicable for request-matcher: '" + matcherType.name() + "'", urlElt);
+ }
+ return hasMatcherRef ? new RuntimeBeanReference(matcherRef)
+ : matcherType.createMatcher(parserContext, path, method, servletPath);
+ }
+
+ String registerDefaultExpressionHandler(ParserContext pc) {
+ BeanDefinition expressionHandler = GrantedAuthorityDefaultsParserUtils.registerWithDefaultRolePrefix(pc,
+ DefaultWebSecurityExpressionHandlerBeanFactory.class);
+ String expressionHandlerRef = pc.getReaderContext().generateBeanName(expressionHandler);
+ pc.registerBeanComponent(new BeanComponentDefinition(expressionHandler, expressionHandlerRef));
+ return expressionHandlerRef;
+ }
+
+ boolean isUseExpressions(Element elt) {
+ String useExpressions = elt.getAttribute(ATT_USE_EXPRESSIONS);
+ return !StringUtils.hasText(useExpressions) || "true".equals(useExpressions);
+ }
+
+ private static class RequestMatcherDelegatingAuthorizationManagerFactory {
+
+ private static AuthorizationManager createRequestMatcherDelegatingAuthorizationManager(
+ Map> beans) {
+ RequestMatcherDelegatingAuthorizationManager.Builder builder = RequestMatcherDelegatingAuthorizationManager
+ .builder();
+ for (Map.Entry> entry : beans
+ .entrySet()) {
+ builder.add(entry.getKey(), entry.getValue());
+ }
+ return builder.add(AnyRequestMatcher.INSTANCE, AuthenticatedAuthorizationManager.authenticated()).build();
+ }
+
+ }
+
+ static class DefaultWebSecurityExpressionHandlerBeanFactory
+ extends GrantedAuthorityDefaultsParserUtils.AbstractGrantedAuthorityDefaultsBeanFactory {
+
+ private DefaultHttpSecurityExpressionHandler handler = new DefaultHttpSecurityExpressionHandler();
+
+ @Override
+ public DefaultHttpSecurityExpressionHandler getBean() {
+ this.handler.setDefaultRolePrefix(this.rolePrefix);
+ return this.handler;
+ }
+
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java
index 7f42ff724e2..b980f635a73 100644
--- a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -36,6 +36,9 @@
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter;
+import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter;
+import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter;
+import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter;
import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter;
import org.springframework.security.web.header.writers.HpkpHeaderWriter;
import org.springframework.security.web.header.writers.HstsHeaderWriter;
@@ -122,6 +125,12 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
private static final String PERMISSIONS_POLICY_ELEMENT = "permissions-policy";
+ private static final String CROSS_ORIGIN_OPENER_POLICY_ELEMENT = "cross-origin-opener-policy";
+
+ private static final String CROSS_ORIGIN_EMBEDDER_POLICY_ELEMENT = "cross-origin-embedder-policy";
+
+ private static final String CROSS_ORIGIN_RESOURCE_POLICY_ELEMENT = "cross-origin-resource-policy";
+
private static final String ALLOW_FROM = "ALLOW-FROM";
private ManagedList headerWriters;
@@ -144,6 +153,9 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
parseReferrerPolicyElement(element, parserContext);
parseFeaturePolicyElement(element, parserContext);
parsePermissionsPolicyElement(element, parserContext);
+ parseCrossOriginOpenerPolicy(disabled, element);
+ parseCrossOriginEmbedderPolicy(disabled, element);
+ parseCrossOriginResourcePolicy(disabled, element);
parseHeaderElements(element);
boolean noWriters = this.headerWriters.isEmpty();
if (disabled && !noWriters) {
@@ -376,6 +388,75 @@ private void addPermissionsPolicy(Element permissionsPolicyElement, ParserContex
this.headerWriters.add(headersWriter.getBeanDefinition());
}
+ private void parseCrossOriginOpenerPolicy(boolean elementDisabled, Element element) {
+ if (elementDisabled || element == null) {
+ return;
+ }
+ CrossOriginOpenerPolicyHeaderWriter writer = new CrossOriginOpenerPolicyHeaderWriter();
+ Element crossOriginOpenerPolicyElement = DomUtils.getChildElementByTagName(element,
+ CROSS_ORIGIN_OPENER_POLICY_ELEMENT);
+ if (crossOriginOpenerPolicyElement != null) {
+ addCrossOriginOpenerPolicy(crossOriginOpenerPolicyElement, writer);
+ }
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder
+ .genericBeanDefinition(CrossOriginOpenerPolicyHeaderWriter.class, () -> writer);
+ this.headerWriters.add(builder.getBeanDefinition());
+ }
+
+ private void parseCrossOriginEmbedderPolicy(boolean elementDisabled, Element element) {
+ if (elementDisabled || element == null) {
+ return;
+ }
+ CrossOriginEmbedderPolicyHeaderWriter writer = new CrossOriginEmbedderPolicyHeaderWriter();
+ Element crossOriginEmbedderPolicyElement = DomUtils.getChildElementByTagName(element,
+ CROSS_ORIGIN_EMBEDDER_POLICY_ELEMENT);
+ if (crossOriginEmbedderPolicyElement != null) {
+ addCrossOriginEmbedderPolicy(crossOriginEmbedderPolicyElement, writer);
+ }
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder
+ .genericBeanDefinition(CrossOriginEmbedderPolicyHeaderWriter.class, () -> writer);
+ this.headerWriters.add(builder.getBeanDefinition());
+ }
+
+ private void parseCrossOriginResourcePolicy(boolean elementDisabled, Element element) {
+ if (elementDisabled || element == null) {
+ return;
+ }
+ CrossOriginResourcePolicyHeaderWriter writer = new CrossOriginResourcePolicyHeaderWriter();
+ Element crossOriginResourcePolicyElement = DomUtils.getChildElementByTagName(element,
+ CROSS_ORIGIN_RESOURCE_POLICY_ELEMENT);
+ if (crossOriginResourcePolicyElement != null) {
+ addCrossOriginResourcePolicy(crossOriginResourcePolicyElement, writer);
+ }
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder
+ .genericBeanDefinition(CrossOriginResourcePolicyHeaderWriter.class, () -> writer);
+ this.headerWriters.add(builder.getBeanDefinition());
+ }
+
+ private void addCrossOriginResourcePolicy(Element crossOriginResourcePolicyElement,
+ CrossOriginResourcePolicyHeaderWriter writer) {
+ String policy = crossOriginResourcePolicyElement.getAttribute(ATT_POLICY);
+ if (StringUtils.hasText(policy)) {
+ writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.from(policy));
+ }
+ }
+
+ private void addCrossOriginEmbedderPolicy(Element crossOriginEmbedderPolicyElement,
+ CrossOriginEmbedderPolicyHeaderWriter writer) {
+ String policy = crossOriginEmbedderPolicyElement.getAttribute(ATT_POLICY);
+ if (StringUtils.hasText(policy)) {
+ writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.from(policy));
+ }
+ }
+
+ private void addCrossOriginOpenerPolicy(Element crossOriginOpenerPolicyElement,
+ CrossOriginOpenerPolicyHeaderWriter writer) {
+ String policy = crossOriginOpenerPolicyElement.getAttribute(ATT_POLICY);
+ if (StringUtils.hasText(policy)) {
+ writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.from(policy));
+ }
+ }
+
private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) {
context.getReaderContext().error("Only one of '" + attrName + "' or '" + otherAttrName + "' can be set.",
element);
diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
index e3339b3b136..e7539558b56 100644
--- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
+++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
@@ -41,6 +41,7 @@
import org.springframework.security.config.Elements;
import org.springframework.security.config.http.GrantedAuthorityDefaultsParserUtils.AbstractGrantedAuthorityDefaultsBeanFactory;
import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
@@ -59,6 +60,7 @@
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.NullSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
@@ -67,6 +69,8 @@
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
+import org.springframework.security.web.session.DisableEncodeUrlFilter;
+import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;
@@ -104,10 +108,16 @@ class HttpConfigurationBuilder {
private static final String ATT_SECURITY_CONTEXT_REPOSITORY = "security-context-repository-ref";
+ private static final String ATT_SECURITY_CONTEXT_EXPLICIT_SAVE = "security-context-explicit-save";
+
private static final String ATT_INVALID_SESSION_STRATEGY_REF = "invalid-session-strategy-ref";
private static final String ATT_DISABLE_URL_REWRITING = "disable-url-rewriting";
+ private static final String ATT_USE_AUTHORIZATION_MGR = "use-authorization-manager";
+
+ private static final String ATT_AUTHORIZATION_MGR = "authorization-manager-ref";
+
private static final String ATT_ACCESS_MGR = "access-decision-manager-ref";
private static final String ATT_ONCE_PER_REQUEST = "once-per-request";
@@ -144,6 +154,8 @@ class HttpConfigurationBuilder {
private BeanDefinition securityContextPersistenceFilter;
+ private BeanDefinition forceEagerSessionCreationFilter;
+
private BeanReference contextRepoRef;
private BeanReference sessionRegistryRef;
@@ -176,6 +188,8 @@ class HttpConfigurationBuilder {
private BeanDefinition csrfFilter;
+ private BeanDefinition disableUrlRewriteFilter;
+
private BeanDefinition wellKnownChangePasswordRedirectFilter;
private BeanMetadataElement csrfLogoutHandler;
@@ -201,15 +215,17 @@ class HttpConfigurationBuilder {
String createSession = element.getAttribute(ATT_CREATE_SESSION);
this.sessionPolicy = !StringUtils.hasText(createSession) ? SessionCreationPolicy.IF_REQUIRED
: createPolicy(createSession);
+ createForceEagerSessionCreationFilter();
+ createDisableEncodeUrlFilter();
createCsrfFilter();
- createSecurityContextPersistenceFilter();
+ createSecurityPersistence();
createSessionManagementFilters();
createWebAsyncManagerFilter();
createRequestCacheFilter();
createServletApiFilter(authenticationManager);
createJaasApiFilter();
createChannelProcessingFilter();
- createFilterSecurityInterceptor(authenticationManager);
+ createFilterSecurity(authenticationManager);
createAddHeadersFilter();
createCorsFilter();
createWellKnownChangePasswordRedirectFilter();
@@ -278,19 +294,51 @@ static String createPath(String path, boolean lowerCase) {
return lowerCase ? path.toLowerCase() : path;
}
+ BeanReference getSecurityContextRepositoryForAuthenticationFilters() {
+ return (isExplicitSave()) ? this.contextRepoRef : null;
+ }
+
+ private void createSecurityPersistence() {
+ createSecurityContextRepository();
+ if (isExplicitSave()) {
+ createSecurityContextHolderFilter();
+ }
+ else {
+ createSecurityContextPersistenceFilter();
+ }
+ }
+
+ private boolean isExplicitSave() {
+ String explicitSaveAttr = this.httpElt.getAttribute(ATT_SECURITY_CONTEXT_EXPLICIT_SAVE);
+ return Boolean.parseBoolean(explicitSaveAttr);
+ }
+
+ private void createForceEagerSessionCreationFilter() {
+ if (this.sessionPolicy == SessionCreationPolicy.ALWAYS) {
+ this.forceEagerSessionCreationFilter = new RootBeanDefinition(ForceEagerSessionCreationFilter.class);
+ }
+ }
+
private void createSecurityContextPersistenceFilter() {
BeanDefinitionBuilder scpf = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextPersistenceFilter.class);
- String repoRef = this.httpElt.getAttribute(ATT_SECURITY_CONTEXT_REPOSITORY);
- String disableUrlRewriting = this.httpElt.getAttribute(ATT_DISABLE_URL_REWRITING);
- if (!StringUtils.hasText(disableUrlRewriting)) {
- disableUrlRewriting = "true";
- }
- if (StringUtils.hasText(repoRef)) {
- if (this.sessionPolicy == SessionCreationPolicy.ALWAYS) {
- scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE);
- }
+ switch (this.sessionPolicy) {
+ case ALWAYS:
+ scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE);
+ break;
+ case NEVER:
+ scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE);
+ break;
+ default:
+ scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE);
}
- else {
+ scpf.addConstructorArgValue(this.contextRepoRef);
+
+ this.securityContextPersistenceFilter = scpf.getBeanDefinition();
+ }
+
+ private void createSecurityContextRepository() {
+ String repoRef = this.httpElt.getAttribute(ATT_SECURITY_CONTEXT_REPOSITORY);
+ if (!StringUtils.hasText(repoRef)) {
BeanDefinitionBuilder contextRepo;
if (this.sessionPolicy == SessionCreationPolicy.STATELESS) {
contextRepo = BeanDefinitionBuilder.rootBeanDefinition(NullSecurityContextRepository.class);
@@ -300,17 +348,14 @@ private void createSecurityContextPersistenceFilter() {
switch (this.sessionPolicy) {
case ALWAYS:
contextRepo.addPropertyValue("allowSessionCreation", Boolean.TRUE);
- scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE);
break;
case NEVER:
contextRepo.addPropertyValue("allowSessionCreation", Boolean.FALSE);
- scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE);
break;
default:
contextRepo.addPropertyValue("allowSessionCreation", Boolean.TRUE);
- scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE);
}
- if ("true".equals(disableUrlRewriting)) {
+ if (isDisableUrlRewriting()) {
contextRepo.addPropertyValue("disableUrlRewriting", Boolean.TRUE);
}
}
@@ -320,9 +365,17 @@ private void createSecurityContextPersistenceFilter() {
}
this.contextRepoRef = new RuntimeBeanReference(repoRef);
- scpf.addConstructorArgValue(this.contextRepoRef);
+ }
- this.securityContextPersistenceFilter = scpf.getBeanDefinition();
+ private boolean isDisableUrlRewriting() {
+ String disableUrlRewriting = this.httpElt.getAttribute(ATT_DISABLE_URL_REWRITING);
+ return !"false".equals(disableUrlRewriting);
+ }
+
+ private void createSecurityContextHolderFilter() {
+ BeanDefinitionBuilder filter = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextHolderFilter.class);
+ filter.addConstructorArgValue(this.contextRepoRef);
+ this.securityContextPersistenceFilter = filter.getBeanDefinition();
}
private void createSessionManagementFilters() {
@@ -627,6 +680,35 @@ private void createRequestCacheFilter() {
this.requestCacheAwareFilter.getConstructorArgumentValues().addGenericArgumentValue(this.requestCache);
}
+ private void createFilterSecurity(BeanReference authManager) {
+ boolean useAuthorizationManager = Boolean.parseBoolean(this.httpElt.getAttribute(ATT_USE_AUTHORIZATION_MGR));
+ if (useAuthorizationManager) {
+ createAuthorizationFilter();
+ return;
+ }
+ if (StringUtils.hasText(this.httpElt.getAttribute(ATT_AUTHORIZATION_MGR))) {
+ createAuthorizationFilter();
+ return;
+ }
+ createFilterSecurityInterceptor(authManager);
+ }
+
+ private void createAuthorizationFilter() {
+ AuthorizationFilterParser authorizationFilterParser = new AuthorizationFilterParser();
+ BeanDefinition fsiBean = authorizationFilterParser.parse(this.httpElt, this.pc);
+ String fsiId = this.pc.getReaderContext().generateBeanName(fsiBean);
+ this.pc.registerBeanComponent(new BeanComponentDefinition(fsiBean, fsiId));
+ // Create and register a AuthorizationManagerWebInvocationPrivilegeEvaluator for
+ // use with
+ // taglibs etc.
+ BeanDefinition wipe = BeanDefinitionBuilder
+ .rootBeanDefinition(AuthorizationManagerWebInvocationPrivilegeEvaluator.class)
+ .addConstructorArgReference(authorizationFilterParser.getAuthorizationManagerRef()).getBeanDefinition();
+ this.pc.registerBeanComponent(
+ new BeanComponentDefinition(wipe, this.pc.getReaderContext().generateBeanName(wipe)));
+ this.fsi = new RuntimeBeanReference(fsiId);
+ }
+
private void createFilterSecurityInterceptor(BeanReference authManager) {
boolean useExpressions = FilterInvocationSecurityMetadataSourceParser.isUseExpressions(this.httpElt);
RootBeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser
@@ -685,6 +767,12 @@ private void createCorsFilter() {
}
+ private void createDisableEncodeUrlFilter() {
+ if (isDisableUrlRewriting()) {
+ this.disableUrlRewriteFilter = new RootBeanDefinition(DisableEncodeUrlFilter.class);
+ }
+ }
+
private void createCsrfFilter() {
Element elmt = DomUtils.getChildElementByTagName(this.httpElt, Elements.CSRF);
this.csrfParser = new CsrfBeanDefinitionParser();
@@ -724,6 +812,13 @@ BeanReference getRequestCache() {
List getFilters() {
List filters = new ArrayList<>();
+ if (this.forceEagerSessionCreationFilter != null) {
+ filters.add(new OrderDecorator(this.forceEagerSessionCreationFilter,
+ SecurityFilters.FORCE_EAGER_SESSION_FILTER));
+ }
+ if (this.disableUrlRewriteFilter != null) {
+ filters.add(new OrderDecorator(this.disableUrlRewriteFilter, SecurityFilters.DISABLE_ENCODE_URL_FILTER));
+ }
if (this.cpf != null) {
filters.add(new OrderDecorator(this.cpf, SecurityFilters.CHANNEL_FILTER));
}
diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java
index 970245d1345..7d0be016ce8 100644
--- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java
@@ -144,9 +144,11 @@ private BeanReference createFilterChain(Element element, ParserContext pc) {
boolean forceAutoConfig = isDefaultHttpConfig(element);
HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element, forceAutoConfig, pc, portMapper,
portResolver, authenticationManager);
+ httpBldr.getSecurityContextRepositoryForAuthenticationFilters();
AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, forceAutoConfig, pc,
httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager,
- httpBldr.getSessionStrategy(), portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
+ httpBldr.getSecurityContextRepositoryForAuthenticationFilters(), httpBldr.getSessionStrategy(),
+ portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers());
httpBldr.setEntryPoint(authBldr.getEntryPointBean());
httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean());
diff --git a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java
index 65c1b3b931a..51d9462d5e0 100644
--- a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java
@@ -59,6 +59,8 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
private boolean csrfEnabled;
+ private BeanMetadataElement logoutSuccessHandler;
+
LogoutBeanDefinitionParser(String loginPageUrl, String rememberMeServices, BeanMetadataElement csrfLogoutHandler) {
this.defaultLogoutUrl = loginPageUrl + "?logout";
this.rememberMeServices = rememberMeServices;
@@ -98,6 +100,7 @@ public BeanDefinition parse(Element element, ParserContext pc) {
pc.extractSource(element));
}
builder.addConstructorArgReference(successHandlerRef);
+ this.logoutSuccessHandler = new RuntimeBeanReference(successHandlerRef);
}
else {
// Use the logout URL if no handler set
@@ -137,4 +140,8 @@ ManagedList getLogoutHandlers() {
return this.logoutHandlers;
}
+ BeanMetadataElement getLogoutSuccessHandler() {
+ return this.logoutSuccessHandler;
+ }
+
}
diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java
index 3a72f62585d..f2c1ebd0f09 100644
--- a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java
@@ -50,6 +50,8 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
private final BeanReference authenticationManager;
+ private final BeanReference authenticationFilterSecurityContextRepositoryRef;
+
private BeanDefinition defaultAuthorizedClientRepository;
private BeanDefinition authorizationRequestRedirectFilter;
@@ -58,9 +60,11 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
private BeanDefinition authorizationCodeAuthenticationProvider;
- OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager) {
+ OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager,
+ BeanReference authenticationFilterSecurityContextRepositoryRef) {
this.requestCache = requestCache;
this.authenticationManager = authenticationManager;
+ this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef;
}
@Override
@@ -92,11 +96,16 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
this.authorizationRequestRedirectFilter = authorizationRequestRedirectFilterBuilder
.addPropertyValue("authorizationRequestRepository", authorizationRequestRepository)
.addPropertyValue("requestCache", this.requestCache).getBeanDefinition();
- this.authorizationCodeGrantFilter = BeanDefinitionBuilder
+ BeanDefinitionBuilder authorizationCodeGrantFilterBldr = BeanDefinitionBuilder
.rootBeanDefinition(OAuth2AuthorizationCodeGrantFilter.class)
.addConstructorArgValue(clientRegistrationRepository).addConstructorArgValue(authorizedClientRepository)
.addConstructorArgValue(this.authenticationManager)
- .addPropertyValue("authorizationRequestRepository", authorizationRequestRepository).getBeanDefinition();
+ .addPropertyValue("authorizationRequestRepository", authorizationRequestRepository);
+ if (this.authenticationFilterSecurityContextRepositoryRef != null) {
+ authorizationCodeGrantFilterBldr.addPropertyValue("securityContextRepository",
+ this.authenticationFilterSecurityContextRepositoryRef);
+ }
+ this.authorizationCodeGrantFilter = authorizationCodeGrantFilterBldr.getBeanDefinition();
BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(authorizationCodeGrantElt);
this.authorizationCodeAuthenticationProvider = BeanDefinitionBuilder
diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java
new file mode 100644
index 00000000000..147166c471f
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java
@@ -0,0 +1,323 @@
+/*
+ * 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 org.springframework.security.config.http;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanReference;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.parsing.BeanComponentDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.ResolvableType;
+import org.springframework.security.config.Elements;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
+import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
+
+/**
+ * SAML 2.0 Login {@link BeanDefinitionParser}
+ *
+ * @author Marcus da Coregio
+ * @since 5.7
+ */
+final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser {
+
+ private static final String DEFAULT_LOGIN_URI = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL;
+
+ private static final String DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL = "/saml2/authenticate/{registrationId}";
+
+ private static final String ATT_LOGIN_PROCESSING_URL = "login-processing-url";
+
+ private static final String ATT_LOGIN_PAGE = "login-page";
+
+ private static final String ELT_RELYING_PARTY_REGISTRATION = "relying-party-registration";
+
+ private static final String ELT_REGISTRATION_ID = "registration-id";
+
+ private static final String ATT_AUTHENTICATION_FAILURE_HANDLER_REF = "authentication-failure-handler-ref";
+
+ private static final String ATT_AUTHENTICATION_SUCCESS_HANDLER_REF = "authentication-success-handler-ref";
+
+ private static final String ATT_AUTHENTICATION_MANAGER_REF = "authentication-manager-ref";
+
+ private final List csrfIgnoreRequestMatchers;
+
+ private final BeanReference portMapper;
+
+ private final BeanReference portResolver;
+
+ private final BeanReference requestCache;
+
+ private final boolean allowSessionCreation;
+
+ private final BeanReference authenticationManager;
+
+ private final BeanReference authenticationFilterSecurityContextRepositoryRef;
+
+ private final List authenticationProviders;
+
+ private final Map entryPoints;
+
+ private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
+
+ private BeanDefinition saml2WebSsoAuthenticationRequestFilter;
+
+ private BeanDefinition saml2AuthenticationUrlToProviderName;
+
+ Saml2LoginBeanDefinitionParser(List csrfIgnoreRequestMatchers, BeanReference portMapper,
+ BeanReference portResolver, BeanReference requestCache, boolean allowSessionCreation,
+ BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef,
+ List authenticationProviders, Map entryPoints) {
+ this.csrfIgnoreRequestMatchers = csrfIgnoreRequestMatchers;
+ this.portMapper = portMapper;
+ this.portResolver = portResolver;
+ this.requestCache = requestCache;
+ this.allowSessionCreation = allowSessionCreation;
+ this.authenticationManager = authenticationManager;
+ this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef;
+ this.authenticationProviders = authenticationProviders;
+ this.entryPoints = entryPoints;
+ }
+
+ @Override
+ public BeanDefinition parse(Element element, ParserContext pc) {
+ String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL);
+ if (StringUtils.hasText(loginProcessingUrl)) {
+ this.loginProcessingUrl = loginProcessingUrl;
+ }
+ BeanDefinition saml2LoginBeanConfig = BeanDefinitionBuilder.rootBeanDefinition(Saml2LoginBeanConfig.class)
+ .getBeanDefinition();
+ String saml2LoginBeanConfigId = pc.getReaderContext().generateBeanName(saml2LoginBeanConfig);
+ pc.registerBeanComponent(new BeanComponentDefinition(saml2LoginBeanConfig, saml2LoginBeanConfigId));
+ registerDefaultCsrfOverride();
+ BeanMetadataElement relyingPartyRegistrationRepository = Saml2LoginBeanDefinitionParserUtils
+ .getRelyingPartyRegistrationRepository(element);
+ BeanMetadataElement authenticationRequestRepository = Saml2LoginBeanDefinitionParserUtils
+ .getAuthenticationRequestRepository(element);
+ BeanMetadataElement authenticationRequestResolver = Saml2LoginBeanDefinitionParserUtils
+ .getAuthenticationRequestResolver(element);
+ if (authenticationRequestResolver == null) {
+ authenticationRequestResolver = Saml2LoginBeanDefinitionParserUtils
+ .createDefaultAuthenticationRequestResolver(relyingPartyRegistrationRepository);
+ }
+ BeanMetadataElement authenticationConverter = Saml2LoginBeanDefinitionParserUtils
+ .getAuthenticationConverter(element);
+ if (authenticationConverter == null) {
+ if (!this.loginProcessingUrl.contains("{registrationId}")) {
+ pc.getReaderContext().error("loginProcessingUrl must contain {registrationId} path variable", element);
+ }
+ authenticationConverter = Saml2LoginBeanDefinitionParserUtils
+ .createDefaultAuthenticationConverter(relyingPartyRegistrationRepository);
+ }
+ // Configure the Saml2WebSsoAuthenticationFilter
+ BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder = BeanDefinitionBuilder
+ .rootBeanDefinition(Saml2WebSsoAuthenticationFilter.class)
+ .addConstructorArgValue(authenticationConverter).addConstructorArgValue(this.loginProcessingUrl)
+ .addPropertyValue("authenticationRequestRepository", authenticationRequestRepository);
+ resolveLoginPage(element, pc);
+ resolveAuthenticationSuccessHandler(element, saml2WebSsoAuthenticationFilterBuilder);
+ resolveAuthenticationFailureHandler(element, saml2WebSsoAuthenticationFilterBuilder);
+ resolveAuthenticationManager(element, saml2WebSsoAuthenticationFilterBuilder);
+ resolveSecurityContextRepository(element, saml2WebSsoAuthenticationFilterBuilder);
+ // Configure the Saml2WebSsoAuthenticationRequestFilter
+ this.saml2WebSsoAuthenticationRequestFilter = BeanDefinitionBuilder
+ .rootBeanDefinition(Saml2WebSsoAuthenticationRequestFilter.class)
+ .addConstructorArgValue(authenticationRequestResolver)
+ .addPropertyValue("authenticationRequestRepository", authenticationRequestRepository)
+ .getBeanDefinition();
+ BeanDefinition saml2AuthenticationProvider = Saml2LoginBeanDefinitionParserUtils.createAuthenticationProvider();
+ this.authenticationProviders.add(
+ new RuntimeBeanReference(pc.getReaderContext().registerWithGeneratedName(saml2AuthenticationProvider)));
+ this.saml2AuthenticationUrlToProviderName = BeanDefinitionBuilder.rootBeanDefinition(Map.class)
+ .setFactoryMethodOnBean("getAuthenticationUrlToProviderName", saml2LoginBeanConfigId)
+ .getBeanDefinition();
+ return saml2WebSsoAuthenticationFilterBuilder.getBeanDefinition();
+ }
+
+ private void resolveAuthenticationManager(Element element,
+ BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) {
+ String authenticationManagerRef = element.getAttribute(ATT_AUTHENTICATION_MANAGER_REF);
+ if (StringUtils.hasText(authenticationManagerRef)) {
+ saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationManager",
+ authenticationManagerRef);
+ }
+ else {
+ saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationManager",
+ this.authenticationManager);
+ }
+ }
+
+ private void resolveSecurityContextRepository(Element element,
+ BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) {
+ if (this.authenticationFilterSecurityContextRepositoryRef != null) {
+ saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("securityContextRepository",
+ this.authenticationFilterSecurityContextRepositoryRef);
+ }
+ }
+
+ private void resolveLoginPage(Element element, ParserContext parserContext) {
+ String loginPage = element.getAttribute(ATT_LOGIN_PAGE);
+ Object source = parserContext.extractSource(element);
+ BeanDefinition saml2LoginAuthenticationEntryPoint = null;
+ if (StringUtils.hasText(loginPage)) {
+ WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source);
+ saml2LoginAuthenticationEntryPoint = BeanDefinitionBuilder
+ .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginPage)
+ .addPropertyValue("portMapper", this.portMapper).addPropertyValue("portResolver", this.portResolver)
+ .getBeanDefinition();
+ }
+ else {
+ Map identityProviderUrlMap = getIdentityProviderUrlMap(element);
+ if (identityProviderUrlMap.size() == 1) {
+ String loginUrl = identityProviderUrlMap.entrySet().iterator().next().getKey();
+ saml2LoginAuthenticationEntryPoint = BeanDefinitionBuilder
+ .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginUrl)
+ .addPropertyValue("portMapper", this.portMapper)
+ .addPropertyValue("portResolver", this.portResolver).getBeanDefinition();
+ }
+ }
+ if (saml2LoginAuthenticationEntryPoint != null) {
+ BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
+ .rootBeanDefinition(AntPathRequestMatcher.class);
+ requestMatcherBuilder.addConstructorArgValue(this.loginProcessingUrl);
+ BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition();
+ this.entryPoints.put(requestMatcher, saml2LoginAuthenticationEntryPoint);
+ }
+ }
+
+ private void resolveAuthenticationFailureHandler(Element element,
+ BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) {
+ String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF);
+ if (StringUtils.hasText(authenticationFailureHandlerRef)) {
+ saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler",
+ authenticationFailureHandlerRef);
+ }
+ else {
+ BeanDefinitionBuilder failureHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition(
+ "org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler");
+ failureHandlerBuilder.addConstructorArgValue(
+ DEFAULT_LOGIN_URI + "?" + DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME);
+ failureHandlerBuilder.addPropertyValue("allowSessionCreation", this.allowSessionCreation);
+ saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationFailureHandler",
+ failureHandlerBuilder.getBeanDefinition());
+ }
+ }
+
+ private void resolveAuthenticationSuccessHandler(Element element,
+ BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) {
+ String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF);
+ if (StringUtils.hasText(authenticationSuccessHandlerRef)) {
+ saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler",
+ authenticationSuccessHandlerRef);
+ }
+ else {
+ BeanDefinitionBuilder successHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition(
+ "org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler")
+ .addPropertyValue("requestCache", this.requestCache);
+ saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationSuccessHandler",
+ successHandlerBuilder.getBeanDefinition());
+ }
+ }
+
+ private void registerDefaultCsrfOverride() {
+ BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
+ .rootBeanDefinition(AntPathRequestMatcher.class);
+ requestMatcherBuilder.addConstructorArgValue(this.loginProcessingUrl);
+ BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition();
+ this.csrfIgnoreRequestMatchers.add(requestMatcher);
+ }
+
+ private Map getIdentityProviderUrlMap(Element element) {
+ Map idps = new LinkedHashMap<>();
+ Element relyingPartyRegistrationsElt = DomUtils.getChildElementByTagName(
+ element.getOwnerDocument().getDocumentElement(), Elements.RELYING_PARTY_REGISTRATIONS);
+ String authenticationRequestProcessingUrl = DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL;
+ if (relyingPartyRegistrationsElt != null) {
+ List relyingPartyRegList = DomUtils.getChildElementsByTagName(relyingPartyRegistrationsElt,
+ ELT_RELYING_PARTY_REGISTRATION);
+ for (Element relyingPartyReg : relyingPartyRegList) {
+ String registrationId = relyingPartyReg.getAttribute(ELT_REGISTRATION_ID);
+ idps.put(authenticationRequestProcessingUrl.replace("{registrationId}", registrationId),
+ registrationId);
+ }
+ }
+ return idps;
+ }
+
+ BeanDefinition getSaml2WebSsoAuthenticationRequestFilter() {
+ return this.saml2WebSsoAuthenticationRequestFilter;
+ }
+
+ BeanDefinition getSaml2AuthenticationUrlToProviderName() {
+ return this.saml2AuthenticationUrlToProviderName;
+ }
+
+ /**
+ * Wrapper bean class to provide configuration from applicationContext
+ */
+ public static class Saml2LoginBeanConfig implements ApplicationContextAware {
+
+ private ApplicationContext context;
+
+ @SuppressWarnings({ "unchecked", "unused" })
+ Map getAuthenticationUrlToProviderName() {
+ Iterable relyingPartyRegistrations = null;
+ RelyingPartyRegistrationRepository relyingPartyRegistrationRepository = this.context
+ .getBean(RelyingPartyRegistrationRepository.class);
+ ResolvableType type = ResolvableType.forInstance(relyingPartyRegistrationRepository).as(Iterable.class);
+ if (type != ResolvableType.NONE
+ && RelyingPartyRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
+ relyingPartyRegistrations = (Iterable) relyingPartyRegistrationRepository;
+ }
+ if (relyingPartyRegistrations == null) {
+ return Collections.emptyMap();
+ }
+ String authenticationRequestProcessingUrl = DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL;
+ Map saml2AuthenticationUrlToProviderName = new HashMap<>();
+ relyingPartyRegistrations.forEach((registration) -> saml2AuthenticationUrlToProviderName.put(
+ authenticationRequestProcessingUrl.replace("{registrationId}", registration.getRegistrationId()),
+ registration.getRegistrationId()));
+ return saml2AuthenticationUrlToProviderName;
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext context) throws BeansException {
+ this.context = context;
+ }
+
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java
new file mode 100644
index 00000000000..225bcbe0870
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java
@@ -0,0 +1,107 @@
+/*
+ * 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 org.springframework.security.config.http;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
+import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
+import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Marcus da Coregio
+ * @since 5.7
+ */
+final class Saml2LoginBeanDefinitionParserUtils {
+
+ private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
+
+ private static final String ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF = "authentication-request-repository-ref";
+
+ private static final String ATT_AUTHENTICATION_REQUEST_RESOLVER_REF = "authentication-request-resolver-ref";
+
+ private static final String ATT_AUTHENTICATION_CONVERTER = "authentication-converter-ref";
+
+ private Saml2LoginBeanDefinitionParserUtils() {
+ }
+
+ static BeanMetadataElement getRelyingPartyRegistrationRepository(Element element) {
+ String relyingPartyRegistrationRepositoryRef = element
+ .getAttribute(ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF);
+ if (StringUtils.hasText(relyingPartyRegistrationRepositoryRef)) {
+ return new RuntimeBeanReference(relyingPartyRegistrationRepositoryRef);
+ }
+ return new RuntimeBeanReference(RelyingPartyRegistrationRepository.class);
+ }
+
+ static BeanMetadataElement getAuthenticationRequestRepository(Element element) {
+ String authenticationRequestRepositoryRef = element.getAttribute(ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF);
+ if (StringUtils.hasText(authenticationRequestRepositoryRef)) {
+ return new RuntimeBeanReference(authenticationRequestRepositoryRef);
+ }
+ return BeanDefinitionBuilder.rootBeanDefinition(HttpSessionSaml2AuthenticationRequestRepository.class)
+ .getBeanDefinition();
+ }
+
+ static BeanMetadataElement getAuthenticationRequestResolver(Element element) {
+ String authenticationRequestContextResolver = element.getAttribute(ATT_AUTHENTICATION_REQUEST_RESOLVER_REF);
+ if (StringUtils.hasText(authenticationRequestContextResolver)) {
+ return new RuntimeBeanReference(authenticationRequestContextResolver);
+ }
+ return null;
+ }
+
+ static BeanMetadataElement createDefaultAuthenticationRequestResolver(
+ BeanMetadataElement relyingPartyRegistrationRepository) {
+ BeanMetadataElement defaultRelyingPartyRegistrationResolver = BeanDefinitionBuilder
+ .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
+ .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition();
+ return BeanDefinitionBuilder.rootBeanDefinition(
+ "org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver")
+ .addConstructorArgValue(defaultRelyingPartyRegistrationResolver).getBeanDefinition();
+ }
+
+ static BeanDefinition createAuthenticationProvider() {
+ return BeanDefinitionBuilder.rootBeanDefinition(
+ "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider")
+ .getBeanDefinition();
+ }
+
+ static BeanMetadataElement getAuthenticationConverter(Element element) {
+ String authenticationConverter = element.getAttribute(ATT_AUTHENTICATION_CONVERTER);
+ if (StringUtils.hasText(authenticationConverter)) {
+ return new RuntimeBeanReference(authenticationConverter);
+ }
+ return null;
+ }
+
+ static BeanDefinition createDefaultAuthenticationConverter(BeanMetadataElement relyingPartyRegistrationRepository) {
+ AbstractBeanDefinition resolver = BeanDefinitionBuilder
+ .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
+ .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition();
+ return BeanDefinitionBuilder.rootBeanDefinition(Saml2AuthenticationTokenConverter.class)
+ .addConstructorArgValue(resolver).getBeanDefinition();
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java
new file mode 100644
index 00000000000..cb3bc26355f
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java
@@ -0,0 +1,236 @@
+/*
+ * 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 org.springframework.security.config.http;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.ManagedList;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
+import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
+import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * SAML 2.0 Single Logout {@link BeanDefinitionParser}
+ *
+ * @author Marcus da Coregio
+ * @since 5.7
+ */
+final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
+
+ private static final String ATT_LOGOUT_REQUEST_URL = "logout-request-url";
+
+ private static final String ATT_LOGOUT_RESPONSE_URL = "logout-response-url";
+
+ private static final String ATT_LOGOUT_URL = "logout-url";
+
+ private List logoutHandlers;
+
+ private String logoutUrl = "/logout";
+
+ private String logoutRequestUrl = "/logout/saml2/slo";
+
+ private String logoutResponseUrl = "/logout/saml2/slo";
+
+ private BeanMetadataElement logoutSuccessHandler;
+
+ private BeanDefinition logoutRequestFilter;
+
+ private BeanDefinition logoutResponseFilter;
+
+ private BeanDefinition logoutFilter;
+
+ Saml2LogoutBeanDefinitionParser(ManagedList logoutHandlers,
+ BeanMetadataElement logoutSuccessHandler) {
+ this.logoutHandlers = logoutHandlers;
+ this.logoutSuccessHandler = logoutSuccessHandler;
+ }
+
+ @Override
+ public BeanDefinition parse(Element element, ParserContext pc) {
+ String logoutUrl = element.getAttribute(ATT_LOGOUT_URL);
+ if (StringUtils.hasText(logoutUrl)) {
+ this.logoutUrl = logoutUrl;
+ }
+ String logoutRequestUrl = element.getAttribute(ATT_LOGOUT_REQUEST_URL);
+ if (StringUtils.hasText(logoutRequestUrl)) {
+ this.logoutRequestUrl = logoutRequestUrl;
+ }
+ String logoutResponseUrl = element.getAttribute(ATT_LOGOUT_RESPONSE_URL);
+ if (StringUtils.hasText(logoutResponseUrl)) {
+ this.logoutResponseUrl = logoutResponseUrl;
+ }
+ WebConfigUtils.validateHttpRedirect(this.logoutUrl, pc, element);
+ WebConfigUtils.validateHttpRedirect(this.logoutRequestUrl, pc, element);
+ WebConfigUtils.validateHttpRedirect(this.logoutResponseUrl, pc, element);
+ if (CollectionUtils.isEmpty(this.logoutHandlers)) {
+ this.logoutHandlers = createDefaultLogoutHandlers();
+ }
+ if (this.logoutSuccessHandler == null) {
+ this.logoutSuccessHandler = createDefaultLogoutSuccessHandler();
+ }
+ BeanMetadataElement relyingPartyRegistrationRepository = Saml2LogoutBeanDefinitionParserUtils
+ .getRelyingPartyRegistrationRepository(element);
+ BeanMetadataElement registrations = BeanDefinitionBuilder
+ .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
+ .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition();
+ BeanMetadataElement logoutResponseResolver = Saml2LogoutBeanDefinitionParserUtils
+ .getLogoutResponseResolver(element, registrations);
+ BeanMetadataElement logoutRequestValidator = Saml2LogoutBeanDefinitionParserUtils
+ .getLogoutRequestValidator(element);
+ BeanMetadataElement logoutRequestMatcher = createSaml2LogoutRequestMatcher();
+ this.logoutRequestFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutRequestFilter.class)
+ .addConstructorArgValue(registrations).addConstructorArgValue(logoutRequestValidator)
+ .addConstructorArgValue(logoutResponseResolver).addConstructorArgValue(this.logoutHandlers)
+ .addPropertyValue("logoutRequestMatcher", logoutRequestMatcher).getBeanDefinition();
+ BeanMetadataElement logoutResponseValidator = Saml2LogoutBeanDefinitionParserUtils
+ .getLogoutResponseValidator(element);
+ BeanMetadataElement logoutRequestRepository = Saml2LogoutBeanDefinitionParserUtils
+ .getLogoutRequestRepository(element);
+ BeanMetadataElement logoutResponseMatcher = createSaml2LogoutResponseMatcher();
+ this.logoutResponseFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutResponseFilter.class)
+ .addConstructorArgValue(registrations).addConstructorArgValue(logoutResponseValidator)
+ .addConstructorArgValue(this.logoutSuccessHandler)
+ .addPropertyValue("logoutRequestMatcher", logoutResponseMatcher)
+ .addPropertyValue("logoutRequestRepository", logoutRequestRepository).getBeanDefinition();
+ BeanMetadataElement logoutRequestResolver = Saml2LogoutBeanDefinitionParserUtils
+ .getLogoutRequestResolver(element, registrations);
+ BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
+ .rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
+ .addConstructorArgValue(logoutRequestResolver).getBeanDefinition();
+ this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
+ .addConstructorArgValue(saml2LogoutRequestSuccessHandler).addConstructorArgValue(this.logoutHandlers)
+ .addPropertyValue("logoutRequestMatcher", createLogoutRequestMatcher()).getBeanDefinition();
+ return null;
+ }
+
+ private static List createDefaultLogoutHandlers() {
+ List handlers = new ManagedList<>();
+ handlers.add(BeanDefinitionBuilder.rootBeanDefinition(SecurityContextLogoutHandler.class).getBeanDefinition());
+ handlers.add(BeanDefinitionBuilder.rootBeanDefinition(LogoutSuccessEventPublishingLogoutHandler.class)
+ .getBeanDefinition());
+ return handlers;
+ }
+
+ private static BeanMetadataElement createDefaultLogoutSuccessHandler() {
+ return BeanDefinitionBuilder.rootBeanDefinition(SimpleUrlLogoutSuccessHandler.class)
+ .addPropertyValue("defaultTargetUrl", "/login?logout").getBeanDefinition();
+ }
+
+ private BeanMetadataElement createLogoutRequestMatcher() {
+ BeanMetadataElement logoutMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class)
+ .addConstructorArgValue(this.logoutUrl).addConstructorArgValue("POST").getBeanDefinition();
+ BeanMetadataElement saml2Matcher = BeanDefinitionBuilder.rootBeanDefinition(Saml2RequestMatcher.class)
+ .getBeanDefinition();
+ return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
+ .addConstructorArgValue(toManagedList(logoutMatcher, saml2Matcher)).getBeanDefinition();
+ }
+
+ private BeanMetadataElement createSaml2LogoutRequestMatcher() {
+ BeanMetadataElement logoutRequestMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class)
+ .addConstructorArgValue(this.logoutRequestUrl).getBeanDefinition();
+ BeanMetadataElement saml2RequestMatcher = BeanDefinitionBuilder
+ .rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLRequest")
+ .getBeanDefinition();
+ return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
+ .addConstructorArgValue(toManagedList(logoutRequestMatcher, saml2RequestMatcher)).getBeanDefinition();
+ }
+
+ private BeanMetadataElement createSaml2LogoutResponseMatcher() {
+ BeanMetadataElement logoutResponseMatcher = BeanDefinitionBuilder
+ .rootBeanDefinition(AntPathRequestMatcher.class).addConstructorArgValue(this.logoutResponseUrl)
+ .getBeanDefinition();
+ BeanMetadataElement saml2ResponseMatcher = BeanDefinitionBuilder
+ .rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLResponse")
+ .getBeanDefinition();
+ return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
+ .addConstructorArgValue(toManagedList(logoutResponseMatcher, saml2ResponseMatcher)).getBeanDefinition();
+ }
+
+ private static List toManagedList(BeanMetadataElement... elements) {
+ List managedList = new ManagedList<>();
+ managedList.addAll(Arrays.asList(elements));
+ return managedList;
+ }
+
+ BeanDefinition getLogoutRequestFilter() {
+ return this.logoutRequestFilter;
+ }
+
+ BeanDefinition getLogoutResponseFilter() {
+ return this.logoutResponseFilter;
+ }
+
+ BeanDefinition getLogoutFilter() {
+ return this.logoutFilter;
+ }
+
+ private static class ParameterRequestMatcher implements RequestMatcher {
+
+ Predicate test = Objects::nonNull;
+
+ String name;
+
+ ParameterRequestMatcher(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean matches(HttpServletRequest request) {
+ return this.test.test(request.getParameter(this.name));
+ }
+
+ }
+
+ private static class Saml2RequestMatcher implements RequestMatcher {
+
+ @Override
+ public boolean matches(HttpServletRequest request) {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication == null) {
+ return false;
+ }
+ return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal;
+ }
+
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java
new file mode 100644
index 00000000000..96ca597889d
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java
@@ -0,0 +1,104 @@
+/*
+ * 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 org.springframework.security.config.http;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator;
+import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Marcus da Coregio
+ * @since 5.7
+ */
+final class Saml2LogoutBeanDefinitionParserUtils {
+
+ private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
+
+ private static final String ATT_LOGOUT_REQUEST_VALIDATOR_REF = "logout-request-validator-ref";
+
+ private static final String ATT_LOGOUT_REQUEST_REPOSITORY_REF = "logout-request-repository-ref";
+
+ private static final String ATT_LOGOUT_REQUEST_RESOLVER_REF = "logout-request-resolver-ref";
+
+ private static final String ATT_LOGOUT_RESPONSE_RESOLVER_REF = "logout-response-resolver-ref";
+
+ private static final String ATT_LOGOUT_RESPONSE_VALIDATOR_REF = "logout-response-validator-ref";
+
+ private Saml2LogoutBeanDefinitionParserUtils() {
+ }
+
+ static BeanMetadataElement getRelyingPartyRegistrationRepository(Element element) {
+ String relyingPartyRegistrationRepositoryRef = element
+ .getAttribute(ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF);
+ if (StringUtils.hasText(relyingPartyRegistrationRepositoryRef)) {
+ return new RuntimeBeanReference(relyingPartyRegistrationRepositoryRef);
+ }
+ return new RuntimeBeanReference(RelyingPartyRegistrationRepository.class);
+ }
+
+ static BeanMetadataElement getLogoutResponseResolver(Element element, BeanMetadataElement registrations) {
+ String logoutResponseResolver = element.getAttribute(ATT_LOGOUT_RESPONSE_RESOLVER_REF);
+ if (StringUtils.hasText(logoutResponseResolver)) {
+ return new RuntimeBeanReference(logoutResponseResolver);
+ }
+ return BeanDefinitionBuilder.rootBeanDefinition(
+ "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
+ .addConstructorArgValue(registrations).getBeanDefinition();
+ }
+
+ static BeanMetadataElement getLogoutRequestValidator(Element element) {
+ String logoutRequestValidator = element.getAttribute(ATT_LOGOUT_REQUEST_VALIDATOR_REF);
+ if (StringUtils.hasText(logoutRequestValidator)) {
+ return new RuntimeBeanReference(logoutRequestValidator);
+ }
+ return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutRequestValidator.class).getBeanDefinition();
+ }
+
+ static BeanMetadataElement getLogoutResponseValidator(Element element) {
+ String logoutResponseValidator = element.getAttribute(ATT_LOGOUT_RESPONSE_VALIDATOR_REF);
+ if (StringUtils.hasText(logoutResponseValidator)) {
+ return new RuntimeBeanReference(logoutResponseValidator);
+ }
+ return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutResponseValidator.class).getBeanDefinition();
+ }
+
+ static BeanMetadataElement getLogoutRequestRepository(Element element) {
+ String logoutRequestRepository = element.getAttribute(ATT_LOGOUT_REQUEST_REPOSITORY_REF);
+ if (StringUtils.hasText(logoutRequestRepository)) {
+ return new RuntimeBeanReference(logoutRequestRepository);
+ }
+ return BeanDefinitionBuilder.rootBeanDefinition(HttpSessionLogoutRequestRepository.class).getBeanDefinition();
+ }
+
+ static BeanMetadataElement getLogoutRequestResolver(Element element, BeanMetadataElement registrations) {
+ String logoutRequestResolver = element.getAttribute(ATT_LOGOUT_REQUEST_RESOLVER_REF);
+ if (StringUtils.hasText(logoutRequestResolver)) {
+ return new RuntimeBeanReference(logoutRequestResolver);
+ }
+ return BeanDefinitionBuilder.rootBeanDefinition(
+ "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
+ .addConstructorArgValue(registrations).getBeanDefinition();
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
index c9b053a3b66..c13012d4a78 100644
--- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
+++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -29,6 +29,10 @@ enum SecurityFilters {
FIRST(Integer.MIN_VALUE),
+ DISABLE_ENCODE_URL_FILTER,
+
+ FORCE_EAGER_SESSION_FILTER,
+
CHANNEL_FILTER,
SECURITY_CONTEXT_FILTER,
@@ -41,12 +45,20 @@ enum SecurityFilters {
CORS_FILTER,
+ SAML2_LOGOUT_REQUEST_FILTER,
+
+ SAML2_LOGOUT_RESPONSE_FILTER,
+
CSRF_FILTER,
+ SAML2_LOGOUT_FILTER,
+
LOGOUT_FILTER,
OAUTH2_AUTHORIZATION_REQUEST_FILTER,
+ SAML2_AUTHENTICATION_REQUEST_FILTER,
+
X509_FILTER,
PRE_AUTH_FILTER,
@@ -55,6 +67,8 @@ enum SecurityFilters {
OAUTH2_LOGIN_FILTER,
+ SAML2_AUTHENTICATION_FILTER,
+
FORM_LOGIN_FILTER,
OPENID_FILTER,
diff --git a/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java
new file mode 100644
index 00000000000..16069f09a47
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java
@@ -0,0 +1,183 @@
+/*
+ * 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 org.springframework.security.config.ldap;
+
+import org.springframework.ldap.core.support.BaseLdapPathContextSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
+import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
+
+/**
+ * Creates an {@link AuthenticationManager} that can perform LDAP authentication.
+ *
+ * @author Eleftheria Stein
+ * @since 5.7
+ */
+public abstract class AbstractLdapAuthenticationManagerFactory {
+
+ AbstractLdapAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) {
+ this.contextSource = contextSource;
+ }
+
+ private BaseLdapPathContextSource contextSource;
+
+ private String[] userDnPatterns;
+
+ private LdapAuthoritiesPopulator ldapAuthoritiesPopulator;
+
+ private GrantedAuthoritiesMapper authoritiesMapper;
+
+ private UserDetailsContextMapper userDetailsContextMapper;
+
+ private String userSearchFilter;
+
+ private String userSearchBase = "";
+
+ /**
+ * Sets the {@link BaseLdapPathContextSource} used to perform LDAP authentication.
+ * @param contextSource the {@link BaseLdapPathContextSource} used to perform LDAP
+ * authentication
+ */
+ public void setContextSource(BaseLdapPathContextSource contextSource) {
+ this.contextSource = contextSource;
+ }
+
+ /**
+ * Gets the {@link BaseLdapPathContextSource} used to perform LDAP authentication.
+ * @return the {@link BaseLdapPathContextSource} used to perform LDAP authentication
+ */
+ protected final BaseLdapPathContextSource getContextSource() {
+ return this.contextSource;
+ }
+
+ /**
+ * Sets the {@link LdapAuthoritiesPopulator} used to obtain a list of granted
+ * authorities for an LDAP user.
+ * @param ldapAuthoritiesPopulator the {@link LdapAuthoritiesPopulator} to use
+ */
+ public void setLdapAuthoritiesPopulator(LdapAuthoritiesPopulator ldapAuthoritiesPopulator) {
+ this.ldapAuthoritiesPopulator = ldapAuthoritiesPopulator;
+ }
+
+ /**
+ * Sets the {@link GrantedAuthoritiesMapper} used for converting the authorities
+ * loaded from storage to a new set of authorities which will be associated to the
+ * {@link UsernamePasswordAuthenticationToken}.
+ * @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
+ * user's authorities
+ */
+ public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
+ this.authoritiesMapper = authoritiesMapper;
+ }
+
+ /**
+ * Sets a custom strategy to be used for creating the {@link UserDetails} which will
+ * be stored as the principal in the {@link Authentication}.
+ * @param userDetailsContextMapper the strategy instance
+ */
+ public void setUserDetailsContextMapper(UserDetailsContextMapper userDetailsContextMapper) {
+ this.userDetailsContextMapper = userDetailsContextMapper;
+ }
+
+ /**
+ * If your users are at a fixed location in the directory (i.e. you can work out the
+ * DN directly from the username without doing a directory search), you can use this
+ * attribute to map directly to the DN. It maps directly to the userDnPatterns
+ * property of AbstractLdapAuthenticator. The value is a specific pattern used to
+ * build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present
+ * and will be substituted with the username.
+ * @param userDnPatterns the LDAP patterns for finding the usernames
+ */
+ public void setUserDnPatterns(String... userDnPatterns) {
+ this.userDnPatterns = userDnPatterns;
+ }
+
+ /**
+ * The LDAP filter used to search for users (optional). For example "(uid={0})". The
+ * substituted parameter is the user's login name.
+ * @param userSearchFilter the LDAP filter used to search for users
+ */
+ public void setUserSearchFilter(String userSearchFilter) {
+ this.userSearchFilter = userSearchFilter;
+ }
+
+ /**
+ * Search base for user searches. Defaults to "". Only used with
+ * {@link #setUserSearchFilter(String)}.
+ * @param userSearchBase search base for user searches
+ */
+ public void setUserSearchBase(String userSearchBase) {
+ this.userSearchBase = userSearchBase;
+ }
+
+ /**
+ * Returns the configured {@link AuthenticationManager} that can be used to perform
+ * LDAP authentication.
+ * @return the configured {@link AuthenticationManager}
+ */
+ public final AuthenticationManager createAuthenticationManager() {
+ LdapAuthenticationProvider ldapAuthenticationProvider = getProvider();
+ return new ProviderManager(ldapAuthenticationProvider);
+ }
+
+ private LdapAuthenticationProvider getProvider() {
+ AbstractLdapAuthenticator authenticator = getAuthenticator();
+ LdapAuthenticationProvider provider;
+ if (this.ldapAuthoritiesPopulator != null) {
+ provider = new LdapAuthenticationProvider(authenticator, this.ldapAuthoritiesPopulator);
+ }
+ else {
+ provider = new LdapAuthenticationProvider(authenticator);
+ }
+ if (this.authoritiesMapper != null) {
+ provider.setAuthoritiesMapper(this.authoritiesMapper);
+ }
+ if (this.userDetailsContextMapper != null) {
+ provider.setUserDetailsContextMapper(this.userDetailsContextMapper);
+ }
+ return provider;
+ }
+
+ private AbstractLdapAuthenticator getAuthenticator() {
+ AbstractLdapAuthenticator authenticator = createDefaultLdapAuthenticator();
+ if (this.userSearchFilter != null) {
+ authenticator.setUserSearch(
+ new FilterBasedLdapUserSearch(this.userSearchBase, this.userSearchFilter, this.contextSource));
+ }
+ if (this.userDnPatterns != null && this.userDnPatterns.length > 0) {
+ authenticator.setUserDnPatterns(this.userDnPatterns);
+ }
+ authenticator.afterPropertiesSet();
+ return authenticator;
+ }
+
+ /**
+ * Allows subclasses to supply the default {@link AbstractLdapAuthenticator}.
+ * @return the {@link AbstractLdapAuthenticator} that will be configured for LDAP
+ * authentication
+ */
+ protected abstract T createDefaultLdapAuthenticator();
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java
new file mode 100644
index 00000000000..4a8c2d56d40
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java
@@ -0,0 +1,185 @@
+/*
+ * 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 org.springframework.security.config.ldap;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.Lifecycle;
+import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
+import org.springframework.security.ldap.server.EmbeddedLdapServerContainer;
+import org.springframework.security.ldap.server.UnboundIdContainer;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Creates a {@link DefaultSpringSecurityContextSource} used to perform LDAP
+ * authentication and starts and in-memory LDAP server.
+ *
+ * @author Eleftheria Stein
+ * @since 5.7
+ */
+public class EmbeddedLdapServerContextSourceFactoryBean
+ implements FactoryBean, DisposableBean, ApplicationContextAware {
+
+ private static final String UNBOUNDID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer";
+
+ private static final int DEFAULT_PORT = 33389;
+
+ private static final int RANDOM_PORT = 0;
+
+ private Integer port;
+
+ private String ldif = "classpath*:*.ldif";
+
+ private String root = "dc=springframework,dc=org";
+
+ private ApplicationContext context;
+
+ private String managerDn;
+
+ private String managerPassword;
+
+ private EmbeddedLdapServerContainer container;
+
+ /**
+ * Create an EmbeddedLdapServerContextSourceFactoryBean that will use an embedded LDAP
+ * server to perform LDAP authentication. This requires a dependency on
+ * `com.unboundid:unboundid-ldapsdk`.
+ * @return the EmbeddedLdapServerContextSourceFactoryBean
+ */
+ public static EmbeddedLdapServerContextSourceFactoryBean fromEmbeddedLdapServer() {
+ return new EmbeddedLdapServerContextSourceFactoryBean();
+ }
+
+ /**
+ * Specifies an LDIF to load at startup for an embedded LDAP server. The default is
+ * "classpath*:*.ldif".
+ * @param ldif the ldif to load at startup for an embedded LDAP server.
+ */
+ public void setLdif(String ldif) {
+ this.ldif = ldif;
+ }
+
+ /**
+ * The port to connect to LDAP to (the default is 33389 or random available port if
+ * unavailable). Supplying 0 as the port indicates that a random available port should
+ * be selected.
+ * @param port the port to connect to
+ */
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ /**
+ * Optional root suffix for the embedded LDAP server. Default is
+ * "dc=springframework,dc=org".
+ * @param root root suffix for the embedded LDAP server
+ */
+ public void setRoot(String root) {
+ this.root = root;
+ }
+
+ /**
+ * Username (DN) of the "manager" user identity (i.e. "uid=admin,ou=system") which
+ * will be used to authenticate to an LDAP server. If omitted, anonymous access will
+ * be used.
+ * @param managerDn the username (DN) of the "manager" user identity used to
+ * authenticate to a LDAP server.
+ */
+ public void setManagerDn(String managerDn) {
+ this.managerDn = managerDn;
+ }
+
+ /**
+ * The password for the manager DN. This is required if the
+ * {@link #setManagerDn(String)} is specified.
+ * @param managerPassword password for the manager DN
+ */
+ public void setManagerPassword(String managerPassword) {
+ this.managerPassword = managerPassword;
+ }
+
+ @Override
+ public DefaultSpringSecurityContextSource getObject() throws Exception {
+ if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) {
+ throw new IllegalStateException("Embedded LDAP server is not provided");
+ }
+ this.container = getContainer();
+ this.port = this.container.getPort();
+ DefaultSpringSecurityContextSource contextSourceFromProviderUrl = new DefaultSpringSecurityContextSource(
+ "ldap://127.0.0.1:" + this.port + "/" + this.root);
+ if (this.managerDn != null) {
+ contextSourceFromProviderUrl.setUserDn(this.managerDn);
+ if (this.managerPassword == null) {
+ throw new IllegalStateException("managerPassword is required if managerDn is supplied");
+ }
+ contextSourceFromProviderUrl.setPassword(this.managerPassword);
+ }
+ contextSourceFromProviderUrl.afterPropertiesSet();
+ return contextSourceFromProviderUrl;
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return DefaultSpringSecurityContextSource.class;
+ }
+
+ @Override
+ public void destroy() {
+ if (this.container instanceof Lifecycle) {
+ ((Lifecycle) this.container).stop();
+ }
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.context = applicationContext;
+ }
+
+ private EmbeddedLdapServerContainer getContainer() {
+ if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) {
+ throw new IllegalStateException("Embedded LDAP server is not provided");
+ }
+ UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif);
+ unboundIdContainer.setApplicationContext(this.context);
+ unboundIdContainer.setPort(getEmbeddedServerPort());
+ unboundIdContainer.afterPropertiesSet();
+ return unboundIdContainer;
+ }
+
+ private int getEmbeddedServerPort() {
+ if (this.port == null) {
+ this.port = getDefaultEmbeddedServerPort();
+ }
+ return this.port;
+ }
+
+ private int getDefaultEmbeddedServerPort() {
+ try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
+ return serverSocket.getLocalPort();
+ }
+ catch (IOException ex) {
+ return RANDOM_PORT;
+ }
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java
new file mode 100644
index 00000000000..a62fbfab44a
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.springframework.security.config.ldap;
+
+import org.springframework.ldap.core.support.BaseLdapPathContextSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.ldap.authentication.BindAuthenticator;
+
+/**
+ * Creates an {@link AuthenticationManager} that can perform LDAP authentication using
+ * bind authentication.
+ *
+ * @author Eleftheria Stein
+ * @since 5.7
+ */
+public class LdapBindAuthenticationManagerFactory extends AbstractLdapAuthenticationManagerFactory {
+
+ public LdapBindAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) {
+ super(contextSource);
+ }
+
+ @Override
+ protected BindAuthenticator createDefaultLdapAuthenticator() {
+ return new BindAuthenticator(getContextSource());
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java
new file mode 100644
index 00000000000..19c14f998df
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.springframework.security.config.ldap;
+
+import org.springframework.ldap.core.support.BaseLdapPathContextSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator;
+import org.springframework.util.Assert;
+
+/**
+ * Creates an {@link AuthenticationManager} that can perform LDAP authentication using
+ * password comparison.
+ *
+ * @author Eleftheria Stein
+ * @since 5.7
+ */
+public class LdapPasswordComparisonAuthenticationManagerFactory
+ extends AbstractLdapAuthenticationManagerFactory {
+
+ private PasswordEncoder passwordEncoder;
+
+ private String passwordAttribute;
+
+ public LdapPasswordComparisonAuthenticationManagerFactory(BaseLdapPathContextSource contextSource,
+ PasswordEncoder passwordEncoder) {
+ super(contextSource);
+ setPasswordEncoder(passwordEncoder);
+ }
+
+ /**
+ * Specifies the {@link PasswordEncoder} to be used when authenticating with password
+ * comparison.
+ * @param passwordEncoder the {@link PasswordEncoder} to use
+ */
+ public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
+ Assert.notNull(passwordEncoder, "passwordEncoder must not be null.");
+ this.passwordEncoder = passwordEncoder;
+ }
+
+ /**
+ * The attribute in the directory which contains the user password. Only used when
+ * authenticating with password comparison. Defaults to "userPassword".
+ * @param passwordAttribute the attribute in the directory which contains the user
+ * password
+ */
+ public void setPasswordAttribute(String passwordAttribute) {
+ this.passwordAttribute = passwordAttribute;
+ }
+
+ @Override
+ protected PasswordComparisonAuthenticator createDefaultLdapAuthenticator() {
+ PasswordComparisonAuthenticator ldapAuthenticator = new PasswordComparisonAuthenticator(getContextSource());
+ if (this.passwordAttribute != null) {
+ ldapAuthenticator.setPasswordAttributeName(this.passwordAttribute);
+ }
+ ldapAuthenticator.setPasswordEncoder(this.passwordEncoder);
+ return ldapAuthenticator;
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java
new file mode 100644
index 00000000000..ab55ad0df82
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java
@@ -0,0 +1,376 @@
+/*
+ * 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 org.springframework.security.config.saml2;
+
+import java.io.InputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.parsing.BeanComponentDefinition;
+import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.security.converter.RsaKeyConverters;
+import org.springframework.security.saml2.core.Saml2X509Credential;
+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.Saml2MessageBinding;
+import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
+
+/**
+ * @author Marcus da Coregio
+ * @since 5.7
+ */
+public final class RelyingPartyRegistrationsBeanDefinitionParser implements BeanDefinitionParser {
+
+ private static final String ELT_RELYING_PARTY_REGISTRATION = "relying-party-registration";
+
+ private static final String ELT_SIGNING_CREDENTIAL = "signing-credential";
+
+ private static final String ELT_DECRYPTION_CREDENTIAL = "decryption-credential";
+
+ private static final String ELT_ASSERTING_PARTY = "asserting-party";
+
+ private static final String ELT_VERIFICATION_CREDENTIAL = "verification-credential";
+
+ private static final String ELT_ENCRYPTION_CREDENTIAL = "encryption-credential";
+
+ private static final String ATT_REGISTRATION_ID = "registration-id";
+
+ private static final String ATT_ASSERTING_PARTY_ID = "asserting-party-id";
+
+ private static final String ATT_ENTITY_ID = "entity-id";
+
+ private static final String ATT_METADATA_LOCATION = "metadata-location";
+
+ private static final String ATT_ASSERTION_CONSUMER_SERVICE_LOCATION = "assertion-consumer-service-location";
+
+ private static final String ATT_ASSERTION_CONSUMER_SERVICE_BINDING = "assertion-consumer-service-binding";
+
+ private static final String ATT_PRIVATE_KEY_LOCATION = "private-key-location";
+
+ private static final String ATT_CERTIFICATE_LOCATION = "certificate-location";
+
+ private static final String ATT_WANT_AUTHN_REQUESTS_SIGNED = "want-authn-requests-signed";
+
+ private static final String ATT_SINGLE_SIGN_ON_SERVICE_LOCATION = "single-sign-on-service-location";
+
+ private static final String ATT_SINGLE_SIGN_ON_SERVICE_BINDING = "single-sign-on-service-binding";
+
+ private static final String ATT_SIGNING_ALGORITHMS = "signing-algorithms";
+
+ private static final String ATT_SINGLE_LOGOUT_SERVICE_LOCATION = "single-logout-service-location";
+
+ private static final String ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION = "single-logout-service-response-location";
+
+ private static final String ATT_SINGLE_LOGOUT_SERVICE_BINDING = "single-logout-service-binding";
+
+ private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
+
+ @Override
+ public BeanDefinition parse(Element element, ParserContext parserContext) {
+ CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(),
+ parserContext.extractSource(element));
+ parserContext.pushContainingComponent(compositeDef);
+ Map> assertingParties = getAssertingParties(element);
+ List relyingPartyRegistrations = getRelyingPartyRegistrations(element,
+ assertingParties, parserContext);
+ BeanDefinition relyingPartyRegistrationRepositoryBean = BeanDefinitionBuilder
+ .rootBeanDefinition(InMemoryRelyingPartyRegistrationRepository.class)
+ .addConstructorArgValue(relyingPartyRegistrations).getBeanDefinition();
+ String relyingPartyRegistrationRepositoryId = parserContext.getReaderContext()
+ .generateBeanName(relyingPartyRegistrationRepositoryBean);
+ parserContext.registerBeanComponent(new BeanComponentDefinition(relyingPartyRegistrationRepositoryBean,
+ relyingPartyRegistrationRepositoryId));
+ parserContext.popAndRegisterContainingComponent();
+ return null;
+ }
+
+ private static Map> getAssertingParties(Element element) {
+ List assertingPartyElts = DomUtils.getChildElementsByTagName(element, ELT_ASSERTING_PARTY);
+ Map> providers = new HashMap<>();
+ for (Element assertingPartyElt : assertingPartyElts) {
+ Map assertingParty = new HashMap<>();
+ String assertingPartyId = assertingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID);
+ String entityId = assertingPartyElt.getAttribute(ATT_ENTITY_ID);
+ String wantAuthnRequestsSigned = assertingPartyElt.getAttribute(ATT_WANT_AUTHN_REQUESTS_SIGNED);
+ String singleSignOnServiceLocation = assertingPartyElt.getAttribute(ATT_SINGLE_SIGN_ON_SERVICE_LOCATION);
+ String singleSignOnServiceBinding = assertingPartyElt.getAttribute(ATT_SINGLE_SIGN_ON_SERVICE_BINDING);
+ String signingAlgorithms = assertingPartyElt.getAttribute(ATT_SIGNING_ALGORITHMS);
+ String singleLogoutServiceLocation = assertingPartyElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
+ String singleLogoutServiceResponseLocation = assertingPartyElt
+ .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
+ String singleLogoutServiceBinding = assertingPartyElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_BINDING);
+ assertingParty.put(ATT_ASSERTING_PARTY_ID, assertingPartyId);
+ assertingParty.put(ATT_ENTITY_ID, entityId);
+ assertingParty.put(ATT_WANT_AUTHN_REQUESTS_SIGNED, wantAuthnRequestsSigned);
+ assertingParty.put(ATT_SINGLE_SIGN_ON_SERVICE_LOCATION, singleSignOnServiceLocation);
+ assertingParty.put(ATT_SINGLE_SIGN_ON_SERVICE_BINDING, singleSignOnServiceBinding);
+ assertingParty.put(ATT_SIGNING_ALGORITHMS, signingAlgorithms);
+ assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_LOCATION, singleLogoutServiceLocation);
+ assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION, singleLogoutServiceResponseLocation);
+ assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_BINDING, singleLogoutServiceBinding);
+ addVerificationCredentials(assertingPartyElt, assertingParty);
+ addEncryptionCredentials(assertingPartyElt, assertingParty);
+ providers.put(assertingPartyId, assertingParty);
+ }
+ return providers;
+ }
+
+ private static void addVerificationCredentials(Map assertingParty,
+ RelyingPartyRegistration.AssertingPartyDetails.Builder builder) {
+ List verificationCertificateLocations = (List) assertingParty.get(ELT_VERIFICATION_CREDENTIAL);
+ List verificationCredentials = new ArrayList<>();
+ for (String certificateLocation : verificationCertificateLocations) {
+ verificationCredentials.add(getSaml2VerificationCredential(certificateLocation));
+ }
+ builder.verificationX509Credentials((credentials) -> credentials.addAll(verificationCredentials));
+ }
+
+ private static void addEncryptionCredentials(Map assertingParty,
+ RelyingPartyRegistration.AssertingPartyDetails.Builder builder) {
+ List encryptionCertificateLocations = (List) assertingParty.get(ELT_ENCRYPTION_CREDENTIAL);
+ List encryptionCredentials = new ArrayList<>();
+ for (String certificateLocation : encryptionCertificateLocations) {
+ encryptionCredentials.add(getSaml2EncryptionCredential(certificateLocation));
+ }
+ builder.encryptionX509Credentials((credentials) -> credentials.addAll(encryptionCredentials));
+ }
+
+ private static void addVerificationCredentials(Element assertingPartyElt, Map assertingParty) {
+ List verificationCertificateLocations = new ArrayList<>();
+ List verificationCredentialElts = DomUtils.getChildElementsByTagName(assertingPartyElt,
+ ELT_VERIFICATION_CREDENTIAL);
+ for (Element verificationCredentialElt : verificationCredentialElts) {
+ String certificateLocation = verificationCredentialElt.getAttribute(ATT_CERTIFICATE_LOCATION);
+ verificationCertificateLocations.add(certificateLocation);
+ }
+ assertingParty.put(ELT_VERIFICATION_CREDENTIAL, verificationCertificateLocations);
+ }
+
+ private static void addEncryptionCredentials(Element assertingPartyElt, Map assertingParty) {
+ List encryptionCertificateLocations = new ArrayList<>();
+ List encryptionCredentialElts = DomUtils.getChildElementsByTagName(assertingPartyElt,
+ ELT_VERIFICATION_CREDENTIAL);
+ for (Element encryptionCredentialElt : encryptionCredentialElts) {
+ String certificateLocation = encryptionCredentialElt.getAttribute(ATT_CERTIFICATE_LOCATION);
+ encryptionCertificateLocations.add(certificateLocation);
+ }
+ assertingParty.put(ELT_ENCRYPTION_CREDENTIAL, encryptionCertificateLocations);
+ }
+
+ private List getRelyingPartyRegistrations(Element element,
+ Map> assertingParties, ParserContext parserContext) {
+ List relyingPartyRegistrationElts = DomUtils.getChildElementsByTagName(element,
+ ELT_RELYING_PARTY_REGISTRATION);
+ List relyingPartyRegistrations = new ArrayList<>();
+ for (Element relyingPartyRegistrationElt : relyingPartyRegistrationElts) {
+ RelyingPartyRegistration.Builder builder = getBuilderFromMetadataLocationIfPossible(
+ relyingPartyRegistrationElt, assertingParties, parserContext);
+ addSigningCredentials(relyingPartyRegistrationElt, builder);
+ addDecryptionCredentials(relyingPartyRegistrationElt, builder);
+ relyingPartyRegistrations.add(builder.build());
+ }
+ return relyingPartyRegistrations;
+ }
+
+ private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIfPossible(
+ Element relyingPartyRegistrationElt, Map> assertingParties,
+ ParserContext parserContext) {
+ String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID);
+ String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION);
+ String singleLogoutServiceLocation = relyingPartyRegistrationElt
+ .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
+ String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt
+ .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
+ Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt);
+ if (StringUtils.hasText(metadataLocation)) {
+ return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId)
+ .singleLogoutServiceLocation(singleLogoutServiceLocation)
+ .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
+ .singleLogoutServiceBinding(singleLogoutServiceBinding);
+ }
+ String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
+ String assertionConsumerServiceLocation = relyingPartyRegistrationElt
+ .getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION);
+ Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding(
+ relyingPartyRegistrationElt);
+ return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId)
+ .assertionConsumerServiceLocation(assertionConsumerServiceLocation)
+ .assertionConsumerServiceBinding(assertionConsumerServiceBinding)
+ .singleLogoutServiceLocation(singleLogoutServiceLocation)
+ .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
+ .singleLogoutServiceBinding(singleLogoutServiceBinding)
+ .assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties,
+ builder, parserContext));
+ }
+
+ private static void buildAssertingParty(Element relyingPartyElt, Map> assertingParties,
+ RelyingPartyRegistration.AssertingPartyDetails.Builder builder, ParserContext parserContext) {
+ String assertingPartyId = relyingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID);
+ if (!assertingParties.containsKey(assertingPartyId)) {
+ Object source = parserContext.extractSource(relyingPartyElt);
+ parserContext.getReaderContext()
+ .error(String.format("Could not find asserting party with id %s", assertingPartyId), source);
+ }
+ Map assertingParty = assertingParties.get(assertingPartyId);
+ String entityId = getAsString(assertingParty, ATT_ENTITY_ID);
+ String wantAuthnRequestsSigned = getAsString(assertingParty, ATT_WANT_AUTHN_REQUESTS_SIGNED);
+ String singleSignOnServiceLocation = getAsString(assertingParty, ATT_SINGLE_SIGN_ON_SERVICE_LOCATION);
+ String singleSignOnServiceBinding = getAsString(assertingParty, ATT_SINGLE_SIGN_ON_SERVICE_BINDING);
+ Saml2MessageBinding saml2MessageBinding = StringUtils.hasText(singleSignOnServiceBinding)
+ ? Saml2MessageBinding.valueOf(singleSignOnServiceBinding) : Saml2MessageBinding.REDIRECT;
+ String singleLogoutServiceLocation = getAsString(assertingParty, ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
+ String singleLogoutServiceResponseLocation = getAsString(assertingParty,
+ ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
+ String singleLogoutServiceBinding = getAsString(assertingParty, ATT_SINGLE_LOGOUT_SERVICE_BINDING);
+ Saml2MessageBinding saml2LogoutMessageBinding = StringUtils.hasText(singleLogoutServiceBinding)
+ ? Saml2MessageBinding.valueOf(singleLogoutServiceBinding) : Saml2MessageBinding.REDIRECT;
+ builder.entityId(entityId).wantAuthnRequestsSigned(Boolean.parseBoolean(wantAuthnRequestsSigned))
+ .singleSignOnServiceLocation(singleSignOnServiceLocation)
+ .singleSignOnServiceBinding(saml2MessageBinding)
+ .singleLogoutServiceLocation(singleLogoutServiceLocation)
+ .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
+ .singleLogoutServiceBinding(saml2LogoutMessageBinding);
+ addSigningAlgorithms(assertingParty, builder);
+ addVerificationCredentials(assertingParty, builder);
+ addEncryptionCredentials(assertingParty, builder);
+ }
+
+ private static void addSigningAlgorithms(Map assertingParty,
+ RelyingPartyRegistration.AssertingPartyDetails.Builder builder) {
+ String signingAlgorithmsAttr = getAsString(assertingParty, ATT_SIGNING_ALGORITHMS);
+ if (StringUtils.hasText(signingAlgorithmsAttr)) {
+ List signingAlgorithms = Arrays.asList(signingAlgorithmsAttr.split(","));
+ builder.signingAlgorithms((s) -> s.addAll(signingAlgorithms));
+ }
+ }
+
+ private static void addSigningCredentials(Element relyingPartyRegistrationElt,
+ RelyingPartyRegistration.Builder builder) {
+ List credentialElts = DomUtils.getChildElementsByTagName(relyingPartyRegistrationElt,
+ ELT_SIGNING_CREDENTIAL);
+ for (Element credentialElt : credentialElts) {
+ String privateKeyLocation = credentialElt.getAttribute(ATT_PRIVATE_KEY_LOCATION);
+ String certificateLocation = credentialElt.getAttribute(ATT_CERTIFICATE_LOCATION);
+ builder.signingX509Credentials(
+ (c) -> c.add(getSaml2SigningCredential(privateKeyLocation, certificateLocation)));
+ }
+ }
+
+ private static void addDecryptionCredentials(Element relyingPartyRegistrationElt,
+ RelyingPartyRegistration.Builder builder) {
+ List credentialElts = DomUtils.getChildElementsByTagName(relyingPartyRegistrationElt,
+ ELT_DECRYPTION_CREDENTIAL);
+ for (Element credentialElt : credentialElts) {
+ String privateKeyLocation = credentialElt.getAttribute(ATT_PRIVATE_KEY_LOCATION);
+ String certificateLocation = credentialElt.getAttribute(ATT_CERTIFICATE_LOCATION);
+ Saml2X509Credential credential = getSaml2DecryptionCredential(privateKeyLocation, certificateLocation);
+ builder.decryptionX509Credentials((c) -> c.add(credential));
+ }
+ }
+
+ private static String getAsString(Map assertingParty, String key) {
+ return (String) assertingParty.get(key);
+ }
+
+ private static Saml2MessageBinding getAssertionConsumerServiceBinding(Element relyingPartyRegistrationElt) {
+ String assertionConsumerServiceBinding = relyingPartyRegistrationElt
+ .getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_BINDING);
+ if (StringUtils.hasText(assertionConsumerServiceBinding)) {
+ return Saml2MessageBinding.valueOf(assertionConsumerServiceBinding);
+ }
+ return Saml2MessageBinding.REDIRECT;
+ }
+
+ private static Saml2MessageBinding getSingleLogoutServiceBinding(Element relyingPartyRegistrationElt) {
+ String singleLogoutServiceBinding = relyingPartyRegistrationElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_BINDING);
+ if (StringUtils.hasText(singleLogoutServiceBinding)) {
+ return Saml2MessageBinding.valueOf(singleLogoutServiceBinding);
+ }
+ return Saml2MessageBinding.POST;
+ }
+
+ private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) {
+ return getSaml2Credential(certificateLocation, Saml2X509Credential.Saml2X509CredentialType.VERIFICATION);
+ }
+
+ private static Saml2X509Credential getSaml2EncryptionCredential(String certificateLocation) {
+ return getSaml2Credential(certificateLocation, Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION);
+ }
+
+ private static Saml2X509Credential getSaml2SigningCredential(String privateKeyLocation,
+ String certificateLocation) {
+ return getSaml2Credential(privateKeyLocation, certificateLocation,
+ Saml2X509Credential.Saml2X509CredentialType.SIGNING);
+ }
+
+ private static Saml2X509Credential getSaml2DecryptionCredential(String privateKeyLocation,
+ String certificateLocation) {
+ return getSaml2Credential(privateKeyLocation, certificateLocation,
+ Saml2X509Credential.Saml2X509CredentialType.DECRYPTION);
+ }
+
+ private static Saml2X509Credential getSaml2Credential(String privateKeyLocation, String certificateLocation,
+ Saml2X509Credential.Saml2X509CredentialType credentialType) {
+ RSAPrivateKey privateKey = readPrivateKey(privateKeyLocation);
+ X509Certificate certificate = readCertificate(certificateLocation);
+ return new Saml2X509Credential(privateKey, certificate, credentialType);
+ }
+
+ private static Saml2X509Credential getSaml2Credential(String certificateLocation,
+ Saml2X509Credential.Saml2X509CredentialType credentialType) {
+ X509Certificate certificate = readCertificate(certificateLocation);
+ return new Saml2X509Credential(certificate, credentialType);
+ }
+
+ private static RSAPrivateKey readPrivateKey(String privateKeyLocation) {
+ Resource privateKey = resourceLoader.getResource(privateKeyLocation);
+ try (InputStream inputStream = privateKey.getInputStream()) {
+ return RsaKeyConverters.pkcs8().convert(inputStream);
+ }
+ catch (Exception ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+
+ private static X509Certificate readCertificate(String certificateLocation) {
+ Resource certificate = resourceLoader.getResource(certificateLocation);
+ try (InputStream inputStream = certificate.getInputStream()) {
+ return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream);
+ }
+ catch (Exception ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
index 282896dd836..5c0094f079a 100644
--- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
+++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
@@ -133,6 +133,7 @@
import org.springframework.security.web.server.authorization.AuthorizationWebFilter;
import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager;
import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
+import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler;
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
@@ -148,6 +149,12 @@
import org.springframework.security.web.server.header.CompositeServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter;
+import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter;
+import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy;
+import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter;
+import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy;
+import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter;
+import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy;
import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter;
import org.springframework.security.web.server.header.PermissionsPolicyServerHttpHeadersWriter;
@@ -1682,6 +1689,18 @@ public AuthorizeExchangeSpec authenticated() {
return access(AuthenticatedReactiveAuthorizationManager.authenticated());
}
+ /**
+ * Require a specific IP address or range using an IP/Netmask (e.g.
+ * 192.168.1.0/24).
+ * @param ipAddress the address or range of addresses from which the request
+ * must come.
+ * @return the {@link AuthorizeExchangeSpec} to configure
+ * @since 5.7
+ */
+ public AuthorizeExchangeSpec hasIpAddress(String ipAddress) {
+ return access(IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress));
+ }
+
/**
* Allows plugging in a custom authorization strategy
* @param manager the authorization manager to use
@@ -2367,10 +2386,17 @@ public final class HeaderSpec {
private ReferrerPolicyServerHttpHeadersWriter referrerPolicy = new ReferrerPolicyServerHttpHeadersWriter();
+ private CrossOriginOpenerPolicyServerHttpHeadersWriter crossOriginOpenerPolicy = new CrossOriginOpenerPolicyServerHttpHeadersWriter();
+
+ private CrossOriginEmbedderPolicyServerHttpHeadersWriter crossOriginEmbedderPolicy = new CrossOriginEmbedderPolicyServerHttpHeadersWriter();
+
+ private CrossOriginResourcePolicyServerHttpHeadersWriter crossOriginResourcePolicy = new CrossOriginResourcePolicyServerHttpHeadersWriter();
+
private HeaderSpec() {
this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts,
this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy,
- this.referrerPolicy));
+ this.referrerPolicy, this.crossOriginOpenerPolicy, this.crossOriginEmbedderPolicy,
+ this.crossOriginResourcePolicy));
}
/**
@@ -2582,6 +2608,84 @@ public HeaderSpec referrerPolicy(Customizer referrerPolicyCu
return this;
}
+ /**
+ * Configures the
+ * Cross-Origin-Opener-Policy header.
+ * @return the {@link CrossOriginOpenerPolicySpec} to configure
+ * @since 5.7
+ * @see CrossOriginOpenerPolicyServerHttpHeadersWriter
+ */
+ public CrossOriginOpenerPolicySpec crossOriginOpenerPolicy() {
+ return new CrossOriginOpenerPolicySpec();
+ }
+
+ /**
+ * Configures the
+ * Cross-Origin-Opener-Policy header.
+ * @return the {@link HeaderSpec} to customize
+ * @since 5.7
+ * @see CrossOriginOpenerPolicyServerHttpHeadersWriter
+ */
+ public HeaderSpec crossOriginOpenerPolicy(
+ Customizer crossOriginOpenerPolicyCustomizer) {
+ crossOriginOpenerPolicyCustomizer.customize(new CrossOriginOpenerPolicySpec());
+ return this;
+ }
+
+ /**
+ * Configures the
+ * Cross-Origin-Embedder-Policy header.
+ * @return the {@link CrossOriginEmbedderPolicySpec} to configure
+ * @since 5.7
+ * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter
+ */
+ public CrossOriginEmbedderPolicySpec crossOriginEmbedderPolicy() {
+ return new CrossOriginEmbedderPolicySpec();
+ }
+
+ /**
+ * Configures the
+ * Cross-Origin-Embedder-Policy header.
+ * @return the {@link HeaderSpec} to customize
+ * @since 5.7
+ * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter
+ */
+ public HeaderSpec crossOriginEmbedderPolicy(
+ Customizer crossOriginEmbedderPolicyCustomizer) {
+ crossOriginEmbedderPolicyCustomizer.customize(new CrossOriginEmbedderPolicySpec());
+ return this;
+ }
+
+ /**
+ * Configures the
+ * Cross-Origin-Resource-Policy header.
+ * @return the {@link CrossOriginResourcePolicySpec} to configure
+ * @since 5.7
+ * @see CrossOriginResourcePolicyServerHttpHeadersWriter
+ */
+ public CrossOriginResourcePolicySpec crossOriginResourcePolicy() {
+ return new CrossOriginResourcePolicySpec();
+ }
+
+ /**
+ * Configures the
+ * Cross-Origin-Resource-Policy header.
+ * @return the {@link HeaderSpec} to customize
+ * @since 5.7
+ * @see CrossOriginResourcePolicyServerHttpHeadersWriter
+ */
+ public HeaderSpec crossOriginResourcePolicy(
+ Customizer crossOriginResourcePolicyCustomizer) {
+ crossOriginResourcePolicyCustomizer.customize(new CrossOriginResourcePolicySpec());
+ return this;
+ }
+
/**
* Configures cache control headers
*
@@ -2897,6 +3001,99 @@ public HeaderSpec and() {
}
+ /**
+ * Configures the Cross-Origin-Opener-Policy header
+ *
+ * @since 5.7
+ */
+ public final class CrossOriginOpenerPolicySpec {
+
+ private CrossOriginOpenerPolicySpec() {
+ }
+
+ /**
+ * Sets the value to be used in the `Cross-Origin-Opener-Policy` header
+ * @param openerPolicy a opener policy
+ * @return the {@link CrossOriginOpenerPolicySpec} to continue configuring
+ */
+ public CrossOriginOpenerPolicySpec policy(CrossOriginOpenerPolicy openerPolicy) {
+ HeaderSpec.this.crossOriginOpenerPolicy.setPolicy(openerPolicy);
+ return this;
+ }
+
+ /**
+ * Allows method chaining to continue configuring the
+ * {@link ServerHttpSecurity}.
+ * @return the {@link HeaderSpec} to continue configuring
+ */
+ public HeaderSpec and() {
+ return HeaderSpec.this;
+ }
+
+ }
+
+ /**
+ * Configures the Cross-Origin-Embedder-Policy header
+ *
+ * @since 5.7
+ */
+ public final class CrossOriginEmbedderPolicySpec {
+
+ private CrossOriginEmbedderPolicySpec() {
+ }
+
+ /**
+ * Sets the value to be used in the `Cross-Origin-Embedder-Policy` header
+ * @param embedderPolicy a opener policy
+ * @return the {@link CrossOriginEmbedderPolicySpec} to continue configuring
+ */
+ public CrossOriginEmbedderPolicySpec policy(CrossOriginEmbedderPolicy embedderPolicy) {
+ HeaderSpec.this.crossOriginEmbedderPolicy.setPolicy(embedderPolicy);
+ return this;
+ }
+
+ /**
+ * Allows method chaining to continue configuring the
+ * {@link ServerHttpSecurity}.
+ * @return the {@link HeaderSpec} to continue configuring
+ */
+ public HeaderSpec and() {
+ return HeaderSpec.this;
+ }
+
+ }
+
+ /**
+ * Configures the Cross-Origin-Resource-Policy header
+ *
+ * @since 5.7
+ */
+ public final class CrossOriginResourcePolicySpec {
+
+ private CrossOriginResourcePolicySpec() {
+ }
+
+ /**
+ * Sets the value to be used in the `Cross-Origin-Resource-Policy` header
+ * @param resourcePolicy a opener policy
+ * @return the {@link CrossOriginResourcePolicySpec} to continue configuring
+ */
+ public CrossOriginResourcePolicySpec policy(CrossOriginResourcePolicy resourcePolicy) {
+ HeaderSpec.this.crossOriginResourcePolicy.setPolicy(resourcePolicy);
+ return this;
+ }
+
+ /**
+ * Allows method chaining to continue configuring the
+ * {@link ServerHttpSecurity}.
+ * @return the {@link HeaderSpec} to continue configuring
+ */
+ public HeaderSpec and() {
+ return HeaderSpec.this;
+ }
+
+ }
+
}
/**
diff --git a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java
index ba8d3ac4e06..246235a955a 100644
--- a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java
@@ -19,6 +19,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
import org.w3c.dom.Element;
@@ -37,20 +38,34 @@
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.beans.factory.xml.XmlReaderContext;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
+import org.springframework.security.access.expression.ExpressionUtils;
+import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.vote.ConsensusBased;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.Elements;
+import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
+import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageExpressionVoter;
+import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor;
+import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
+import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
+import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.util.AntPathMatcher;
+import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
@@ -99,6 +114,10 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
private static final String DISABLED_ATTR = "same-origin-disabled";
+ private static final String USE_AUTHORIZATION_MANAGER_ATTR = "use-authorization-manager";
+
+ private static final String AUTHORIZATION_MANAGER_REF_ATTR = "authorization-manager-ref";
+
private static final String PATTERN_ATTR = "pattern";
private static final String ACCESS_ATTR = "access";
@@ -114,14 +133,83 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
*/
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
+ String id = element.getAttribute(ID_ATTR);
+ String inSecurityInterceptorName = parseAuthorization(element, parserContext);
+ BeanDefinitionRegistry registry = parserContext.getRegistry();
+ if (StringUtils.hasText(id)) {
+ registry.registerAlias(inSecurityInterceptorName, id);
+ if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
+ registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
+ }
+ }
+ else {
+ boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR));
+ XmlReaderContext context = parserContext.getReaderContext();
+ BeanDefinitionBuilder mspp = BeanDefinitionBuilder.rootBeanDefinition(MessageSecurityPostProcessor.class);
+ mspp.addConstructorArgValue(inSecurityInterceptorName);
+ mspp.addConstructorArgValue(sameOriginDisabled);
+ context.registerWithGeneratedName(mspp.getBeanDefinition());
+ }
+ return null;
+ }
+
+ private String parseAuthorization(Element element, ParserContext parserContext) {
+ boolean useAuthorizationManager = Boolean.parseBoolean(element.getAttribute(USE_AUTHORIZATION_MANAGER_ATTR));
+ if (useAuthorizationManager) {
+ return parseAuthorizationManager(element, parserContext);
+ }
+ if (StringUtils.hasText(element.getAttribute(AUTHORIZATION_MANAGER_REF_ATTR))) {
+ return parseAuthorizationManager(element, parserContext);
+ }
+ return parseSecurityMetadataSource(element, parserContext);
+ }
+
+ private String parseAuthorizationManager(Element element, ParserContext parserContext) {
+ XmlReaderContext context = parserContext.getReaderContext();
+ String mdsId = createAuthorizationManager(element, parserContext);
+ BeanDefinitionBuilder inboundChannelSecurityInterceptor = BeanDefinitionBuilder
+ .rootBeanDefinition(AuthorizationChannelInterceptor.class);
+ inboundChannelSecurityInterceptor.addConstructorArgReference(mdsId);
+ return context.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition());
+ }
+
+ private String createAuthorizationManager(Element element, ParserContext parserContext) {
+ XmlReaderContext context = parserContext.getReaderContext();
+ String authorizationManagerRef = element.getAttribute(AUTHORIZATION_MANAGER_REF_ATTR);
+ if (StringUtils.hasText(authorizationManagerRef)) {
+ return authorizationManagerRef;
+ }
+ Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
+ String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null;
+ ManagedMap matcherToExpression = new ManagedMap<>();
+ List interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_MESSAGE);
+ for (Element interceptMessage : interceptMessages) {
+ String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR);
+ String accessExpression = interceptMessage.getAttribute(ACCESS_ATTR);
+ String messageType = interceptMessage.getAttribute(TYPE_ATTR);
+ BeanDefinition matcher = createMatcher(matcherPattern, messageType, parserContext, interceptMessage);
+ BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder
+ .rootBeanDefinition(ExpressionBasedAuthorizationManager.class);
+ if (StringUtils.hasText(expressionHandlerRef)) {
+ authorizationManager.addConstructorArgReference(expressionHandlerRef);
+ }
+ authorizationManager.addConstructorArgValue(accessExpression);
+ matcherToExpression.put(matcher, authorizationManager.getBeanDefinition());
+ }
+ BeanDefinitionBuilder mds = BeanDefinitionBuilder
+ .rootBeanDefinition(MessageMatcherDelegatingAuthorizationManagerFactory.class);
+ mds.setFactoryMethod("createMessageMatcherDelegatingAuthorizationManager");
+ mds.addConstructorArgValue(matcherToExpression);
+ return context.registerWithGeneratedName(mds.getBeanDefinition());
+ }
+
+ private String parseSecurityMetadataSource(Element element, ParserContext parserContext) {
BeanDefinitionRegistry registry = parserContext.getRegistry();
XmlReaderContext context = parserContext.getReaderContext();
ManagedMap matcherToExpression = new ManagedMap<>();
- String id = element.getAttribute(ID_ATTR);
Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null;
boolean expressionHandlerDefined = StringUtils.hasText(expressionHandlerRef);
- boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR));
List interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_MESSAGE);
for (Element interceptMessage : interceptMessages) {
String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR);
@@ -151,21 +239,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
.rootBeanDefinition(ChannelSecurityInterceptor.class);
inboundChannelSecurityInterceptor.addConstructorArgValue(registry.getBeanDefinition(mdsId));
inboundChannelSecurityInterceptor.addPropertyValue("accessDecisionManager", adm.getBeanDefinition());
- String inSecurityInterceptorName = context
- .registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition());
- if (StringUtils.hasText(id)) {
- registry.registerAlias(inSecurityInterceptorName, id);
- if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
- registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
- }
- }
- else {
- BeanDefinitionBuilder mspp = BeanDefinitionBuilder.rootBeanDefinition(MessageSecurityPostProcessor.class);
- mspp.addConstructorArgValue(inSecurityInterceptorName);
- mspp.addConstructorArgValue(sameOriginDisabled);
- context.registerWithGeneratedName(mspp.getBeanDefinition());
- }
- return null;
+ return context.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition());
}
private BeanDefinition createMatcher(String matcherPattern, String messageType, ParserContext parserContext,
@@ -341,4 +415,48 @@ void setPathMatcher(PathMatcher pathMatcher) {
}
+ private static final class ExpressionBasedAuthorizationManager
+ implements AuthorizationManager> {
+
+ private final SecurityExpressionHandler> expressionHandler;
+
+ private final Expression expression;
+
+ private ExpressionBasedAuthorizationManager(String expression) {
+ this(new MessageAuthorizationContextSecurityExpressionHandler(), expression);
+ }
+
+ private ExpressionBasedAuthorizationManager(
+ SecurityExpressionHandler> expressionHandler, String expression) {
+ Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+ Assert.notNull(expression, "expression cannot be null");
+ this.expressionHandler = expressionHandler;
+ this.expression = this.expressionHandler.getExpressionParser().parseExpression(expression);
+ }
+
+ @Override
+ public AuthorizationDecision check(Supplier authentication,
+ MessageAuthorizationContext> object) {
+ EvaluationContext context = this.expressionHandler.createEvaluationContext(authentication, object);
+ boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, context);
+ return new AuthorizationDecision(granted);
+ }
+
+ }
+
+ private static class MessageMatcherDelegatingAuthorizationManagerFactory {
+
+ private static AuthorizationManager> createMessageMatcherDelegatingAuthorizationManager(
+ Map, AuthorizationManager>> beans) {
+ MessageMatcherDelegatingAuthorizationManager.Builder builder = MessageMatcherDelegatingAuthorizationManager
+ .builder();
+ for (Map.Entry, AuthorizationManager>> entry : beans
+ .entrySet()) {
+ builder.matchers(entry.getKey()).access(entry.getValue());
+ }
+ return builder.anyMessage().permitAll().build();
+ }
+
+ }
+
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt
index 8df31aaca51..bfd029ec986 100644
--- a/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -22,6 +22,7 @@ import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.authorization.ReactiveAuthorizationManager
import org.springframework.security.core.Authentication
import org.springframework.security.web.server.authorization.AuthorizationContext
+import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
import org.springframework.security.web.util.matcher.RequestMatcher
@@ -108,6 +109,13 @@ class AuthorizeExchangeDsl {
fun hasAnyAuthority(vararg authorities: String): ReactiveAuthorizationManager =
AuthorityReactiveAuthorizationManager.hasAnyAuthority(*authorities)
+ /**
+ * Require a specific IP or range of IP addresses.
+ * @since 5.7
+ */
+ fun hasIpAddress(ipAddress: String): ReactiveAuthorizationManager =
+ IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress)
+
/**
* Require an authenticated user.
*/
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt
new file mode 100644
index 00000000000..cf5ae7ec9d9
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2021 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 org.springframework.security.config.web.server
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter
+
+/**
+ * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Embedder-Policy header using
+ * idiomatic Kotlin code.
+ *
+ * @author Marcus Da Coregio
+ * @since 5.7
+ * @property policy the policy to be used in the response header.
+ */
+@ServerSecurityMarker
+class ServerCrossOriginEmbedderPolicyDsl {
+
+ var policy: CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy? = null
+
+ internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginEmbedderPolicySpec) -> Unit {
+ return { crossOriginEmbedderPolicy ->
+ policy?.also {
+ crossOriginEmbedderPolicy.policy(policy)
+ }
+ }
+ }
+}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt
new file mode 100644
index 00000000000..70d6576c837
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2021 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 org.springframework.security.config.web.server
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter
+
+/**
+ * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Opener-Policy header using
+ * idiomatic Kotlin code.
+ *
+ * @author Marcus Da Coregio
+ * @since 5.7
+ * @property policy the policy to be used in the response header.
+ */
+@ServerSecurityMarker
+class ServerCrossOriginOpenerPolicyDsl {
+
+ var policy: CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy? = null
+
+ internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginOpenerPolicySpec) -> Unit {
+ return { crossOriginOpenerPolicy ->
+ policy?.also {
+ crossOriginOpenerPolicy.policy(policy)
+ }
+ }
+ }
+}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt
new file mode 100644
index 00000000000..580ee355ee7
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2021 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 org.springframework.security.config.web.server
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter
+
+/**
+ * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Resource-Policy header using
+ * idiomatic Kotlin code.
+ *
+ * @author Marcus Da Coregio
+ * @since 5.7
+ * @property policy the policy to be used in the response header.
+ */
+@ServerSecurityMarker
+class ServerCrossOriginResourcePolicyDsl {
+
+ var policy: CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy? = null
+
+ internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginResourcePolicySpec) -> Unit {
+ return { crossOriginResourcePolicy ->
+ policy?.also {
+ crossOriginResourcePolicy.policy(policy)
+ }
+ }
+ }
+}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt
index f38b1527218..37bd1f177a9 100644
--- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt
@@ -16,7 +16,12 @@
package org.springframework.security.config.web.server
-import org.springframework.security.web.server.header.*
+import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter
+import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
+import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter
+import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter
+import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
+import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter
/**
* A Kotlin DSL to configure [ServerHttpSecurity] headers using idiomatic Kotlin code.
@@ -35,6 +40,9 @@ class ServerHeadersDsl {
private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null
private var featurePolicyDirectives: String? = null
private var permissionsPolicy: ((ServerHttpSecurity.HeaderSpec.PermissionsPolicySpec) -> Unit)? = null
+ private var crossOriginOpenerPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginOpenerPolicySpec) -> Unit)? = null
+ private var crossOriginEmbedderPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginEmbedderPolicySpec) -> Unit)? = null
+ private var crossOriginResourcePolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginResourcePolicySpec) -> Unit)? = null
private var disabled = false
@@ -157,6 +165,39 @@ class ServerHeadersDsl {
this.permissionsPolicy = ServerPermissionsPolicyDsl().apply(permissionsPolicyConfig).get()
}
+ /**
+ * Allows configuration for
+ * Cross-Origin-Opener-Policy header.
+ *
+ * @since 5.7
+ * @param crossOriginOpenerPolicyConfig the customization to apply to the header
+ */
+ fun crossOriginOpenerPolicy(crossOriginOpenerPolicyConfig: ServerCrossOriginOpenerPolicyDsl.() -> Unit) {
+ this.crossOriginOpenerPolicy = ServerCrossOriginOpenerPolicyDsl().apply(crossOriginOpenerPolicyConfig).get()
+ }
+
+ /**
+ * Allows configuration for
+ * Cross-Origin-Embedder-Policy header.
+ *
+ * @since 5.7
+ * @param crossOriginEmbedderPolicyConfig the customization to apply to the header
+ */
+ fun crossOriginEmbedderPolicy(crossOriginEmbedderPolicyConfig: ServerCrossOriginEmbedderPolicyDsl.() -> Unit) {
+ this.crossOriginEmbedderPolicy = ServerCrossOriginEmbedderPolicyDsl().apply(crossOriginEmbedderPolicyConfig).get()
+ }
+
+ /**
+ * Allows configuration for
+ * Cross-Origin-Resource-Policy header.
+ *
+ * @since 5.7
+ * @param crossOriginResourcePolicyConfig the customization to apply to the header
+ */
+ fun crossOriginResourcePolicy(crossOriginResourcePolicyConfig: ServerCrossOriginResourcePolicyDsl.() -> Unit) {
+ this.crossOriginResourcePolicy = ServerCrossOriginResourcePolicyDsl().apply(crossOriginResourcePolicyConfig).get()
+ }
+
/**
* Disables HTTP response headers.
*/
@@ -194,6 +235,15 @@ class ServerHeadersDsl {
referrerPolicy?.also {
headers.referrerPolicy(referrerPolicy)
}
+ crossOriginOpenerPolicy?.also {
+ headers.crossOriginOpenerPolicy(crossOriginOpenerPolicy)
+ }
+ crossOriginEmbedderPolicy?.also {
+ headers.crossOriginEmbedderPolicy(crossOriginEmbedderPolicy)
+ }
+ crossOriginResourcePolicy?.also {
+ headers.crossOriginResourcePolicy(crossOriginResourcePolicy)
+ }
if (disabled) {
headers.disable()
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt
index 9e35287b5f5..3fd1f4ea283 100644
--- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * 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.
@@ -17,6 +17,8 @@
package org.springframework.security.config.web.servlet
import org.springframework.http.HttpMethod
+import org.springframework.security.authorization.AuthorizationManager
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext
import org.springframework.security.web.util.matcher.AnyRequestMatcher
import org.springframework.security.web.util.matcher.RequestMatcher
@@ -36,14 +38,25 @@ abstract class AbstractRequestMatcherDsl {
protected data class MatcherAuthorizationRule(val matcher: RequestMatcher,
override val rule: String) : AuthorizationRule(rule)
+ protected data class MatcherAuthorizationManagerRule(val matcher: RequestMatcher,
+ override val rule: AuthorizationManager) : AuthorizationManagerRule(rule)
+
protected data class PatternAuthorizationRule(val pattern: String,
val patternType: PatternType,
val servletPath: String? = null,
val httpMethod: HttpMethod? = null,
override val rule: String) : AuthorizationRule(rule)
+ protected data class PatternAuthorizationManagerRule(val pattern: String,
+ val patternType: PatternType,
+ val servletPath: String? = null,
+ val httpMethod: HttpMethod? = null,
+ override val rule: AuthorizationManager) : AuthorizationManagerRule(rule)
+
protected abstract class AuthorizationRule(open val rule: String)
+ protected abstract class AuthorizationManagerRule(open val rule: AuthorizationManager)
+
protected enum class PatternType {
ANT, MVC
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDsl.kt
new file mode 100644
index 00000000000..f0ac9f4971e
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDsl.kt
@@ -0,0 +1,260 @@
+/*
+ * 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 org.springframework.security.config.web.servlet
+
+import org.springframework.http.HttpMethod
+import org.springframework.security.authorization.AuthenticatedAuthorizationManager
+import org.springframework.security.authorization.AuthorityAuthorizationManager
+import org.springframework.security.authorization.AuthorizationDecision
+import org.springframework.security.authorization.AuthorizationManager
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer
+import org.springframework.security.core.Authentication
+import org.springframework.security.web.access.intercept.AuthorizationFilter
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext
+import org.springframework.security.web.util.matcher.AnyRequestMatcher
+import org.springframework.security.web.util.matcher.RequestMatcher
+import org.springframework.util.ClassUtils
+import java.util.function.Supplier
+
+/**
+ * A Kotlin DSL to configure [HttpSecurity] request authorization using idiomatic Kotlin code.
+ *
+ * @author Yuriy Savchenko
+ * @since 5.7
+ * @property shouldFilterAllDispatcherTypes whether the [AuthorizationFilter] should filter all dispatcher types
+ */
+class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() {
+ var shouldFilterAllDispatcherTypes: Boolean? = null
+
+ private val authorizationRules = mutableListOf()
+
+ private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector"
+ private val MVC_PRESENT = ClassUtils.isPresent(
+ HANDLER_MAPPING_INTROSPECTOR,
+ AuthorizeHttpRequestsDsl::class.java.classLoader)
+ private val PATTERN_TYPE = if (MVC_PRESENT) PatternType.MVC else PatternType.ANT
+
+ /**
+ * Adds a request authorization rule.
+ *
+ * @param matches the [RequestMatcher] to match incoming requests against
+ * @param access the [AuthorizationManager] to secure the matching request
+ * (i.e. created via hasAuthority("ROLE_USER"))
+ */
+ fun authorize(matches: RequestMatcher = AnyRequestMatcher.INSTANCE,
+ access: AuthorizationManager) {
+ authorizationRules.add(MatcherAuthorizationManagerRule(matches, access))
+ }
+
+ /**
+ * Adds a request authorization rule for an endpoint matching the provided
+ * pattern.
+ * If Spring MVC is on the classpath, it will use an MVC matcher.
+ * If Spring MVC is not on the classpath, it will use an ant matcher.
+ * The MVC will use the same rules that Spring MVC uses for matching.
+ * For example, often times a mapping of the path "/path" will match on
+ * "/path", "/path/", "/path.html", etc.
+ * If the current request will not be processed by Spring MVC, a reasonable default
+ * using the pattern as an ant pattern will be used.
+ *
+ * @param pattern the pattern to match incoming requests against.
+ * @param access the [AuthorizationManager] to secure the matching request
+ * (i.e. created via hasAuthority("ROLE_USER"))
+ */
+ fun authorize(pattern: String,
+ access: AuthorizationManager) {
+ authorizationRules.add(
+ PatternAuthorizationManagerRule(
+ pattern = pattern,
+ patternType = PATTERN_TYPE,
+ rule = access
+ )
+ )
+ }
+
+ /**
+ * Adds a request authorization rule for an endpoint matching the provided
+ * pattern.
+ * If Spring MVC is on the classpath, it will use an MVC matcher.
+ * If Spring MVC is not on the classpath, it will use an ant matcher.
+ * The MVC will use the same rules that Spring MVC uses for matching.
+ * For example, often times a mapping of the path "/path" will match on
+ * "/path", "/path/", "/path.html", etc.
+ * If the current request will not be processed by Spring MVC, a reasonable default
+ * using the pattern as an ant pattern will be used.
+ *
+ * @param method the HTTP method to match the income requests against.
+ * @param pattern the pattern to match incoming requests against.
+ * @param access the [AuthorizationManager] to secure the matching request
+ * (i.e. created via hasAuthority("ROLE_USER"))
+ */
+ fun authorize(method: HttpMethod,
+ pattern: String,
+ access: AuthorizationManager) {
+ authorizationRules.add(
+ PatternAuthorizationManagerRule(
+ pattern = pattern,
+ patternType = PATTERN_TYPE,
+ httpMethod = method,
+ rule = access
+ )
+ )
+ }
+
+ /**
+ * Adds a request authorization rule for an endpoint matching the provided
+ * pattern.
+ * If Spring MVC is on the classpath, it will use an MVC matcher.
+ * If Spring MVC is not on the classpath, it will use an ant matcher.
+ * The MVC will use the same rules that Spring MVC uses for matching.
+ * For example, often times a mapping of the path "/path" will match on
+ * "/path", "/path/", "/path.html", etc.
+ * If the current request will not be processed by Spring MVC, a reasonable default
+ * using the pattern as an ant pattern will be used.
+ *
+ * @param pattern the pattern to match incoming requests against.
+ * @param servletPath the servlet path to match incoming requests against. This
+ * only applies when using an MVC pattern matcher.
+ * @param access the [AuthorizationManager] to secure the matching request
+ * (i.e. created via hasAuthority("ROLE_USER"))
+ */
+ fun authorize(pattern: String,
+ servletPath: String,
+ access: AuthorizationManager