Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3e7d76b70 | |||
| 9dde69746f | |||
| 884cf0d62e | |||
| aaf738f7ac | |||
| ccd39a23c9 | |||
| 793820acfa | |||
| b6ed037c39 | |||
| 5da0cbea4b | |||
| e6b4d461e7 | |||
| 4daf089e46 | |||
| 6501e97ece | |||
| 3a84894bf4 | |||
| 90855aa128 | |||
| 9f7e92d6f2 | |||
| 727f0e27d6 | |||
| f548aaf5c5 | |||
| 743817fc15 | |||
| fb701e4615 | |||
| 1c112005fa | |||
| e0a71eb00e | |||
| 69d28dc35b | |||
| 42ddaba870 | |||
| da46ba2619 | |||
| a406f5fe2d | |||
| dcb4e47cd5 | |||
| 82f87cf2b6 | |||
| 0a2f55d485 | |||
| 9b61533db2 | |||
| eb43830260 | |||
| b55c28cf25 | |||
| 0927bed66a | |||
| 9ed446e6f5 | |||
| 56a23d9ddc | |||
| dc5aed9b5f |
@@ -0,0 +1,41 @@
|
||||
name: Finalize Release
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
project-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.project-version.outputs.version }}
|
||||
steps:
|
||||
- id: project-version
|
||||
run: echo "version=$(grep '^version=' gradle.properties | cut -d'=' -f2)" >> $GITHUB_OUTPUT
|
||||
perform-release:
|
||||
name: Perform Release
|
||||
needs: [ project-version ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
|
||||
with:
|
||||
should-perform-release: true
|
||||
project-version: ${{ needs.project-version.outputs.version }}
|
||||
milestone-repo-url: https://repo1.maven.org/maven2
|
||||
release-repo-url: https://repo1.maven.org/maven2
|
||||
artifact-path: org/springframework/security/spring-security-core
|
||||
slack-announcing-id: spring-security-announcing
|
||||
secrets: inherit
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ perform-release ]
|
||||
if: ${{ !success() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
+5
-5
@@ -27,14 +27,14 @@ import org.springframework.security.authorization.AuthorizationManagerFactories;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
|
||||
/**
|
||||
* Uses {@link EnableGlobalMultiFactorAuthentication} to configure a
|
||||
* Uses {@link EnableMultiFactorAuthentication} to configure a
|
||||
* {@link DefaultAuthorizationManagerFactory}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 7.0
|
||||
* @see EnableGlobalMultiFactorAuthentication
|
||||
* @see EnableMultiFactorAuthentication
|
||||
*/
|
||||
class GlobalMultiFactorAuthenticationConfiguration implements ImportAware {
|
||||
class AuthorizationManagerFactoryConfiguration implements ImportAware {
|
||||
|
||||
private String[] authorities;
|
||||
|
||||
@@ -49,9 +49,9 @@ class GlobalMultiFactorAuthenticationConfiguration implements ImportAware {
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
Map<String, Object> multiFactorAuthenticationAttrs = importMetadata
|
||||
.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
|
||||
.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName());
|
||||
|
||||
this.authorities = (String[]) multiFactorAuthenticationAttrs.get("authorities");
|
||||
this.authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.authorization;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class EnableMfaFiltersConfiguration {
|
||||
|
||||
@Bean
|
||||
BeanPostProcessor mfaBeanPostProcessor() {
|
||||
return new EnableMfaFiltersPostProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link BeanPostProcessor} that enables MFA on authentication filters.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 7.0
|
||||
*/
|
||||
private static class EnableMfaFiltersPostProcessor implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public @Nullable Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof AbstractAuthenticationProcessingFilter filter) {
|
||||
filter.setMfaEnabled(true);
|
||||
}
|
||||
if (bean instanceof AuthenticationFilter filter) {
|
||||
filter.setMfaEnabled(true);
|
||||
}
|
||||
if (bean instanceof AbstractPreAuthenticatedProcessingFilter filter) {
|
||||
filter.setMfaEnabled(true);
|
||||
}
|
||||
if (bean instanceof BasicAuthenticationFilter filter) {
|
||||
filter.setMfaEnabled(true);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+12
-7
@@ -26,9 +26,12 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
|
||||
/**
|
||||
* Exposes a {@link DefaultAuthorizationManagerFactory} as a Bean with the
|
||||
* {@link #authorities()} specified as additional required authorities. The configuration
|
||||
* will be picked up by both
|
||||
* Enables Multi-Factor Authentication (MFA) support within Spring Security.
|
||||
*
|
||||
* When {@link #authorities()} is specified creates a
|
||||
* {@link DefaultAuthorizationManagerFactory} as a Bean with the {@link #authorities()}
|
||||
* specified as additional required authorities. The configuration will be picked up by
|
||||
* both
|
||||
* {@link org.springframework.security.config.annotation.web.configuration.EnableWebSecurity}
|
||||
* and
|
||||
* {@link org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity}.
|
||||
@@ -36,7 +39,7 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
|
||||
* <pre>
|
||||
|
||||
* @Configuration
|
||||
* @EnableGlobalMultiFactorAuthentication(authorities = { GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
|
||||
* @EnableMultiFactorAuthentication(authorities = { GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
|
||||
* public class MyConfiguration {
|
||||
* // ...
|
||||
* }
|
||||
@@ -51,13 +54,15 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Import(GlobalMultiFactorAuthenticationConfiguration.class)
|
||||
public @interface EnableGlobalMultiFactorAuthentication {
|
||||
@Import(MultiFactorAuthenticationSelector.class)
|
||||
public @interface EnableMultiFactorAuthentication {
|
||||
|
||||
/**
|
||||
* The additional authorities that are required.
|
||||
* @return the additional authorities that are required (e.g. {
|
||||
* FactorGrantedAuthority.FACTOR_OTT, FactorGrantedAuthority.FACTOR_PASSWORD })
|
||||
* FactorGrantedAuthority.FACTOR_OTT, FactorGrantedAuthority.FACTOR_PASSWORD }). Can
|
||||
* be null or an empty array if no additional authorities are required (if
|
||||
* authorization rules are not globally requiring MFA).
|
||||
* @see org.springframework.security.core.authority.FactorGrantedAuthority
|
||||
*/
|
||||
String[] authorities();
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2004-present 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.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.annotation.ImportSelector;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
|
||||
/**
|
||||
* Uses {@link EnableMultiFactorAuthentication} to configure a
|
||||
* {@link DefaultAuthorizationManagerFactory}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 7.0
|
||||
* @see EnableMultiFactorAuthentication
|
||||
*/
|
||||
class MultiFactorAuthenticationSelector implements ImportSelector {
|
||||
|
||||
@Override
|
||||
public String[] selectImports(AnnotationMetadata metadata) {
|
||||
Map<String, Object> multiFactorAuthenticationAttrs = metadata
|
||||
.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName());
|
||||
String[] authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
|
||||
List<String> imports = new ArrayList<>(2);
|
||||
if (authorities.length > 0) {
|
||||
imports.add(AuthorizationManagerFactoryConfiguration.class.getName());
|
||||
}
|
||||
imports.add(EnableMfaFiltersConfiguration.class.getName());
|
||||
return imports.toArray(new String[imports.size()]);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -35,7 +35,7 @@ import jakarta.servlet.http.HttpSession
|
||||
* @since 5.3
|
||||
* @property clearAuthentication whether the [SecurityContextLogoutHandler] should clear
|
||||
* the [Authentication] at the time of logout.
|
||||
* @property clearAuthentication whether to invalidate the [HttpSession] at the time of logout.
|
||||
* @property invalidateHttpSession whether to invalidate the [HttpSession] at the time of logout.
|
||||
* @property logoutUrl the URL that triggers log out to occur.
|
||||
* @property logoutRequestMatcher the [RequestMatcher] that triggers log out to occur.
|
||||
* @property logoutSuccessUrl the URL to redirect to after logout has occurred.
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2004-present 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.authorization;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link EnableMultiFactorAuthentication}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@WithMockUser(authorities = FactorGrantedAuthority.PASSWORD_AUTHORITY)
|
||||
public class EnableMultiFactorAuthenticationFiltersSetTests {
|
||||
|
||||
@Autowired
|
||||
private AuthenticationManager manager;
|
||||
|
||||
private TestingAuthenticationToken newAuthn = new TestingAuthenticationToken("user", "password", "ROLE_USER",
|
||||
FactorGrantedAuthority.OTT_AUTHORITY);
|
||||
|
||||
@Test
|
||||
void preAuthenticationFilter(@Autowired AbstractAuthenticationProcessingFilter filter) throws Exception {
|
||||
assertMfaEnabled(filter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticationFilter(@Autowired AuthenticationFilter filter) throws Exception {
|
||||
assertMfaEnabled(filter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preAuthnFilter(@Autowired AbstractPreAuthenticatedProcessingFilter filter) throws Exception {
|
||||
assertMfaEnabled(filter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void basicAuthnFilter(@Autowired BasicAuthenticationFilter filter) throws Exception {
|
||||
assertMfaEnabled(filter);
|
||||
}
|
||||
|
||||
private void assertMfaEnabled(Filter filter) throws Exception {
|
||||
given(this.manager.authenticate(any())).willReturn(this.newAuthn);
|
||||
MockHttpServletRequest request = MockMvcRequestBuilders.get("/")
|
||||
.headers((headers) -> headers.setBasicAuth("u", "p"))
|
||||
.buildRequest(new MockServletContext());
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
filter.doFilter(request, response, chain);
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isNotNull();
|
||||
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactlyInAnyOrder(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY,
|
||||
"ROLE_USER");
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableMultiFactorAuthentication(
|
||||
authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
AuthenticationManager authenticationManager() {
|
||||
return mock(AuthenticationManager.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
static AbstractAuthenticationProcessingFilter authnProcessingFilter(
|
||||
AuthenticationManager authenticationManager) {
|
||||
AbstractAuthenticationProcessingFilter result = new AbstractAuthenticationProcessingFilter(
|
||||
AnyRequestMatcher.INSTANCE, authenticationManager) {
|
||||
};
|
||||
result.setAuthenticationConverter(new BasicAuthenticationConverter());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Bean
|
||||
static AuthenticationFilter authenticationFilter(AuthenticationManager authenticationManager) {
|
||||
return new AuthenticationFilter(authenticationManager, new BasicAuthenticationConverter());
|
||||
}
|
||||
|
||||
@Bean
|
||||
static AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter(
|
||||
AuthenticationManager authenticationManager) {
|
||||
AbstractPreAuthenticatedProcessingFilter result = new AbstractPreAuthenticatedProcessingFilter() {
|
||||
@Override
|
||||
protected @Nullable Object getPreAuthenticatedCredentials(HttpServletRequest request) {
|
||||
return "password";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
|
||||
return "user";
|
||||
}
|
||||
};
|
||||
result.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
|
||||
result.setAuthenticationManager(authenticationManager);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Bean
|
||||
static BasicAuthenticationFilter basicAuthenticationFilter(AuthenticationManager authenticationManager) {
|
||||
return new BasicAuthenticationFilter(authenticationManager);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+57
-3
@@ -16,6 +16,13 @@
|
||||
|
||||
package org.springframework.security.config.annotation.authorization;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -25,10 +32,18 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
@@ -37,18 +52,22 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests for {@link EnableGlobalMultiFactorAuthentication}.
|
||||
* Tests for {@link EnableMultiFactorAuthentication}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
public class EnableMultiFactorAuthenticationTests {
|
||||
|
||||
private static final String ATTR_NAME = "org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors$SecurityContextRequestPostProcessorSupport$TestSecurityContextRepository.REPO";
|
||||
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
@@ -56,6 +75,14 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
@Autowired
|
||||
Service service;
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { "ROLE_USER", FactorGrantedAuthority.OTT_AUTHORITY })
|
||||
public void formLoginWhenAuthenticatedThenMergedAuthorities() throws Exception {
|
||||
this.mvc.perform(formLogin())
|
||||
.andExpect(authenticated().withAuthorities("ROLE_USER", FactorGrantedAuthority.OTT_AUTHORITY,
|
||||
FactorGrantedAuthority.PASSWORD_AUTHORITY));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY })
|
||||
void webWhenAuthorized() throws Exception {
|
||||
@@ -84,7 +111,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@Configuration
|
||||
@EnableGlobalMultiFactorAuthentication(
|
||||
@EnableMultiFactorAuthentication(
|
||||
authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
|
||||
static class Config {
|
||||
|
||||
@@ -98,6 +125,33 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
return MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
static Customizer<HttpSecurity> captureAuthn() {
|
||||
return (http) -> http.addFilterAfter(new Filter() {
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
|
||||
FilterChain filterChain) throws IOException, ServletException {
|
||||
try {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
finally {
|
||||
servletRequest.setAttribute(ATTR_NAME, SecurityContextHolder.getContext());
|
||||
}
|
||||
}
|
||||
}, SecurityContextHolderFilter.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("deprecation")
|
||||
UserDetailsService userDetailsService() {
|
||||
UserDetails user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class OkController {
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ public final class AllRequiredFactorsAuthorizationManager<T> implements Authoriz
|
||||
private @Nullable RequiredFactorError requiredFactorError(RequiredFactor requiredFactor,
|
||||
List<GrantedAuthority> currentFactors) {
|
||||
Optional<GrantedAuthority> matchingAuthority = currentFactors.stream()
|
||||
.filter((authority) -> authority.getAuthority().equals(requiredFactor.getAuthority()))
|
||||
.filter((authority) -> Objects.equals(authority.getAuthority(), requiredFactor.getAuthority()))
|
||||
.findFirst();
|
||||
if (!matchingAuthority.isPresent()) {
|
||||
return RequiredFactorError.createMissing(requiredFactor);
|
||||
|
||||
+5
-2
@@ -17,7 +17,10 @@
|
||||
package org.springframework.security.authorization;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -47,8 +50,8 @@ public class AuthorityReactiveAuthorizationManager<T> implements ReactiveAuthori
|
||||
// @formatter:off
|
||||
return authentication.filter(Authentication::isAuthenticated)
|
||||
.flatMapIterable(Authentication::getAuthorities)
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.any((grantedAuthority) -> this.authorities.stream().anyMatch((authority) -> authority.getAuthority().equals(grantedAuthority)))
|
||||
.mapNotNull((Function<GrantedAuthority, @Nullable String>) GrantedAuthority::getAuthority)
|
||||
.any((grantedAuthority) -> this.authorities.stream().anyMatch((authority) -> Objects.equals(authority.getAuthority(), grantedAuthority)))
|
||||
.map((granted) -> ((AuthorizationResult) new AuthorityAuthorizationDecision(granted, this.authorities)))
|
||||
.defaultIfEmpty(new AuthorityAuthorizationDecision(false, this.authorities));
|
||||
// @formatter:on
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.security.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
|
||||
/**
|
||||
@@ -46,6 +48,6 @@ public interface GrantedAuthority extends Serializable {
|
||||
* granted authority cannot be expressed as a <code>String</code> with sufficient
|
||||
* precision).
|
||||
*/
|
||||
String getAuthority();
|
||||
@Nullable String getAuthority();
|
||||
|
||||
}
|
||||
|
||||
+9
-3
@@ -27,6 +27,7 @@ import org.springframework.util.Assert;
|
||||
* {@link org.springframework.security.core.Authentication Authentication} object.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Yanming Zhou
|
||||
*/
|
||||
public final class SimpleGrantedAuthority implements GrantedAuthority {
|
||||
|
||||
@@ -34,9 +35,14 @@ public final class SimpleGrantedAuthority implements GrantedAuthority {
|
||||
|
||||
private final String role;
|
||||
|
||||
public SimpleGrantedAuthority(String role) {
|
||||
Assert.hasText(role, "A granted authority textual representation is required");
|
||||
this.role = role;
|
||||
/**
|
||||
* Constructs a {@code SimpleGrantedAuthority} using the provided authority.
|
||||
* @param authority The provided authority, including any prefix; for example,
|
||||
* {@code ROLE_ADMIN}
|
||||
*/
|
||||
public SimpleGrantedAuthority(String authority) {
|
||||
Assert.hasText(authority, "A granted authority textual representation is required");
|
||||
this.role = authority;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-1
@@ -64,7 +64,10 @@ public final class SimpleAuthorityMapper implements GrantedAuthoritiesMapper, In
|
||||
public Set<GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
|
||||
HashSet<GrantedAuthority> mapped = new HashSet<>(authorities.size());
|
||||
for (GrantedAuthority authority : authorities) {
|
||||
mapped.add(mapAuthority(authority.getAuthority()));
|
||||
String authorityStr = authority.getAuthority();
|
||||
if (authorityStr != null) {
|
||||
mapped.add(mapAuthority(authorityStr));
|
||||
}
|
||||
}
|
||||
if (this.defaultAuthority != null) {
|
||||
mapped.add(this.defaultAuthority);
|
||||
|
||||
+12
-5
@@ -36,10 +36,9 @@ import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* This utility class will find all the Jackson modules contributed by Spring Security in
|
||||
* the classpath (except {@code OAuth2AuthorizationServerJacksonModule} and
|
||||
* {@code WebauthnJacksonModule}), enable automatic inclusion of type information and
|
||||
* configure a {@link PolymorphicTypeValidator} that handles the validation of class
|
||||
* names.
|
||||
* the classpath (except {@code WebauthnJacksonModule}), enable automatic inclusion of
|
||||
* type information and configure a {@link PolymorphicTypeValidator} that handles the
|
||||
* validation of class names.
|
||||
*
|
||||
* <p>
|
||||
* <pre>
|
||||
@@ -77,6 +76,8 @@ public final class SecurityJacksonModules {
|
||||
|
||||
private static final String oauth2ClientJacksonModuleClass = "org.springframework.security.oauth2.client.jackson.OAuth2ClientJacksonModule";
|
||||
|
||||
private static final String oauth2AuthorizationServerJacksonModuleClass = "org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationServerJacksonModule";
|
||||
|
||||
private static final String ldapJacksonModuleClass = "org.springframework.security.ldap.jackson.LdapJacksonModule";
|
||||
|
||||
private static final String saml2JacksonModuleClass = "org.springframework.security.saml2.jackson.Saml2JacksonModule";
|
||||
@@ -87,6 +88,8 @@ public final class SecurityJacksonModules {
|
||||
|
||||
private static final boolean oauth2ClientPresent;
|
||||
|
||||
private static final boolean oauth2AuthorizationServerPresent;
|
||||
|
||||
private static final boolean ldapJacksonPresent;
|
||||
|
||||
private static final boolean saml2JacksonPresent;
|
||||
@@ -94,11 +97,12 @@ public final class SecurityJacksonModules {
|
||||
private static final boolean casJacksonPresent;
|
||||
|
||||
static {
|
||||
|
||||
ClassLoader classLoader = SecurityJacksonModules.class.getClassLoader();
|
||||
webServletPresent = ClassUtils.isPresent("jakarta.servlet.http.Cookie", classLoader);
|
||||
oauth2ClientPresent = ClassUtils.isPresent("org.springframework.security.oauth2.client.OAuth2AuthorizedClient",
|
||||
classLoader);
|
||||
oauth2AuthorizationServerPresent = ClassUtils
|
||||
.isPresent("org.springframework.security.oauth2.server.authorization.OAuth2Authorization", classLoader);
|
||||
ldapJacksonPresent = ClassUtils.isPresent(ldapJacksonModuleClass, classLoader);
|
||||
saml2JacksonPresent = ClassUtils.isPresent(saml2JacksonModuleClass, classLoader);
|
||||
casJacksonPresent = ClassUtils.isPresent(casJacksonModuleClass, classLoader);
|
||||
@@ -156,6 +160,9 @@ public final class SecurityJacksonModules {
|
||||
if (oauth2ClientPresent) {
|
||||
addToModulesList(loader, modules, oauth2ClientJacksonModuleClass);
|
||||
}
|
||||
if (oauth2AuthorizationServerPresent) {
|
||||
addToModulesList(loader, modules, oauth2AuthorizationServerJacksonModuleClass);
|
||||
}
|
||||
if (ldapJacksonPresent) {
|
||||
addToModulesList(loader, modules, ldapJacksonModuleClass);
|
||||
}
|
||||
|
||||
@@ -67,68 +67,12 @@ Instead Spring Security introduces `DelegatingPasswordEncoder`, which solves all
|
||||
You can easily construct an instance of `DelegatingPasswordEncoder` by using `PasswordEncoderFactories`:
|
||||
|
||||
.Create Default DelegatingPasswordEncoder
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
PasswordEncoder passwordEncoder =
|
||||
PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
|
||||
----
|
||||
======
|
||||
include-code::./DelegatingPasswordEncoderUsage[tag=createDefaultPasswordEncoder,indent=0]
|
||||
|
||||
Alternatively, you can create your own custom instance:
|
||||
|
||||
.Create Custom DelegatingPasswordEncoder
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
String idForEncode = "bcrypt";
|
||||
Map encoders = new HashMap<>();
|
||||
encoders.put(idForEncode, new BCryptPasswordEncoder());
|
||||
encoders.put("noop", NoOpPasswordEncoder.getInstance());
|
||||
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
|
||||
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
|
||||
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
|
||||
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
encoders.put("sha256", new StandardPasswordEncoder());
|
||||
|
||||
PasswordEncoder passwordEncoder =
|
||||
new DelegatingPasswordEncoder(idForEncode, encoders);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val idForEncode = "bcrypt"
|
||||
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
|
||||
encoders[idForEncode] = BCryptPasswordEncoder()
|
||||
encoders["noop"] = NoOpPasswordEncoder.getInstance()
|
||||
encoders["pbkdf2"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()
|
||||
encoders["pbkdf2@SpringSecurity_v5_8"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
encoders["scrypt"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()
|
||||
encoders["scrypt@SpringSecurity_v5_8"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
encoders["argon2"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()
|
||||
encoders["argon2@SpringSecurity_v5_8"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
encoders["sha256"] = StandardPasswordEncoder()
|
||||
|
||||
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
|
||||
----
|
||||
======
|
||||
include-code::./DelegatingPasswordEncoderUsage[tag=createCustomPasswordEncoder,indent=0]
|
||||
|
||||
[[authentication-password-storage-dpe-format]]
|
||||
=== Password Storage Format
|
||||
@@ -209,74 +153,12 @@ If you are putting together a demo or a sample, it is a bit cumbersome to take t
|
||||
There are convenience mechanisms to make this easier, but this is still not intended for production.
|
||||
|
||||
.withDefaultPasswordEncoder Example
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
UserDetails user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("user")
|
||||
.build();
|
||||
System.out.println(user.getPassword());
|
||||
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
val user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("user")
|
||||
.build()
|
||||
println(user.password)
|
||||
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
|
||||
----
|
||||
======
|
||||
include-code::./WithDefaultPasswordEncoderUsage[tag=createSingleUser,indent=0]
|
||||
|
||||
If you are creating multiple users, you can also reuse the builder:
|
||||
|
||||
.withDefaultPasswordEncoder Reusing the Builder
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||||
UserDetails user = users
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = users
|
||||
.username("admin")
|
||||
.password("password")
|
||||
.roles("USER","ADMIN")
|
||||
.build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val users = User.withDefaultPasswordEncoder()
|
||||
val user = users
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
val admin = users
|
||||
.username("admin")
|
||||
.password("password")
|
||||
.roles("USER", "ADMIN")
|
||||
.build()
|
||||
----
|
||||
======
|
||||
include-code::./WithDefaultPasswordEncoderUsage[tag=createMultipleUsers,indent=0]
|
||||
|
||||
This does hash the password that is stored, but the passwords are still exposed in memory and in the compiled source code.
|
||||
Therefore, it is still not considered secure for a production environment.
|
||||
@@ -337,28 +219,7 @@ The default implementation of `BCryptPasswordEncoder` uses strength 10 as mentio
|
||||
tune and test the strength parameter on your own system so that it takes roughly 1 second to verify a password.
|
||||
|
||||
.BCryptPasswordEncoder
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with strength 16
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with strength 16
|
||||
val encoder = BCryptPasswordEncoder(16)
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
======
|
||||
include-code::./BCryptPasswordEncoderUsage[tag=bcryptPasswordEncoder,indent=0]
|
||||
|
||||
[[authentication-password-storage-argon2]]
|
||||
== Argon2PasswordEncoder
|
||||
@@ -370,28 +231,7 @@ Like other adaptive one-way functions, it should be tuned to take about 1 second
|
||||
The current implementation of the `Argon2PasswordEncoder` requires BouncyCastle.
|
||||
|
||||
.Argon2PasswordEncoder
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
======
|
||||
include-code::./Argon2PasswordEncoderUsage[tag=argon2PasswordEncoder,indent=0]
|
||||
|
||||
[[authentication-password-storage-pbkdf2]]
|
||||
== Pbkdf2PasswordEncoder
|
||||
@@ -402,28 +242,7 @@ Like other adaptive one-way functions, it should be tuned to take about 1 second
|
||||
This algorithm is a good choice when FIPS certification is required.
|
||||
|
||||
.Pbkdf2PasswordEncoder
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
======
|
||||
include-code::./Pbkdf2PasswordEncoderUsage[tag=pbkdf2PasswordEncoder,indent=0]
|
||||
|
||||
[[authentication-password-storage-scrypt]]
|
||||
== SCryptPasswordEncoder
|
||||
@@ -433,28 +252,7 @@ To defeat password cracking on custom hardware, scrypt is a deliberately slow al
|
||||
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
|
||||
|
||||
.SCryptPasswordEncoder
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
======
|
||||
include-code::./SCryptPasswordEncoderUsage[tag=sCryptPasswordEncoder,indent=0]
|
||||
|
||||
[[authentication-password-storage-other]]
|
||||
== Other ``PasswordEncoder``s
|
||||
@@ -715,86 +513,4 @@ However, just a 401 or the redirect is not so useful in that case, it will cause
|
||||
In such cases, you can handle the `CompromisedPasswordException` via the `AuthenticationFailureHandler` to perform your desired logic, like redirecting the user-agent to `/reset-password`, for example:
|
||||
|
||||
.Using CompromisedPasswordChecker
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin((login) -> login
|
||||
.failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompromisedPasswordChecker compromisedPasswordChecker() {
|
||||
return new HaveIBeenPwnedRestApiPasswordChecker();
|
||||
}
|
||||
|
||||
static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
|
||||
"/login?error");
|
||||
|
||||
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) throws IOException, ServletException {
|
||||
if (exception instanceof CompromisedPasswordException) {
|
||||
this.redirectStrategy.sendRedirect(request, response, "/reset-password");
|
||||
return;
|
||||
}
|
||||
this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun filterChain(http:HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin {
|
||||
failureHandler = CompromisedPasswordAuthenticationFailureHandler()
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun compromisedPasswordChecker(): CompromisedPasswordChecker {
|
||||
return HaveIBeenPwnedRestApiPasswordChecker()
|
||||
}
|
||||
|
||||
class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler {
|
||||
private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error")
|
||||
private val redirectStrategy = DefaultRedirectStrategy()
|
||||
|
||||
override fun onAuthenticationFailure(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
exception: AuthenticationException
|
||||
) {
|
||||
if (exception is CompromisedPasswordException) {
|
||||
redirectStrategy.sendRedirect(request, response, "/reset-password")
|
||||
return
|
||||
}
|
||||
defaultFailureHandler.onAuthenticationFailure(request, response, exception)
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./CompromisedPasswordCheckerUsage[tag=configuration,indent=0]
|
||||
|
||||
@@ -86,6 +86,7 @@ The following Spring Security modules provide Jackson support:
|
||||
- spring-security-core (javadoc:org.springframework.security.jackson.CoreJacksonModule[])
|
||||
- spring-security-web (javadoc:org.springframework.security.web.jackson.WebJacksonModule[], javadoc:org.springframework.security.web.jackson.WebServletJacksonModule[], javadoc:org.springframework.security.web.server.jackson.WebServerJacksonModule[])
|
||||
- spring-security-oauth2-client (javadoc:org.springframework.security.oauth2.client.jackson.OAuth2ClientJacksonModule[])
|
||||
- spring-security-oauth2-authorization-server (javadoc:org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationServerJacksonModule[])
|
||||
- spring-security-cas (javadoc:org.springframework.security.cas.jackson.CasJacksonModule[])
|
||||
- spring-security-ldap (javadoc:org.springframework.security.ldap.jackson.LdapJacksonModule[])
|
||||
- spring-security-saml2 (javadoc:org.springframework.security.saml2.jackson.Saml2JacksonModule[])
|
||||
|
||||
@@ -18,17 +18,17 @@ In order to require MFA with Spring Security you must:
|
||||
- Specify an authorization rule that requires multiple factors
|
||||
- Setup authentication for each of those factors
|
||||
|
||||
[[egmfa]]
|
||||
== @EnableGlobalMultiFactorAuthentication
|
||||
[[emfa]]
|
||||
== @EnableMultiFactorAuthentication
|
||||
|
||||
javadoc:org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication[format=annotation] simplifies Global MFA (the entire application requires MFA).
|
||||
javadoc:org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication[format=annotation] makes it easy to enable multifactor authentication.
|
||||
Below you can find a configuration that adds the requirement for both passwords and OTT to every authorization rule.
|
||||
|
||||
include-code::./EnableGlobalMultiFactorAuthenticationConfiguration[tag=enable-global-mfa,indent=0]
|
||||
include-code::./EnableMultiFactorAuthenticationConfiguration[tag=enable-mfa,indent=0]
|
||||
|
||||
We are now able to concisely create a configuration that always requires multiple factors.
|
||||
|
||||
include-code::./EnableGlobalMultiFactorAuthenticationConfiguration[tag=httpSecurity,indent=0]
|
||||
include-code::./EnableMultiFactorAuthenticationConfiguration[tag=httpSecurity,indent=0]
|
||||
<1> URLs that begin with `/admin/**` require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`, `ROLE_ADMIN`.
|
||||
<2> Every other URL requires the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`
|
||||
<3> Set up the authentication mechanisms that can provide the required factors.
|
||||
@@ -40,18 +40,18 @@ If the user logged in initially with a token, then Spring Security redirects to
|
||||
[[authorization-manager-factory]]
|
||||
== AuthorizationManagerFactory
|
||||
|
||||
The `@EnableGlobalMultiFactorAuthentication` annotation is just a shortcut for publishing an javadoc:org.springframework.security.authorization.AuthorizationManagerFactory[] Bean.
|
||||
The `@EnableMultiFactorAuthentication` `authorities` property is just a shortcut for publishing an javadoc:org.springframework.security.authorization.AuthorizationManagerFactory[] Bean.
|
||||
When an `AuthorizationManagerFactory` Bean is available, it is used by Spring Security to create authorization rules, like `hasAnyRole(String)`, that are defined on the `AuthorizationManagerFactory` Bean interface.
|
||||
The implementation published by `@EnableGlobalMultiFactorAuthentication` will ensure that each authorization is combined with the requirement of having the specified factors.
|
||||
The implementation published by `@EnableMultiFactorAuthentication` will ensure that each authorization is combined with the requirement of having the specified factors.
|
||||
|
||||
The `AuthorizationManagerFactory` Bean below is what is published in the previously discussed xref:./mfa.adoc#using-egmfa[`@EnableGlobalMultiFactorAuthentication` example].
|
||||
The `AuthorizationManagerFactory` Bean below is what is published in the previously discussed xref:./mfa.adoc#emfa[`@EnableMultiFactorAuthentication` example].
|
||||
|
||||
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=authorizationManagerFactoryBean,indent=0]
|
||||
|
||||
[[selective-mfa]]
|
||||
== Selectively Requiring MFA
|
||||
|
||||
We have demonstrated how to configure an entire application to require MFA (Global MFA) by using xref:./mfa.adoc#egmfa[`@EnableGlobalMultiFactorAuthentication`].
|
||||
We have demonstrated how to configure an entire application to require MFA by using xref:./mfa.adoc#emfa[`@EnableMultiFactorAuthentication`]s `authorities` property.
|
||||
However, there are times that an application only wants parts of the application to require MFA.
|
||||
Consider the following requirements:
|
||||
|
||||
@@ -63,6 +63,13 @@ In this case, some URLs require MFA while others do not.
|
||||
This means that the global approach that we saw before does not work.
|
||||
Fortunately, we can use what we learned in xref:./mfa.adoc#authorization-manager-factory[] to solve this in a concise manner.
|
||||
|
||||
Start by specifying `@EnableMultiFactorAuthentication` without any authorities.
|
||||
By doing so we enable MFA support, but no `AuthorizationManagerFactory` Bean is published.
|
||||
|
||||
include-code::./SelectiveMfaConfiguration[tag=enable-mfa,indent=0]
|
||||
|
||||
Next create an `AuthorizationManagerFactory` instance, but do not publish it as a Bean.
|
||||
|
||||
include-code::./SelectiveMfaConfiguration[tag=httpSecurity,indent=0]
|
||||
<1> Create a `DefaultAuthorizationManagerFactory` as we did previously, but do not publish it as a Bean.
|
||||
By not publishing it as a Bean, we are able to selectively use the `AuthorizationManagerFactory` instead of using it for every authorization rule.
|
||||
|
||||
+24
-13
@@ -11,8 +11,6 @@
|
||||
The OAuth2 authorization server `SecurityFilterChain` `@Bean` is configured with the following default protocol endpoints:
|
||||
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-authorization-endpoint[OAuth2 Authorization endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-device-authorization-endpoint[OAuth2 Device Authorization Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-device-verification-endpoint[OAuth2 Device Verification Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-endpoint[OAuth2 Token endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-introspection-endpoint[OAuth2 Token Introspection endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-revocation-endpoint[OAuth2 Token Revocation endpoint]
|
||||
@@ -22,6 +20,15 @@ The OAuth2 authorization server `SecurityFilterChain` `@Bean` is configured with
|
||||
[NOTE]
|
||||
The JWK Set endpoint is configured *only* if a `JWKSource<SecurityContext>` `@Bean` is registered.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The following protocol endpoints are disabled by default:
|
||||
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-device-authorization-endpoint[OAuth2 Device Authorization Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-device-verification-endpoint[OAuth2 Device Verification Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-client-registration-endpoint[OAuth2 Client Registration endpoint]
|
||||
====
|
||||
|
||||
The following example shows how to use `OAuth2AuthorizationServerConfiguration` to apply the minimal default configuration:
|
||||
|
||||
[source,java]
|
||||
@@ -71,7 +78,7 @@ In addition to the default protocol endpoints, the OAuth2 authorization server `
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint]
|
||||
|
||||
[NOTE]
|
||||
The xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint] is disabled by default because many deployments do not require dynamic client registration.
|
||||
The xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint] is disabled by default.
|
||||
|
||||
[TIP]
|
||||
`OAuth2AuthorizationServerConfiguration.jwtDecoder(JWKSource<SecurityContext>)` is a convenience (`static`) utility method that can be used to register a `JwtDecoder` `@Bean`, which is *REQUIRED* for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint] and the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint].
|
||||
@@ -117,12 +124,13 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.tokenEndpoint(tokenEndpoint -> { }) <11>
|
||||
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint -> { }) <12>
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint -> { }) <13>
|
||||
.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint -> { }) <14>
|
||||
.clientRegistrationEndpoint(clientRegistrationEndpoint -> { }) <14>
|
||||
.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint -> { }) <15>
|
||||
.oidc(oidc -> oidc
|
||||
.providerConfigurationEndpoint(providerConfigurationEndpoint -> { }) <15>
|
||||
.logoutEndpoint(logoutEndpoint -> { }) <16>
|
||||
.userInfoEndpoint(userInfoEndpoint -> { }) <17>
|
||||
.clientRegistrationEndpoint(clientRegistrationEndpoint -> { }) <18>
|
||||
.providerConfigurationEndpoint(providerConfigurationEndpoint -> { }) <16>
|
||||
.logoutEndpoint(logoutEndpoint -> { }) <17>
|
||||
.userInfoEndpoint(userInfoEndpoint -> { }) <18>
|
||||
.clientRegistrationEndpoint(clientRegistrationEndpoint -> { }) <19>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -142,11 +150,12 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
<11> `tokenEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-endpoint[OAuth2 Token endpoint].
|
||||
<12> `tokenIntrospectionEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-introspection-endpoint[OAuth2 Token Introspection endpoint].
|
||||
<13> `tokenRevocationEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-revocation-endpoint[OAuth2 Token Revocation endpoint].
|
||||
<14> `authorizationServerMetadataEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-authorization-server-metadata-endpoint[OAuth2 Authorization Server Metadata endpoint].
|
||||
<15> `providerConfigurationEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-provider-configuration-endpoint[OpenID Connect 1.0 Provider Configuration endpoint].
|
||||
<16> `logoutEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-logout-endpoint[OpenID Connect 1.0 Logout endpoint].
|
||||
<17> `userInfoEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint].
|
||||
<18> `clientRegistrationEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint].
|
||||
<14> `clientRegistrationEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-client-registration-endpoint[OAuth2 Client Registration endpoint].
|
||||
<15> `authorizationServerMetadataEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-authorization-server-metadata-endpoint[OAuth2 Authorization Server Metadata endpoint].
|
||||
<16> `providerConfigurationEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-provider-configuration-endpoint[OpenID Connect 1.0 Provider Configuration endpoint].
|
||||
<17> `logoutEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-logout-endpoint[OpenID Connect 1.0 Logout endpoint].
|
||||
<18> `userInfoEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint].
|
||||
<19> `clientRegistrationEndpoint()`: The configurer for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint].
|
||||
|
||||
[[oauth2AuthorizationServer-configuring-authorization-server-settings]]
|
||||
== Configuring Authorization Server Settings
|
||||
@@ -170,6 +179,7 @@ public final class AuthorizationServerSettings extends AbstractSettings {
|
||||
.tokenEndpoint("/oauth2/token")
|
||||
.tokenIntrospectionEndpoint("/oauth2/introspect")
|
||||
.tokenRevocationEndpoint("/oauth2/revoke")
|
||||
.clientRegistrationEndpoint("/oauth2/register")
|
||||
.jwkSetEndpoint("/oauth2/jwks")
|
||||
.oidcLogoutEndpoint("/connect/logout")
|
||||
.oidcUserInfoEndpoint("/userinfo")
|
||||
@@ -202,6 +212,7 @@ public AuthorizationServerSettings authorizationServerSettings() {
|
||||
.tokenEndpoint("/oauth2/v1/token")
|
||||
.tokenIntrospectionEndpoint("/oauth2/v1/introspect")
|
||||
.tokenRevocationEndpoint("/oauth2/v1/revoke")
|
||||
.clientRegistrationEndpoint("/oauth2/v1/register")
|
||||
.jwkSetEndpoint("/oauth2/v1/jwks")
|
||||
.oidcLogoutEndpoint("/connect/v1/logout")
|
||||
.oidcUserInfoEndpoint("/connect/v1/userinfo")
|
||||
|
||||
@@ -86,6 +86,7 @@ Spring Security Authorization Server supports the following features:
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-endpoint[OAuth2 Token Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-introspection-endpoint[OAuth2 Token Introspection Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-token-revocation-endpoint[OAuth2 Token Revocation Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-client-registration-endpoint[OAuth2 Client Registration Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oauth2-authorization-server-metadata-endpoint[OAuth2 Authorization Server Metadata Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-jwk-set-endpoint[JWK Set Endpoint]
|
||||
* xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc#oauth2AuthorizationServer-oidc-provider-configuration-endpoint[OpenID Connect 1.0 Provider Configuration Endpoint]
|
||||
@@ -103,6 +104,7 @@ Spring Security Authorization Server supports the following features:
|
||||
** https://tools.ietf.org/html/rfc8628#section-3.3[Device Verification Endpoint]
|
||||
* OAuth 2.0 Token Introspection (https://tools.ietf.org/html/rfc7662[RFC 7662])
|
||||
* OAuth 2.0 Token Revocation (https://tools.ietf.org/html/rfc7009[RFC 7009])
|
||||
* OAuth 2.0 Dynamic Client Registration Protocol (https://datatracker.ietf.org/doc/html/rfc7591[RFC 7591])
|
||||
* OAuth 2.0 Authorization Server Metadata (https://tools.ietf.org/html/rfc8414[RFC 8414])
|
||||
* JSON Web Key (JWK) (https://tools.ietf.org/html/rfc7517[RFC 7517])
|
||||
* OpenID Connect Discovery 1.0 (https://openid.net/specs/openid-connect-discovery-1_0.html[spec])
|
||||
|
||||
+67
-1
@@ -271,6 +271,9 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[OAuth2Error response].
|
||||
<7> `verificationUri()`: The `URI` of the custom end-user verification page to direct resource owners to on a secondary device.
|
||||
|
||||
[NOTE]
|
||||
The OAuth2 Device Authorization endpoint is disabled by default.
|
||||
|
||||
`OAuth2DeviceAuthorizationEndpointConfigurer` configures the `OAuth2DeviceAuthorizationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2DeviceAuthorizationEndpointFilter` is the `Filter` that processes OAuth2 device authorization requests.
|
||||
|
||||
@@ -319,6 +322,9 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the error response.
|
||||
<7> `consentPage()`: The `URI` of the custom consent page to redirect resource owners to if consent is required during the device verification request flow.
|
||||
|
||||
[NOTE]
|
||||
The OAuth2 Device Verification endpoint is disabled by default.
|
||||
|
||||
`OAuth2DeviceVerificationEndpointConfigurer` configures the `OAuth2DeviceVerificationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2DeviceVerificationEndpointFilter` is the `Filter` that processes OAuth2 device verification requests (and consents).
|
||||
|
||||
@@ -664,6 +670,66 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2TokenRevocationAuthenticationToken` and returns the OAuth2 revocation response.
|
||||
* `*AuthenticationFailureHandler*` -- An `OAuth2ErrorAuthenticationFailureHandler`.
|
||||
|
||||
[[oauth2AuthorizationServer-oauth2-client-registration-endpoint]]
|
||||
== OAuth2 Client Registration Endpoint
|
||||
|
||||
`OAuth2ClientRegistrationEndpointConfigurer` provides the ability to customize the https://datatracker.ietf.org/doc/html/rfc7591#section-3[OAuth2 Client Registration endpoint].
|
||||
It defines extension points that let you customize the pre-processing, main processing, and post-processing logic for https://datatracker.ietf.org/doc/html/rfc7591#section-3.1[Client Registration requests].
|
||||
|
||||
`OAuth2ClientRegistrationEndpointConfigurer` provides the following configuration options:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.oauth2AuthorizationServer((authorizationServer) ->
|
||||
authorizationServer
|
||||
.clientRegistrationEndpoint(clientRegistrationEndpoint ->
|
||||
clientRegistrationEndpoint
|
||||
.clientRegistrationRequestConverter(clientRegistrationRequestConverter) <1>
|
||||
.clientRegistrationRequestConverters(clientRegistrationRequestConvertersConsumer) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.clientRegistrationResponseHandler(clientRegistrationResponseHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `clientRegistrationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract a https://datatracker.ietf.org/doc/html/rfc7591#section-3.1[Client Registration request] from `HttpServletRequest` to an instance of `OAuth2ClientRegistrationAuthenticationToken`.
|
||||
<2> `clientRegistrationRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`.
|
||||
<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2ClientRegistrationAuthenticationToken`.
|
||||
<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`.
|
||||
<5> `clientRegistrationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2ClientRegistrationAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1[Client Registration response].
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2[Client Registration Error response].
|
||||
|
||||
[NOTE]
|
||||
The OAuth2 Client Registration endpoint is disabled by default.
|
||||
|
||||
`OAuth2ClientRegistrationEndpointConfigurer` configures the `OAuth2ClientRegistrationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2ClientRegistrationEndpointFilter` is the `Filter` that processes https://datatracker.ietf.org/doc/html/rfc7591#section-3.1[Client Registration requests] and returns the https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1[OAuth2ClientRegistration response].
|
||||
|
||||
`OAuth2ClientRegistrationEndpointFilter` is configured with the following defaults:
|
||||
|
||||
* `*AuthenticationConverter*` -- An `OAuth2ClientRegistrationAuthenticationConverter`.
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2ClientRegistrationAuthenticationProvider`.
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2ClientRegistrationAuthenticationToken` and returns the `OAuth2ClientRegistration` response.
|
||||
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response.
|
||||
|
||||
The OAuth2 Client Registration endpoint is an https://datatracker.ietf.org/doc/html/rfc7591#section-3[OAuth2 protected resource], which *REQUIRES* an access token to be sent as a bearer token in the Client Registration request.
|
||||
|
||||
[NOTE]
|
||||
OAuth2 resource server support is autoconfigured, however, a `JwtDecoder` `@Bean` is *REQUIRED* for the OAuth2 Client Registration endpoint.
|
||||
|
||||
[IMPORTANT]
|
||||
The access token in a Client Registration request *REQUIRES* the OAuth2 scope `client.create`.
|
||||
|
||||
[TIP]
|
||||
To allow open client registration (no access token in request), configure `OAuth2ClientRegistrationAuthenticationProvider.setOpenRegistrationAllowed(true)`.
|
||||
|
||||
[[oauth2AuthorizationServer-oauth2-authorization-server-metadata-endpoint]]
|
||||
== OAuth2 Authorization Server Metadata Endpoint
|
||||
|
||||
@@ -950,7 +1016,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError[Client Registration Error response] or https://openid.net/specs/openid-connect-registration-1_0.html#ReadError[Client Read Error response].
|
||||
|
||||
[NOTE]
|
||||
The OpenID Connect 1.0 Client Registration endpoint is disabled by default because many deployments do not require dynamic client registration.
|
||||
The OpenID Connect 1.0 Client Registration endpoint is disabled by default.
|
||||
|
||||
`OidcClientRegistrationEndpointConfigurer` configures the `OidcClientRegistrationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OidcClientRegistrationEndpointFilter` is the `Filter` that processes https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[Client Registration requests] and returns the https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse[OidcClientRegistration response].
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package org.springframework.security.docs.features.authentication.authenticationcompromisedpasswordcheck;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordException;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.password.HaveIBeenPwnedRestApiPasswordChecker;
|
||||
|
||||
public class CompromisedPasswordCheckerUsage {
|
||||
// tag::configuration[]
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin((login) -> login
|
||||
.failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompromisedPasswordChecker compromisedPasswordChecker() {
|
||||
return new HaveIBeenPwnedRestApiPasswordChecker();
|
||||
}
|
||||
|
||||
static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
|
||||
"/login?error");
|
||||
|
||||
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) throws IOException, ServletException {
|
||||
if (exception instanceof CompromisedPasswordException) {
|
||||
this.redirectStrategy.sendRedirect(request, response, "/reset-password");
|
||||
return;
|
||||
}
|
||||
this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
|
||||
}
|
||||
|
||||
}
|
||||
// end::configuration[]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package org.springframework.security.docs.features.authentication.authenticationpasswordstorageargon2;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
|
||||
public class Argon2PasswordEncoderUsage {
|
||||
public void testArgon2PasswordEncoder() {
|
||||
// tag::argon2PasswordEncoder[]
|
||||
// Create an encoder with all the defaults
|
||||
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
// end::argon2PasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package org.springframework.security.docs.features.authentication.authenticationpasswordstoragebcrypt;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
public class BCryptPasswordEncoderUsage {
|
||||
public void testBCryptPasswordEncoder() {
|
||||
// tag::bcryptPasswordEncoder[]
|
||||
// Create an encoder with strength 16
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
// end::bcryptPasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package org.springframework.security.docs.features.authentication.authenticationpasswordstoragedepgettingstarted;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import static org.springframework.security.core.userdetails.User.UserBuilder;
|
||||
|
||||
public class WithDefaultPasswordEncoderUsage {
|
||||
public UserDetails createSingleUser() {
|
||||
// tag::createSingleUser[]
|
||||
UserDetails user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("user")
|
||||
.build();
|
||||
System.out.println(user.getPassword());
|
||||
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
|
||||
// end::createSingleUser[]
|
||||
return user;
|
||||
}
|
||||
|
||||
public List<UserDetails> createMultipleUsers() {
|
||||
// tag::createMultipleUsers[]
|
||||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||||
UserDetails user = users
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = users
|
||||
.username("admin")
|
||||
.password("password")
|
||||
.roles("USER","ADMIN")
|
||||
.build();
|
||||
// end::createMultipleUsers[]
|
||||
return List.of(user, admin);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package org.springframework.security.docs.features.authentication.authenticationpasswordstoragedpe;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
|
||||
import org.springframework.security.crypto.password.StandardPasswordEncoder;
|
||||
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
||||
|
||||
public class DelegatingPasswordEncoderUsage {
|
||||
PasswordEncoder defaultDelegatingPasswordEncoder() {
|
||||
// tag::createDefaultPasswordEncoder[]
|
||||
PasswordEncoder passwordEncoder =
|
||||
PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
// end::createDefaultPasswordEncoder[]
|
||||
return passwordEncoder;
|
||||
}
|
||||
|
||||
PasswordEncoder customDelegatingPasswordEncoder() {
|
||||
// tag::createCustomPasswordEncoder[]
|
||||
String idForEncode = "bcrypt";
|
||||
Map encoders = new HashMap<>();
|
||||
encoders.put(idForEncode, new BCryptPasswordEncoder());
|
||||
encoders.put("noop", NoOpPasswordEncoder.getInstance());
|
||||
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
|
||||
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
|
||||
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
|
||||
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
encoders.put("sha256", new StandardPasswordEncoder());
|
||||
|
||||
PasswordEncoder passwordEncoder =
|
||||
new DelegatingPasswordEncoder(idForEncode, encoders);
|
||||
// end::createCustomPasswordEncoder[]
|
||||
return passwordEncoder;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package org.springframework.security.docs.features.authentication.authenticationpasswordstoragepbkdf2;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
|
||||
|
||||
public class Pbkdf2PasswordEncoderUsage {
|
||||
void testPbkdf2PasswordEncoder() {
|
||||
// tag::pbkdf2PasswordEncoder[]
|
||||
// Create an encoder with all the defaults
|
||||
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
// end::pbkdf2PasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package org.springframework.security.docs.features.authentication.authenticationpasswordstoragescrypt;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
||||
|
||||
public class SCryptPasswordEncoderUsage {
|
||||
void testSCryptPasswordEncoder() {
|
||||
// tag::sCryptPasswordEncoder[]
|
||||
// Create an encoder with all the defaults
|
||||
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
// end::sCryptPasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,9 +1,9 @@
|
||||
package org.springframework.security.docs.servlet.authentication.egmfa;
|
||||
package org.springframework.security.docs.servlet.authentication.emfa;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication;
|
||||
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
@@ -16,12 +16,12 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
// tag::enable-global-mfa[]
|
||||
@EnableGlobalMultiFactorAuthentication(authorities = {
|
||||
// tag::enable-mfa[]
|
||||
@EnableMultiFactorAuthentication(authorities = {
|
||||
FactorGrantedAuthority.PASSWORD_AUTHORITY,
|
||||
FactorGrantedAuthority.OTT_AUTHORITY })
|
||||
// end::enable-global-mfa[]
|
||||
public class EnableGlobalMultiFactorAuthenticationConfiguration {
|
||||
// end::enable-mfa[]
|
||||
public class EnableMultiFactorAuthenticationConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
+7
-7
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.egmfa;
|
||||
package org.springframework.security.docs.servlet.authentication.emfa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -44,7 +44,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
public class EnableMultiFactorAuthenticationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@@ -54,7 +54,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
@Test
|
||||
@WithMockUser(authorities = { FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY, "ROLE_USER" })
|
||||
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -65,7 +65,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
@Test
|
||||
@WithMockUser(authorities = FactorGrantedAuthority.PASSWORD_AUTHORITY)
|
||||
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
@@ -76,7 +76,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
@Test
|
||||
@WithMockUser(authorities = FactorGrantedAuthority.OTT_AUTHORITY)
|
||||
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
@@ -87,7 +87,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
@@ -97,7 +97,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
|
||||
@Test
|
||||
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
+4
@@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactories;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
@@ -16,6 +17,9 @@ import org.springframework.security.web.authentication.ott.OneTimeTokenGeneratio
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
// tag::enable-mfa[]
|
||||
@EnableMultiFactorAuthentication(authorities = {})
|
||||
// end::enable-mfa[]
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class SelectiveMfaConfiguration {
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package org.springframework.security.kt.docs.features.authentication.authenticationcompromisedpasswordcheck
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordChecker
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordException
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.DefaultRedirectStrategy
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
|
||||
import org.springframework.security.web.authentication.password.HaveIBeenPwnedRestApiPasswordChecker
|
||||
|
||||
|
||||
class CompromisedPasswordCheckerUsage {
|
||||
// tag::configuration[]
|
||||
@Bean
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin {
|
||||
authenticationFailureHandler = CompromisedPasswordAuthenticationFailureHandler()
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun compromisedPasswordChecker(): CompromisedPasswordChecker {
|
||||
return HaveIBeenPwnedRestApiPasswordChecker()
|
||||
}
|
||||
|
||||
class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler {
|
||||
private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error")
|
||||
private val redirectStrategy = DefaultRedirectStrategy()
|
||||
|
||||
override fun onAuthenticationFailure(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
exception: AuthenticationException
|
||||
) {
|
||||
if (exception is CompromisedPasswordException) {
|
||||
redirectStrategy.sendRedirect(request, response, "/reset-password")
|
||||
return
|
||||
}
|
||||
defaultFailureHandler.onAuthenticationFailure(request, response, exception)
|
||||
}
|
||||
}
|
||||
// end::configuration[]
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package org.springframework.security.kt.docs.features.authentication.authenticationpasswordstorageargon2
|
||||
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder
|
||||
|
||||
class Argon2PasswordEncoderUsage {
|
||||
fun testArgon2PasswordEncoder() {
|
||||
// tag::argon2PasswordEncoder[]
|
||||
// Create an encoder with all the defaults
|
||||
val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String? = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
// end::argon2PasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package org.springframework.security.kt.docs.features.authentication.authenticationpasswordstoragebcrypt
|
||||
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
|
||||
class BCryptPasswordEncoderUsage {
|
||||
fun testBCryptPasswordEncoder() {
|
||||
// tag::bcryptPasswordEncoder[]
|
||||
// Create an encoder with strength 16
|
||||
val encoder = BCryptPasswordEncoder(16)
|
||||
val result: String? = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
// end::bcryptPasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package org.springframework.security.kt.docs.features.authentication.authenticationpasswordstoragedepgettingstarted
|
||||
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
|
||||
class WithDefaultPasswordEncoderUsage {
|
||||
fun createSingleUser(): UserDetails {
|
||||
// tag::createSingleUser[]
|
||||
val user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("user")
|
||||
.build()
|
||||
println(user.password)
|
||||
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
|
||||
// end::createSingleUser[]
|
||||
return user
|
||||
}
|
||||
|
||||
fun createMultipleUsers(): List<UserDetails> {
|
||||
// tag::createMultipleUsers[]
|
||||
val users = User.withDefaultPasswordEncoder()
|
||||
val user = users
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
val admin = users
|
||||
.username("admin")
|
||||
.password("password")
|
||||
.roles("USER", "ADMIN")
|
||||
.build()
|
||||
// end::createMultipleUsers[]
|
||||
return listOf(user, admin)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package org.springframework.security.kt.docs.features.authentication.authenticationpasswordstoragedpe
|
||||
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories
|
||||
import org.springframework.security.crypto.password.DelegatingPasswordEncoder
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder
|
||||
import org.springframework.security.crypto.password.StandardPasswordEncoder
|
||||
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder
|
||||
|
||||
class DelegatingPasswordEncoderUsage {
|
||||
fun defaultDelegatingPasswordEncoder(): PasswordEncoder {
|
||||
// tag::createDefaultPasswordEncoder[]
|
||||
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
|
||||
// end::createDefaultPasswordEncoder[]
|
||||
return passwordEncoder
|
||||
}
|
||||
|
||||
fun customDelegatingPasswordEncoder(): PasswordEncoder {
|
||||
// tag::createCustomPasswordEncoder[]
|
||||
val idForEncode = "bcrypt"
|
||||
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
|
||||
encoders[idForEncode] = BCryptPasswordEncoder()
|
||||
encoders["noop"] = NoOpPasswordEncoder.getInstance()
|
||||
encoders["pbkdf2"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()
|
||||
encoders["pbkdf2@SpringSecurity_v5_8"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
encoders["scrypt"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()
|
||||
encoders["scrypt@SpringSecurity_v5_8"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
encoders["argon2"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()
|
||||
encoders["argon2@SpringSecurity_v5_8"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
encoders["sha256"] = StandardPasswordEncoder()
|
||||
|
||||
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
|
||||
// end::createCustomPasswordEncoder[]
|
||||
return passwordEncoder
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package org.springframework.security.kt.docs.features.authentication.authenticationpasswordstoragepbkdf2
|
||||
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder
|
||||
|
||||
class Pbkdf2PasswordEncoderUsage {
|
||||
fun testPbkdf2PasswordEncoder() {
|
||||
// tag::pbkdf2PasswordEncoder[]
|
||||
// Create an encoder with all the defaults
|
||||
val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String? = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
// end::pbkdf2PasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package org.springframework.security.kt.docs.features.authentication.authenticationpasswordstoragescrypt
|
||||
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder
|
||||
|
||||
class SCryptPasswordEncoderUsage {
|
||||
fun testSCryptPasswordEncoder() {
|
||||
// tag::sCryptPasswordEncoder[]
|
||||
// Create an encoder with all the defaults
|
||||
val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String? = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
// end::sCryptPasswordEncoder[]
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.egmfa
|
||||
package org.springframework.security.kt.docs.servlet.authentication.emfa
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication
|
||||
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
@@ -17,12 +17,12 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
// tag::enable-global-mfa[]
|
||||
@EnableGlobalMultiFactorAuthentication( authorities = [
|
||||
// tag::enable-mfa[]
|
||||
@EnableMultiFactorAuthentication( authorities = [
|
||||
FactorGrantedAuthority.PASSWORD_AUTHORITY,
|
||||
FactorGrantedAuthority.OTT_AUTHORITY])
|
||||
// end::enable-global-mfa[]
|
||||
internal class EnableGlobalMultiFactorAuthenticationConfiguration {
|
||||
// end::enable-mfa[]
|
||||
internal class EnableMultiFactorAuthenticationConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
+7
-7
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.egmfa
|
||||
package org.springframework.security.kt.docs.servlet.authentication.emfa
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
@@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.RestController
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class EnableGlobalMultiFactorAuthenticationConfigurationTests {
|
||||
class EnableMultiFactorAuthenticationConfigurationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@@ -50,7 +50,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
|
||||
@WithMockUser(authorities = [FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY, "ROLE_ADMIN"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
@@ -62,7 +62,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
|
||||
@WithMockUser(authorities = [FactorGrantedAuthority.PASSWORD_AUTHORITY])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
@@ -74,7 +74,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
|
||||
@WithMockUser(authorities = [FactorGrantedAuthority.OTT_AUTHORITY])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
@@ -86,7 +86,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedThenRedirectsToPassword() {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
@@ -97,7 +97,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenUnauthenticatedThenRedirectsToBoth() {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
+4
@@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactories
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory
|
||||
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
@@ -15,6 +16,9 @@ import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
|
||||
// tag::enable-mfa[]
|
||||
@EnableMultiFactorAuthentication(authorities = [])
|
||||
// end::enable-mfa[]
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class SelectiveMfaConfiguration {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=4.0.0-SNAPSHOT
|
||||
version=7.0.0-RC1
|
||||
version=7.0.0-RC2
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -85,7 +85,7 @@ org-springframework-data-spring-data-bom = "org.springframework.data:spring-data
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.0-RC1"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.0"
|
||||
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.1"
|
||||
|
||||
com-google-code-gson-gson = "com.google.code.gson:gson:2.13.2"
|
||||
com-thaiopensource-trag = "com.thaiopensource:trang:20091111"
|
||||
|
||||
+1
-1
@@ -13,9 +13,9 @@ dependencies {
|
||||
api "com.nimbusds:nimbus-jose-jwt"
|
||||
api 'tools.jackson.core:jackson-databind'
|
||||
|
||||
optional "com.fasterxml.jackson.core:jackson-databind"
|
||||
optional "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
|
||||
optional "org.springframework:spring-jdbc"
|
||||
optional "com.fasterxml.jackson.core:jackson-databind"
|
||||
|
||||
testImplementation project(":spring-security-test")
|
||||
testImplementation project(path : ':spring-security-oauth2-jose', configuration : 'tests')
|
||||
|
||||
+3
-7
@@ -68,7 +68,6 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationServerJacksonModule;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -361,7 +360,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
|
||||
/**
|
||||
* Sets the {@link RowMapper} used for mapping the current row in
|
||||
* {@code java.sql.ResultSet} to {@link OAuth2Authorization}. The default is
|
||||
* {@link OAuth2AuthorizationRowMapper}.
|
||||
* {@link JsonMapperOAuth2AuthorizationRowMapper}.
|
||||
* @param authorizationRowMapper the {@link RowMapper} used for mapping the current
|
||||
* row in {@code ResultSet} to {@link OAuth2Authorization}
|
||||
*/
|
||||
@@ -373,7 +372,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
|
||||
/**
|
||||
* Sets the {@code Function} used for mapping {@link OAuth2Authorization} to a
|
||||
* {@code List} of {@link SqlParameterValue}. The default is
|
||||
* {@link OAuth2AuthorizationParametersMapper}.
|
||||
* {@link JsonMapperOAuth2AuthorizationParametersMapper}.
|
||||
* @param authorizationParametersMapper the {@code Function} used for mapping
|
||||
* {@link OAuth2Authorization} to a {@code List} of {@link SqlParameterValue}
|
||||
*/
|
||||
@@ -743,10 +742,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
|
||||
|
||||
static JsonMapper createJsonMapper() {
|
||||
List<JacksonModule> modules = SecurityJacksonModules.getModules(Jackson3.class.getClassLoader());
|
||||
return JsonMapper.builder()
|
||||
.addModules(modules)
|
||||
.addModules(new OAuth2AuthorizationServerJacksonModule())
|
||||
.build();
|
||||
return JsonMapper.builder().addModules(modules).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-16
@@ -18,13 +18,11 @@ package org.springframework.security.oauth2.server.authorization.jackson;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import tools.jackson.core.Version;
|
||||
import tools.jackson.databind.DefaultTyping;
|
||||
import tools.jackson.databind.cfg.MapperBuilder;
|
||||
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
|
||||
|
||||
import org.springframework.security.jackson.CoreJacksonModule;
|
||||
import org.springframework.security.jackson.SecurityJacksonModule;
|
||||
import org.springframework.security.jackson.SecurityJacksonModules;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
@@ -37,20 +35,22 @@ import org.springframework.security.oauth2.server.authorization.settings.OAuth2T
|
||||
* registers the following mix-in annotations:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link OAuth2TokenExchangeActor}</li>
|
||||
* <li>{@link OAuth2TokenExchangeActorMixin}</li>
|
||||
* <li>{@link OAuth2AuthorizationRequestMixin}</li>
|
||||
* <li>{@link OAuth2TokenExchangeCompositeAuthenticationTokenMixin}</li>
|
||||
* <li>{@link JwsAlgorithmMixin}</li>
|
||||
* <li>{@link OAuth2TokenFormatMixin}</li>
|
||||
* </ul>
|
||||
*
|
||||
* If not already enabled, default typing will be automatically enabled as type info is
|
||||
* required to properly serialize/deserialize objects. In order to use this module just
|
||||
* add it to your {@code JsonMapper.Builder} configuration.
|
||||
* <p>
|
||||
* The recommended way to configure it is to use {@link SecurityJacksonModules} in order
|
||||
* to enable properly automatic inclusion of type information with related validation.
|
||||
*
|
||||
* <pre>
|
||||
* ClassLoader loader = getClass().getClassLoader();
|
||||
* JsonMapper mapper = JsonMapper.builder()
|
||||
* .addModules(new OAuth2AuthorizationServerJacksonModule()).build;
|
||||
* .addModules(SecurityJacksonModules.getModules(loader))
|
||||
* .build();
|
||||
* </pre>
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
@@ -58,7 +58,7 @@ import org.springframework.security.oauth2.server.authorization.settings.OAuth2T
|
||||
* @since 7.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class OAuth2AuthorizationServerJacksonModule extends CoreJacksonModule {
|
||||
public class OAuth2AuthorizationServerJacksonModule extends SecurityJacksonModule {
|
||||
|
||||
public OAuth2AuthorizationServerJacksonModule() {
|
||||
super(OAuth2AuthorizationServerJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null));
|
||||
@@ -66,7 +66,6 @@ public class OAuth2AuthorizationServerJacksonModule extends CoreJacksonModule {
|
||||
|
||||
@Override
|
||||
public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) {
|
||||
super.configurePolymorphicTypeValidator(builder);
|
||||
builder.allowIfSubType(OAuth2TokenFormat.class)
|
||||
.allowIfSubType(OAuth2TokenExchangeActor.class)
|
||||
.allowIfSubType(OAuth2TokenExchangeCompositeAuthenticationToken.class)
|
||||
@@ -78,11 +77,6 @@ public class OAuth2AuthorizationServerJacksonModule extends CoreJacksonModule {
|
||||
|
||||
@Override
|
||||
public void setupModule(SetupContext context) {
|
||||
super.setupModule(context);
|
||||
BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder();
|
||||
this.configurePolymorphicTypeValidator(builder);
|
||||
((MapperBuilder<?, ?>) context.getOwner()).activateDefaultTyping(builder.build(), DefaultTyping.NON_FINAL,
|
||||
JsonTypeInfo.As.PROPERTY);
|
||||
context.setMixIn(OAuth2TokenExchangeActor.class, OAuth2TokenExchangeActorMixin.class);
|
||||
context.setMixIn(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
|
||||
context.setMixIn(OAuth2TokenExchangeCompositeAuthenticationToken.class,
|
||||
|
||||
+15
-8
@@ -40,6 +40,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
@@ -151,18 +152,24 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
.matcher(HttpMethod.GET, authorizationEndpointUri);
|
||||
RequestMatcher authorizationRequestPostMatcher = PathPatternRequestMatcher.withDefaults()
|
||||
.matcher(HttpMethod.POST, authorizationEndpointUri);
|
||||
|
||||
RequestMatcher responseTypeParameterMatcher = (
|
||||
request) -> request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null;
|
||||
|
||||
RequestMatcher authorizationConsentMatcher = createAuthorizationConsentMatcher(authorizationEndpointUri);
|
||||
RequestMatcher authorizationRequestMatcher = new OrRequestMatcher(authorizationRequestGetMatcher,
|
||||
new AndRequestMatcher(authorizationRequestPostMatcher, responseTypeParameterMatcher));
|
||||
RequestMatcher authorizationConsentMatcher = new AndRequestMatcher(authorizationRequestPostMatcher,
|
||||
new NegatedRequestMatcher(responseTypeParameterMatcher));
|
||||
|
||||
new AndRequestMatcher(authorizationRequestPostMatcher,
|
||||
new NegatedRequestMatcher(authorizationConsentMatcher)));
|
||||
return new OrRequestMatcher(authorizationRequestMatcher, authorizationConsentMatcher);
|
||||
}
|
||||
|
||||
private static RequestMatcher createAuthorizationConsentMatcher(String authorizationEndpointUri) {
|
||||
final RequestMatcher authorizationConsentPostMatcher = PathPatternRequestMatcher.withDefaults()
|
||||
.matcher(HttpMethod.POST, authorizationEndpointUri);
|
||||
return (request) -> authorizationConsentPostMatcher.matches(request)
|
||||
&& request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) == null
|
||||
&& request.getParameter(OAuth2ParameterNames.REQUEST_URI) == null
|
||||
&& request.getParameter(OAuth2ParameterNames.REDIRECT_URI) == null
|
||||
&& request.getParameter(PkceParameterNames.CODE_CHALLENGE) == null
|
||||
&& request.getParameter(PkceParameterNames.CODE_CHALLENGE_METHOD) == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
+4
-8
@@ -44,8 +44,6 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -198,12 +196,10 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
}
|
||||
|
||||
private static RequestMatcher createDefaultRequestMatcher() {
|
||||
RequestMatcher getMethodMatcher = (request) -> "GET".equals(request.getMethod());
|
||||
RequestMatcher postMethodMatcher = (request) -> "POST".equals(request.getMethod());
|
||||
RequestMatcher responseTypeParameterMatcher = (
|
||||
request) -> request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null;
|
||||
return new OrRequestMatcher(getMethodMatcher,
|
||||
new AndRequestMatcher(postMethodMatcher, responseTypeParameterMatcher));
|
||||
final RequestMatcher authorizationConsentMatcher = OAuth2AuthorizationConsentAuthenticationConverter
|
||||
.createDefaultRequestMatcher();
|
||||
return (request) -> "GET".equals(request.getMethod())
|
||||
|| ("POST".equals(request.getMethod()) && !authorizationConsentMatcher.matches(request));
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
|
||||
+8
-7
@@ -30,12 +30,11 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -106,11 +105,13 @@ public final class OAuth2AuthorizationConsentAuthenticationConverter implements
|
||||
additionalParameters);
|
||||
}
|
||||
|
||||
private static RequestMatcher createDefaultRequestMatcher() {
|
||||
RequestMatcher postMethodMatcher = (request) -> "POST".equals(request.getMethod());
|
||||
RequestMatcher responseTypeParameterMatcher = (
|
||||
request) -> request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null;
|
||||
return new AndRequestMatcher(postMethodMatcher, new NegatedRequestMatcher(responseTypeParameterMatcher));
|
||||
static RequestMatcher createDefaultRequestMatcher() {
|
||||
return (request) -> "POST".equals(request.getMethod())
|
||||
&& request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) == null
|
||||
&& request.getParameter(OAuth2ParameterNames.REQUEST_URI) == null
|
||||
&& request.getParameter(OAuth2ParameterNames.REDIRECT_URI) == null
|
||||
&& request.getParameter(PkceParameterNames.CODE_CHALLENGE) == null
|
||||
&& request.getParameter(PkceParameterNames.CODE_CHALLENGE_METHOD) == null;
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
|
||||
+28
-2
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
@@ -45,6 +46,7 @@ import org.springframework.jdbc.core.SqlParameterValue;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.jackson.SecurityJacksonModules;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
@@ -58,6 +60,7 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -227,10 +230,11 @@ public class JdbcOAuth2AuthorizationServiceTests {
|
||||
.build();
|
||||
|
||||
RowMapper<OAuth2Authorization> authorizationRowMapper = spy(
|
||||
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(this.registeredClientRepository));
|
||||
new JdbcOAuth2AuthorizationService.JsonMapperOAuth2AuthorizationRowMapper(
|
||||
this.registeredClientRepository));
|
||||
this.authorizationService.setAuthorizationRowMapper(authorizationRowMapper);
|
||||
Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper = spy(
|
||||
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper());
|
||||
new JdbcOAuth2AuthorizationService.JsonMapperOAuth2AuthorizationParametersMapper());
|
||||
this.authorizationService.setAuthorizationParametersMapper(authorizationParametersMapper);
|
||||
|
||||
this.authorizationService.save(originalAuthorization);
|
||||
@@ -461,6 +465,28 @@ public class JdbcOAuth2AuthorizationServiceTests {
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
// gh-18102
|
||||
@Test
|
||||
public void findByTokenWhenPrincipalHasWebAuthenticationDetailsThenDeserializes() {
|
||||
given(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId()))).willReturn(REGISTERED_CLIENT);
|
||||
|
||||
String state = "state";
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken(PRINCIPAL_NAME, "credentials");
|
||||
principal.setDetails(new WebAuthenticationDetails("remoteAddress", "sessionId"));
|
||||
|
||||
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
|
||||
.id(ID)
|
||||
.principalName(PRINCIPAL_NAME)
|
||||
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||
.attribute(OAuth2ParameterNames.STATE, state)
|
||||
.attribute(Principal.class.getName(), principal)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OAuth2Authorization result = this.authorizationService.findByToken(state, STATE_TOKEN_TYPE);
|
||||
assertThat(authorization).isEqualTo(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tableDefinitionWhenCustomThenAbleToOverride() {
|
||||
given(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId()))).willReturn(REGISTERED_CLIENT);
|
||||
|
||||
+5
-3
@@ -27,20 +27,20 @@ import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.jackson.SecurityJacksonModules;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationServerJackson2Module}.
|
||||
* Tests for {@link OAuth2AuthorizationServerJacksonModule}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @author Joe Grandja
|
||||
@@ -55,7 +55,9 @@ public class OAuth2AuthorizationServerJacksonModuleTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.mapper = JsonMapper.builder().addModules(new OAuth2AuthorizationServerJacksonModule()).build();
|
||||
this.mapper = JsonMapper.builder()
|
||||
.addModules(SecurityJacksonModules.getModules(getClass().getClassLoader()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+13
-2
@@ -207,6 +207,17 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
});
|
||||
}
|
||||
|
||||
// gh-2226
|
||||
@Test
|
||||
public void doFilterWhenPostAuthorizationRequestMissingResponseTypeThenInvalidRequestError() throws Exception {
|
||||
doFilterWhenAuthorizationRequestInvalidParameterThenError(TestRegisteredClients.registeredClient().build(),
|
||||
OAuth2ParameterNames.RESPONSE_TYPE, OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
|
||||
request.setMethod("POST");
|
||||
request.removeParameter(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
request.setQueryString(null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestMultipleResponseTypeThenInvalidRequestError() throws Exception {
|
||||
doFilterWhenAuthorizationRequestInvalidParameterThenError(TestRegisteredClients.registeredClient().build(),
|
||||
@@ -646,7 +657,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(this.authenticationManager).authenticate(any());
|
||||
verify(this.authenticationManager).authenticate(any(OAuth2AuthorizationCodeRequestAuthenticationToken.class));
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
@@ -675,7 +686,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(this.authenticationManager).authenticate(any());
|
||||
verify(this.authenticationManager).authenticate(any(OAuth2AuthorizationCodeRequestAuthenticationToken.class));
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
|
||||
+4
-2
@@ -72,8 +72,10 @@ public final class JwtTypeValidator implements OAuth2TokenValidator<Jwt> {
|
||||
if (this.allowEmpty && !StringUtils.hasText(typ)) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
if (this.validTypes.contains(typ)) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
for (String validType : this.validTypes) {
|
||||
if (validType.equalsIgnoreCase(typ)) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
}
|
||||
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
|
||||
"the given typ value needs to be one of " + this.validTypes,
|
||||
|
||||
+8
@@ -44,4 +44,12 @@ class JwtTypeValidatorTests {
|
||||
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateWhenTypHeaderHasDifferentCaseThenSuccess() {
|
||||
Jwt.Builder jwt = TestJwts.jwt();
|
||||
JwtTypeValidator validator = new JwtTypeValidator("at+jwt");
|
||||
jwt.header(JoseHeaderNames.TYP, "AT+JWT");
|
||||
assertThat(validator.validate(jwt.build()).hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -281,7 +281,8 @@ public final class SecurityMockMvcResultMatchers {
|
||||
for (String role : roles) {
|
||||
withPrefix.add(new SimpleGrantedAuthority(rolePrefix + role));
|
||||
}
|
||||
this.ignoreAuthorities = (authority) -> !authority.getAuthority().startsWith(rolePrefix);
|
||||
this.ignoreAuthorities = (authority) -> (authority.getAuthority() != null
|
||||
&& !authority.getAuthority().startsWith(rolePrefix));
|
||||
return withAuthorities(withPrefix);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -162,7 +162,8 @@ public final class DelegatingMissingAuthorityAccessDeniedHandler implements Acce
|
||||
if (authorizationResult instanceof AuthorityAuthorizationDecision authorityDecision) {
|
||||
// @formatter:off
|
||||
return authorityDecision.getAuthorities().stream()
|
||||
.map((grantedAuthority) -> {
|
||||
.filter((ga) -> ga.getAuthority() != null)
|
||||
.map((grantedAuthority) -> {
|
||||
String authority = grantedAuthority.getAuthority();
|
||||
if (authority.startsWith("FACTOR_")) {
|
||||
RequiredFactor required = RequiredFactor.withAuthority(authority).build();
|
||||
|
||||
+27
-1
@@ -35,6 +35,7 @@ import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.MessageSourceAware;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
@@ -157,6 +158,8 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
|
||||
|
||||
private boolean mfaEnabled;
|
||||
|
||||
/**
|
||||
* @param defaultFilterProcessesUrl the default value for <tt>filterProcessesUrl</tt>.
|
||||
*/
|
||||
@@ -253,7 +256,7 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
|
||||
return;
|
||||
}
|
||||
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
|
||||
if (current != null && current.isAuthenticated() && declaresToBuilder(authenticationResult)) {
|
||||
if (shouldPerformMfa(current, authenticationResult)) {
|
||||
authenticationResult = authenticationResult.toBuilder()
|
||||
// @formatter:off
|
||||
.authorities((a) -> {
|
||||
@@ -286,6 +289,20 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
|
||||
}
|
||||
}
|
||||
|
||||
@Contract("null, _ -> false")
|
||||
private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
|
||||
if (!this.mfaEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (current == null || !current.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
if (!declaresToBuilder(authenticationResult)) {
|
||||
return false;
|
||||
}
|
||||
return current.getName().equals(authenticationResult.getName());
|
||||
}
|
||||
|
||||
private static boolean declaresToBuilder(Authentication authentication) {
|
||||
for (Method method : authentication.getClass().getDeclaredMethods()) {
|
||||
if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
|
||||
@@ -479,6 +496,15 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
|
||||
this.allowSessionCreation = allowSessionCreation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables Multi-Factor Authentication (MFA) support.
|
||||
* @param mfaEnabled true to enable MFA support, false to disable it. Default is
|
||||
* false.
|
||||
*/
|
||||
public void setMfaEnabled(boolean mfaEnabled) {
|
||||
this.mfaEnabled = mfaEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* The session handling strategy which will be invoked immediately after an
|
||||
* authentication request is successfully processed by the
|
||||
|
||||
+27
-1
@@ -30,6 +30,7 @@ import jakarta.servlet.http.HttpSession;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -90,6 +91,8 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
|
||||
|
||||
private boolean mfaEnabled;
|
||||
|
||||
public AuthenticationFilter(AuthenticationManager authenticationManager,
|
||||
AuthenticationConverter authenticationConverter) {
|
||||
this((AuthenticationManagerResolver<HttpServletRequest>) (r) -> authenticationManager, authenticationConverter);
|
||||
@@ -116,6 +119,15 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
return this.authenticationConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables Multi-Factor Authentication (MFA) support.
|
||||
* @param mfaEnabled true to enable MFA support, false to disable it. Default is
|
||||
* false.
|
||||
*/
|
||||
public void setMfaEnabled(boolean mfaEnabled) {
|
||||
this.mfaEnabled = mfaEnabled;
|
||||
}
|
||||
|
||||
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
@@ -189,7 +201,7 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
|
||||
if (current != null && current.isAuthenticated() && declaresToBuilder(authenticationResult)) {
|
||||
if (shouldPerformMfa(current, authenticationResult)) {
|
||||
authenticationResult = authenticationResult.toBuilder()
|
||||
// @formatter:off
|
||||
.authorities((a) -> {
|
||||
@@ -216,6 +228,20 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
|
||||
@Contract("null, _ -> false")
|
||||
private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
|
||||
if (!this.mfaEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (current == null || !current.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
if (!declaresToBuilder(authenticationResult)) {
|
||||
return false;
|
||||
}
|
||||
return current.getName().equals(authenticationResult.getName());
|
||||
}
|
||||
|
||||
private static boolean declaresToBuilder(Authentication authentication) {
|
||||
for (Method method : authentication.getClass().getDeclaredMethods()) {
|
||||
if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
|
||||
|
||||
-6
@@ -30,7 +30,6 @@ import org.springframework.security.authentication.ott.OneTimeTokenService;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
@@ -68,11 +67,6 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
String username = request.getParameter("username");
|
||||
if (!StringUtils.hasText(username)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
GenerateOneTimeTokenRequest generateRequest = this.requestResolver.resolve(request);
|
||||
if (generateRequest == null) {
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
+27
-1
@@ -33,6 +33,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
|
||||
@@ -122,6 +123,8 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
private boolean mfaEnabled;
|
||||
|
||||
/**
|
||||
* Check whether all required properties have been set.
|
||||
*/
|
||||
@@ -209,7 +212,7 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
|
||||
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
|
||||
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
|
||||
if (current != null && current.isAuthenticated() && declaresToBuilder(authenticationResult)) {
|
||||
if (shouldPerformMfa(current, authenticationResult)) {
|
||||
authenticationResult = authenticationResult.toBuilder()
|
||||
// @formatter:off
|
||||
.authorities((a) -> {
|
||||
@@ -235,6 +238,20 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
|
||||
}
|
||||
}
|
||||
|
||||
@Contract("null, _ -> false")
|
||||
private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
|
||||
if (!this.mfaEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (current == null || !current.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
if (!declaresToBuilder(authenticationResult)) {
|
||||
return false;
|
||||
}
|
||||
return current.getName().equals(authenticationResult.getName());
|
||||
}
|
||||
|
||||
private static boolean declaresToBuilder(Authentication authentication) {
|
||||
for (Method method : authentication.getClass().getDeclaredMethods()) {
|
||||
if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
|
||||
@@ -287,6 +304,15 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
|
||||
this.eventPublisher = anApplicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables Multi-Factor Authentication (MFA) support.
|
||||
* @param mfaEnabled true to enable MFA support, false to disable it. Default is
|
||||
* false.
|
||||
*/
|
||||
public void setMfaEnabled(boolean mfaEnabled) {
|
||||
this.mfaEnabled = mfaEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
|
||||
* authentication success. The default action is to save the {@link SecurityContext}
|
||||
|
||||
+27
-1
@@ -29,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
@@ -115,6 +116,8 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
|
||||
|
||||
private boolean mfaEnabled;
|
||||
|
||||
/**
|
||||
* Creates an instance which will authenticate against the supplied
|
||||
* {@code AuthenticationManager} and which will ignore failed authentication attempts,
|
||||
@@ -155,6 +158,15 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables Multi-Factor Authentication (MFA) support.
|
||||
* @param mfaEnabled true to enable MFA support, false to disable it. Default is
|
||||
* false.
|
||||
*/
|
||||
public void setMfaEnabled(boolean mfaEnabled) {
|
||||
this.mfaEnabled = mfaEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the
|
||||
* {@link org.springframework.security.web.authentication.AuthenticationConverter} to
|
||||
@@ -191,7 +203,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
|
||||
if (authenticationIsRequired(username)) {
|
||||
Authentication authResult = this.authenticationManager.authenticate(authRequest);
|
||||
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
|
||||
if (current != null && current.isAuthenticated() && declaresToBuilder(authResult)) {
|
||||
if (shouldPerformMfa(current, authResult)) {
|
||||
authResult = authResult.toBuilder()
|
||||
// @formatter:off
|
||||
.authorities((a) -> {
|
||||
@@ -235,6 +247,20 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Contract("null, _ -> false")
|
||||
private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
|
||||
if (!this.mfaEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (current == null || !current.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
if (!declaresToBuilder(authenticationResult)) {
|
||||
return false;
|
||||
}
|
||||
return current.getName().equals(authenticationResult.getName());
|
||||
}
|
||||
|
||||
private static boolean declaresToBuilder(Authentication authentication) {
|
||||
for (Method method : authentication.getClass().getDeclaredMethods()) {
|
||||
if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
|
||||
|
||||
-43
@@ -16,10 +16,7 @@
|
||||
|
||||
package org.springframework.security.web.server.authentication;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -30,7 +27,6 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
@@ -126,51 +122,12 @@ public class AuthenticationWebFilter implements WebFilter {
|
||||
.flatMap((authenticationManager) -> authenticationManager.authenticate(token))
|
||||
.switchIfEmpty(Mono
|
||||
.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
|
||||
.flatMap(this::applyCurrentAuthenication)
|
||||
.flatMap(
|
||||
(authentication) -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)))
|
||||
.doOnError(AuthenticationException.class,
|
||||
(ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage()), ex));
|
||||
}
|
||||
|
||||
private Mono<Authentication> applyCurrentAuthenication(Authentication result) {
|
||||
return ReactiveSecurityContextHolder.getContext().map((context) -> {
|
||||
Authentication current = context.getAuthentication();
|
||||
if (current == null) {
|
||||
return result;
|
||||
}
|
||||
if (!current.isAuthenticated()) {
|
||||
return result;
|
||||
}
|
||||
if (!declaresToBuilder(result)) {
|
||||
return result;
|
||||
}
|
||||
return result.toBuilder()
|
||||
// @formatter:off
|
||||
.authorities((a) -> {
|
||||
Set<String> newAuthorities = a.stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
for (GrantedAuthority currentAuthority : current.getAuthorities()) {
|
||||
if (!newAuthorities.contains(currentAuthority.getAuthority())) {
|
||||
a.add(currentAuthority);
|
||||
}
|
||||
}
|
||||
})
|
||||
.build();
|
||||
// @formatter:on
|
||||
}).switchIfEmpty(Mono.just(result));
|
||||
}
|
||||
|
||||
private static boolean declaresToBuilder(Authentication authentication) {
|
||||
for (Method method : authentication.getClass().getDeclaredMethods()) {
|
||||
if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
|
||||
ServerWebExchange exchange = webFilterExchange.getExchange();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl();
|
||||
|
||||
+40
-1
@@ -454,13 +454,50 @@ public class AbstractAuthenticationProcessingFilterTests {
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
MockHttpServletRequest request = createMockAuthenticationRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockAuthenticationFilter filter = new MockAuthenticationFilter(true);
|
||||
Authentication newAuthn = UsernamePasswordAuthenticationToken.authenticated(existingAuthn.getName(), "test",
|
||||
AuthorityUtils.createAuthorityList("TEST"));
|
||||
MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, new MockFilterChain(false));
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDefaultThenMfaDisabled() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
MockHttpServletRequest request = createMockAuthenticationRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Authentication newAuthn = UsernamePasswordAuthenticationToken.authenticated(existingAuthn.getName(), "test",
|
||||
AuthorityUtils.createAuthorityList("TEST"));
|
||||
MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
|
||||
filter.doFilter(request, response, new MockFilterChain(false));
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isEqualTo(newAuthn);
|
||||
}
|
||||
|
||||
// gh-18112
|
||||
@Test
|
||||
void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
MockHttpServletRequest request = createMockAuthenticationRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Authentication newAuthn = UsernamePasswordAuthenticationToken
|
||||
.authenticated(existingAuthn.getName() + "different", "test", AuthorityUtils.createAuthorityList("TEST"));
|
||||
MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, new MockFilterChain(false));
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isEqualTo(newAuthn);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is critical to avoid adding duplicate GrantedAuthority instances with the
|
||||
* same' authority when the issuedAt is too old and a new instance is requested.
|
||||
@@ -475,6 +512,7 @@ public class AbstractAuthenticationProcessingFilterTests {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockAuthenticationFilter filter = new MockAuthenticationFilter(
|
||||
new TestingAuthenticationToken("username", "password", new DefaultEqualsGrantedAuthority()));
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, new MockFilterChain(false));
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(new ArrayList<GrantedAuthority>(authentication.getAuthorities()))
|
||||
@@ -490,6 +528,7 @@ public class AbstractAuthenticationProcessingFilterTests {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockAuthenticationFilter filter = new MockAuthenticationFilter(
|
||||
new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, new MockFilterChain(false));
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
SecurityAssertions.assertThat(authentication)
|
||||
|
||||
+48
-3
@@ -314,7 +314,29 @@ public class AuthenticationFilterTests {
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
given(this.authenticationConverter.convert(any())).willReturn(existingAuthn);
|
||||
given(this.authenticationManager.authenticate(any()))
|
||||
.willReturn(new TestingAuthenticationToken("user", "password", "TEST"));
|
||||
.willReturn(new TestingAuthenticationToken(existingAuthn.getName(), "password", "TEST"));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain chain = new MockFilterChain();
|
||||
AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
|
||||
this.authenticationConverter);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, chain);
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenDefaultThenMfaDisabled() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
given(this.authenticationConverter.convert(any())).willReturn(existingAuthn);
|
||||
TestingAuthenticationToken newAuthn = new TestingAuthenticationToken(existingAuthn.getName(), "password",
|
||||
"TEST");
|
||||
given(this.authenticationManager.authenticate(any())).willReturn(newAuthn);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain chain = new MockFilterChain();
|
||||
@@ -322,8 +344,29 @@ public class AuthenticationFilterTests {
|
||||
this.authenticationConverter);
|
||||
filter.doFilter(request, response, chain);
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
|
||||
assertThat(authentication).isEqualTo(newAuthn);
|
||||
}
|
||||
|
||||
// gh-18112
|
||||
@Test
|
||||
public void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
given(this.authenticationConverter.convert(any())).willReturn(existingAuthn);
|
||||
TestingAuthenticationToken expected = new TestingAuthenticationToken(existingAuthn.getName() + "different",
|
||||
"password", "TEST");
|
||||
given(this.authenticationManager.authenticate(any())).willReturn(expected);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain chain = new MockFilterChain();
|
||||
AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
|
||||
this.authenticationConverter);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, chain);
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,6 +387,7 @@ public class AuthenticationFilterTests {
|
||||
FilterChain chain = new MockFilterChain();
|
||||
AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
|
||||
this.authenticationConverter);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, chain);
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
@@ -362,6 +406,7 @@ public class AuthenticationFilterTests {
|
||||
FilterChain chain = new MockFilterChain();
|
||||
AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
|
||||
this.authenticationConverter);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, chain);
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
SecurityAssertions.assertThat(authentication)
|
||||
|
||||
+18
@@ -113,4 +113,22 @@ public class GenerateOneTimeTokenFilterTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenUsernameFormParamIsEmptyButRequestResolverCanResolveThenSuccess()
|
||||
throws ServletException, IOException {
|
||||
GenerateOneTimeTokenRequestResolver requestResolver = mock();
|
||||
given(this.oneTimeTokenService.generate(ArgumentMatchers.any(GenerateOneTimeTokenRequest.class)))
|
||||
.willReturn((new DefaultOneTimeToken(TOKEN, USERNAME, Instant.now())));
|
||||
given(requestResolver.resolve(this.request)).willReturn(new GenerateOneTimeTokenRequest(USERNAME));
|
||||
|
||||
GenerateOneTimeTokenFilter filter = new GenerateOneTimeTokenFilter(this.oneTimeTokenService,
|
||||
this.successHandler);
|
||||
filter.setRequestResolver(requestResolver);
|
||||
|
||||
filter.doFilter(this.request, this.response, this.filterChain);
|
||||
|
||||
verify(this.oneTimeTokenService).generate(ArgumentMatchers.any(GenerateOneTimeTokenRequest.class));
|
||||
assertThat(this.response.getRedirectedUrl()).isEqualTo("/login/ott");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+36
@@ -406,6 +406,7 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.filter = createFilterAuthenticatesWith(new TestingAuthenticationToken("username", "password", "TEST"));
|
||||
this.filter.setMfaEnabled(true);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
// @formatter:off
|
||||
@@ -415,6 +416,39 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDefaultThenMfaDisabled() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
TestingAuthenticationToken newAuthn = new TestingAuthenticationToken("username", "password", "TEST");
|
||||
this.filter = createFilterAuthenticatesWith(newAuthn);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isEqualTo(newAuthn);
|
||||
}
|
||||
|
||||
// gh-18112
|
||||
@Test
|
||||
void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
TestingAuthenticationToken newAuthn = new TestingAuthenticationToken(existingAuthn.getName() + "different",
|
||||
"password", "TEST");
|
||||
this.filter = createFilterAuthenticatesWith(newAuthn);
|
||||
this.filter.setMfaEnabled(true);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isEqualTo(newAuthn);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is critical to avoid adding duplicate GrantedAuthority instances with the
|
||||
* same' authority when the issuedAt is too old and a new instance is requested.
|
||||
@@ -429,6 +463,7 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.filter = createFilterAuthenticatesWith(
|
||||
new TestingAuthenticationToken("username", "password", new DefaultEqualsGrantedAuthority()));
|
||||
this.filter.setMfaEnabled(true);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
// @formatter:off
|
||||
@@ -446,6 +481,7 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.filter = createFilterAuthenticatesWith(
|
||||
new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
|
||||
this.filter.setMfaEnabled(true);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
SecurityAssertions.assertThat(authentication)
|
||||
|
||||
+40
@@ -511,12 +511,52 @@ public class BasicAuthenticationFilterTests {
|
||||
AuthenticationManager manager = mock(AuthenticationManager.class);
|
||||
given(manager.authenticate(any())).willReturn(new TestingAuthenticationToken("username", "password", "TEST"));
|
||||
BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDefaultThenMfaDisabled() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + CodecTestUtils.encodeBase64("a:b"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
AuthenticationManager manager = mock(AuthenticationManager.class);
|
||||
TestingAuthenticationToken newAuthn = new TestingAuthenticationToken("username", "password", "TEST");
|
||||
given(manager.authenticate(any())).willReturn(newAuthn);
|
||||
BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isEqualTo(newAuthn);
|
||||
}
|
||||
|
||||
// gh-18112
|
||||
@Test
|
||||
void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + CodecTestUtils.encodeBase64("a:b"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
AuthenticationManager manager = mock(AuthenticationManager.class);
|
||||
TestingAuthenticationToken newAuthn = new TestingAuthenticationToken(existingAuthn.getName() + "different",
|
||||
"password", "TEST");
|
||||
given(manager.authenticate(any())).willReturn(newAuthn);
|
||||
BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
|
||||
filter.setMfaEnabled(true);
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertThat(authentication).isEqualTo(newAuthn);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is critical to avoid adding duplicate GrantedAuthority instances with the
|
||||
* same' authority when the issuedAt is too old and a new instance is requested.
|
||||
|
||||
-52
@@ -25,10 +25,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.NonBuildableAuthenticationToken;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
|
||||
import org.springframework.security.authentication.SecurityAssertions;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -178,31 +176,6 @@ public class AuthenticationWebFilterTests {
|
||||
assertThat(result.getResponseCookies()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenAuthenticatedThenCombinesAuthorities() {
|
||||
String ROLE_EXISTING = "ROLE_EXISTING";
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
|
||||
ROLE_EXISTING);
|
||||
given(this.authenticationManager.authenticate(any()))
|
||||
.willReturn(Mono.just(new TestingAuthenticationToken("user", "password", "TEST")));
|
||||
given(this.securityContextRepository.save(any(), any())).willReturn(Mono.empty());
|
||||
this.filter = new AuthenticationWebFilter(this.authenticationManager);
|
||||
this.filter.setSecurityContextRepository(this.securityContextRepository);
|
||||
WebTestClient client = WebTestClientBuilder.bindToWebFilters(new RunAsWebFilter(existingAuthn), this.filter)
|
||||
.build();
|
||||
client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("test", "this"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ArgumentCaptor<SecurityContext> context = ArgumentCaptor.forClass(SecurityContext.class);
|
||||
verify(this.securityContextRepository).save(any(), context.capture());
|
||||
Authentication authentication = context.getValue().getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
|
||||
}
|
||||
|
||||
/**
|
||||
* This is critical to avoid adding duplicate GrantedAuthority instances with the
|
||||
* same' authority when the issuedAt is too old and a new instance is requested.
|
||||
@@ -232,31 +205,6 @@ public class AuthenticationWebFilterTests {
|
||||
.containsExactly(DefaultEqualsGrantedAuthority.AUTHORITY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNotOverridingToBuilderThenDoesNotMergeAuthorities() throws Exception {
|
||||
TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password", "FACTORONE");
|
||||
given(this.authenticationManager.authenticate(any()))
|
||||
.willReturn(Mono.just(new NonBuildableAuthenticationToken("user", "password", "FACTORTWO")));
|
||||
given(this.securityContextRepository.save(any(), any())).willReturn(Mono.empty());
|
||||
this.filter = new AuthenticationWebFilter(this.authenticationManager);
|
||||
this.filter.setSecurityContextRepository(this.securityContextRepository);
|
||||
WebTestClient client = WebTestClientBuilder.bindToWebFilters(new RunAsWebFilter(existingAuthn), this.filter)
|
||||
.build();
|
||||
client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("test", "this"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ArgumentCaptor<SecurityContext> context = ArgumentCaptor.forClass(SecurityContext.class);
|
||||
verify(this.securityContextRepository).save(any(), context.capture());
|
||||
Authentication authentication = context.getValue().getAuthentication();
|
||||
SecurityAssertions.assertThat(authentication)
|
||||
.authorities()
|
||||
.extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactly("FACTORTWO");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationFailThenUnauthorized() {
|
||||
given(this.authenticationManager.authenticate(any()))
|
||||
|
||||
Reference in New Issue
Block a user