1
0
mirror of synced 2026-07-10 13:20:02 +00:00

Compare commits

..

34 Commits

Author SHA1 Message Date
Josh Cummings e3e7d76b70 Add Workflow to Finalize a Release 2025-11-04 09:32:41 -07:00
github-actions[bot] 9dde69746f Release 7.0.0-RC2 2025-11-04 05:32:57 +00:00
Rob Winch 884cf0d62e EnableGlobalMultiFactorAuthentication->EnableMultiFactorAuthentication
Closes gh-18127
2025-11-03 22:42:28 -06:00
Rob Winch aaf738f7ac MFA is now Opt In
This commit ensures that MFA is only performed when users opt in. By
doing so, we allow users to decide if they will opt into the semantics
of merging two Authentication instances.

Closes gh-18126
2025-11-03 22:42:27 -06:00
Rob Winch ccd39a23c9 Only perform MFA if Authentication.getName() is the same
Closes gh-18112
2025-11-03 22:42:27 -06:00
Josh Cummings 793820acfa Remove Authority Copying From Reactive
We will re-address this when adding factors to
ReactiveAuthenticationManager implementations.

Issue gh-2603
2025-11-03 13:31:30 -07:00
Joe Grandja b6ed037c39 Document device_code grant disabled by default
Issue gh-17998
2025-10-31 06:38:09 -04:00
Joe Grandja 5da0cbea4b Document OAuth 2.0 Dynamic Client Registration support
Issue gh-17964
2025-10-30 16:01:51 -04:00
Joe Grandja e6b4d461e7 Fix OAuth2AuthorizationServerJacksonModule type validator configuration
Closes gh-18102
2025-10-30 07:19:45 -04:00
Josh Cummings 4daf089e46 Merge remote-tracking branch 'origin/6.5.x' 2025-10-28 12:08:53 -06:00
namest504 6501e97ece Fix sensitive case in JwtTypeValidator
Closes gh-18092

Signed-off-by: namest504 <namest504@gmail.com>
2025-10-28 12:08:29 -06:00
Josh Cummings 3a84894bf4 Revert "Add AuthorizationProxyMixin"
This reverts commit 743817fc15.
2025-10-27 17:30:44 -06:00
Joe Grandja 90855aa128 Missing response_type in POST authorization request returns invalid_request
Issue https://github.com/spring-projects/spring-authorization-server/issues/2226
2025-10-24 05:55:45 -04:00
dependabot[bot] 9f7e92d6f2 Bump tools.jackson:jackson-bom from 3.0.0 to 3.0.1
Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.0.0 to 3.0.1.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.0.0...jackson-bom-3.0.1)

---
updated-dependencies:
- dependency-name: tools.jackson:jackson-bom
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-23 09:33:13 -05:00
Josh Cummings 727f0e27d6 Merge branch '6.5.x' 2025-10-20 17:42:52 -06:00
Josh Cummings f548aaf5c5 Merge branch '6.4.x' into 6.5.x 2025-10-20 17:42:25 -06:00
Josh Cummings 743817fc15 Add AuthorizationProxyMixin
This commit adds Jackson configuration specific to
authorization proxies created by Spring Security

Closes gh-18077
2025-10-20 17:16:21 -06:00
Josh Cummings fb701e4615 Merge remote-tracking branch 'origin/6.5.x' 2025-10-20 17:10:05 -06:00
Josh Cummings 1c112005fa Don't Attempt to Generate Token Without Valid Token Request
Closes gh-18088

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2025-10-20 17:09:43 -06:00
Marcus Hert da Coregio e0a71eb00e Fix GenerateOneTimeTokenRequestResolver ignored if username param not present
Signed-off-by: Marcus Hert da Coregio <marcusdacoregio@gmail.com>
2025-10-20 17:09:43 -06:00
Josh Cummings 69d28dc35b Merge branch '6.5.x' 2025-10-20 17:07:34 -06:00
Josh Cummings 42ddaba870 Next Development Version 2025-10-20 17:07:18 -06:00
Josh Cummings da46ba2619 Update Password Samples for Nullability
Issue gh-16226
2025-10-20 17:04:22 -06:00
Josh Cummings a406f5fe2d Merge remote-tracking branch 'origin/6.5.x' 2025-10-20 16:46:49 -06:00
Himanshu Pareek dcb4e47cd5 Add Include-Code to the Password Storage page
References gh-16226

Signed-off-by: Himanshu Pareek <himanshupareekiit01@gmail.com>
2025-10-20 16:35:23 -06:00
Rob Winch 82f87cf2b6 Next Development Version 2025-10-20 16:55:17 -05:00
Josh Cummings 0a2f55d485 Clarify Nullability in Granted Authority Lambda
Issue gh-17999
2025-10-20 15:22:24 -06:00
Andrey Litvitski 9b61533db2 Mark GrantedAuthority#getAuthority as @Nullable
Closes: gh-17999

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2025-10-20 15:22:24 -06:00
Josh Cummings eb43830260 Polish JavaDoc
1. Removed comment about not changing field name in a
serialized object as this is true for all fields in a
Java-serialize POJO
2. Added example value for the constructor that demonstrates
the relationship between a role and an authority

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2025-10-20 15:18:11 -06:00
Yanming Zhou b55c28cf25 Polish SimpleGrantedAuthority
1. Add Javadoc to state that role is prefixed.
2. Rename constructor argument from `role` to `authority` for better readability.

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2025-10-20 15:18:11 -06:00
Simon Von 0927bed66a 📔 Documentation
1. Correct the org.springframework.security.config.annotation.web.LogoutDsl's property description

Signed-off-by: Simon Von <g1672943850@gmail.com>
2025-10-20 15:17:32 -06:00
Josh Cummings 9ed446e6f5 Next Development Version 2025-10-20 15:15:57 -06:00
github-actions[bot] 56a23d9ddc Release 6.5.6 2025-10-20 17:17:40 +00:00
github-actions[bot] dc5aed9b5f Release 6.4.12 2025-10-20 17:17:37 +00:00
67 changed files with 1363 additions and 540 deletions
+41
View File
@@ -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 }}
@@ -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]);
}
}
@@ -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;
}
}
}
@@ -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>
* &#64;Configuration
* &#64;EnableGlobalMultiFactorAuthentication(authorities = { GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
* &#64;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();
@@ -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()]);
}
}
@@ -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.
@@ -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);
}
}
}
@@ -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 {
@@ -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);
@@ -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();
}
@@ -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
@@ -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);
@@ -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.
@@ -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])
@@ -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].
@@ -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[]
}
@@ -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[]
}
}
@@ -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[]
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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[]
}
}
@@ -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[]
}
}
@@ -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
@@ -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())
@@ -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 {
@@ -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[]
}
@@ -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[]
}
}
@@ -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[]
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -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[]
}
}
@@ -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[]
}
}
@@ -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
@@ -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,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
View File
@@ -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
+1 -1
View File
@@ -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"
@@ -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')
@@ -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();
}
}
@@ -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,
@@ -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 {
@@ -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) {
@@ -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) {
@@ -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);
@@ -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
@@ -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());
@@ -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,
@@ -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();
}
}
@@ -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);
}
@@ -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();
@@ -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
@@ -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) {
@@ -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);
@@ -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}
@@ -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) {
@@ -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();
@@ -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)
@@ -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)
@@ -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");
}
}
@@ -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)
@@ -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.
@@ -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()))