diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 4bb7d682b5b..820aa324be3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -317,6 +317,39 @@ public AuthorizationManagerRequestMatcherRegistry authenticated() { return access(AuthenticatedAuthorizationManager.authenticated()); } + /** + * Specify that URLs are allowed by users who have authenticated and were not + * "remembered". + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customization + * @since 5.8 + * @see RememberMeConfigurer + */ + public AuthorizationManagerRequestMatcherRegistry fullyAuthenticated() { + return access(AuthenticatedAuthorizationManager.fullyAuthenticated()); + } + + /** + * Specify that URLs are allowed by users that have been remembered. + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customization + * @since 5.8 + * @see RememberMeConfigurer + */ + public AuthorizationManagerRequestMatcherRegistry rememberMe() { + return access(AuthenticatedAuthorizationManager.rememberMe()); + } + + /** + * Specify that URLs are allowed by anonymous users. + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customization + * @since 5.8 + */ + public AuthorizationManagerRequestMatcherRegistry anonymous() { + return access(AuthenticatedAuthorizationManager.anonymous()); + } + /** * Allows specifying a custom {@link AuthorizationManager}. * @param manager the {@link AuthorizationManager} to use diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 7f14d3dca9f..4165e16abcd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -26,6 +26,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.RememberMeAuthenticationToken; +import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; @@ -35,7 +37,10 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.security.web.access.intercept.AuthorizationFilter; @@ -57,6 +62,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -492,6 +498,50 @@ private static RequestPostProcessor remoteAddress(String remoteAddress) { }; } + @Test + public void getWhenFullyAuthenticatedConfiguredAndRememberMeTokenThenRespondsWithUnauthorized() throws Exception { + this.spring.register(FullyAuthenticatedConfig.class, BasicController.class).autowire(); + RememberMeAuthenticationToken rememberMe = new RememberMeAuthenticationToken("key", "user", + AuthorityUtils.createAuthorityList("ROLE_USER")); + MockHttpServletRequestBuilder requestWithRememberMe = get("/").with(authentication(rememberMe)); + this.mvc.perform(requestWithRememberMe).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenFullyAuthenticatedConfiguredAndUserThenRespondsWithOk() throws Exception { + this.spring.register(FullyAuthenticatedConfig.class, BasicController.class).autowire(); + MockHttpServletRequestBuilder requestWithUser = get("/").with(user("user").roles("USER")); + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenRememberMeConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception { + this.spring.register(RememberMeConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenRememberMeConfiguredAndRememberMeTokenThenRespondsWithOk() throws Exception { + this.spring.register(RememberMeConfig.class, BasicController.class).autowire(); + RememberMeAuthenticationToken rememberMe = new RememberMeAuthenticationToken("key", "user", + AuthorityUtils.createAuthorityList("ROLE_USER")); + MockHttpServletRequestBuilder requestWithRememberMe = get("/").with(authentication(rememberMe)); + this.mvc.perform(requestWithRememberMe).andExpect(status().isOk()); + } + + @Test + public void getWhenAnonymousConfiguredAndAnonymousUserThenRespondsWithOk() throws Exception { + this.spring.register(AnonymousConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isOk()); + } + + @Test + public void getWhenAnonymousConfiguredAndLoggedInUserThenRespondsWithForbidden() throws Exception { + this.spring.register(AnonymousConfig.class, BasicController.class).autowire(); + MockHttpServletRequestBuilder requestWithUser = get("/").with(user("user")); + this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); + } + @EnableWebSecurity static class NoRequestsConfig { @@ -887,6 +937,74 @@ String path(@PathVariable("username") String username) { } + @EnableWebSecurity + static class FullyAuthenticatedConfig { + + @Bean + SecurityFilterChain chain(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic() + .and() + .rememberMe() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().fullyAuthenticated() + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(TestAuthentication.user()); + } + + } + + @EnableWebSecurity + static class RememberMeConfig { + + @Bean + SecurityFilterChain chain(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic() + .and() + .rememberMe() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().rememberMe() + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(TestAuthentication.user()); + } + + } + + @EnableWebSecurity + static class AnonymousConfig { + + @Bean + SecurityFilterChain chain(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().anonymous() + ); + // @formatter:on + return http.build(); + } + + } + @Configuration static class AuthorizationEventPublisherConfig { diff --git a/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java index baa65ac041a..b9236eae3d6 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; /** * An {@link AuthorizationManager} that determines if the current user is authenticated. @@ -31,7 +32,35 @@ */ public final class AuthenticatedAuthorizationManager implements AuthorizationManager { - private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + private final AbstractAuthorizationStrategy authorizationStrategy; + + /** + * Creates an instance that determines if the current user is authenticated, this is + * the same as calling {@link #authenticated()} factory method. + * + * @since 5.8 + * @see #authenticated() + * @see #fullyAuthenticated() + * @see #rememberMe() + * @see #anonymous() + */ + public AuthenticatedAuthorizationManager() { + this(new AuthenticatedAuthorizationStrategy()); + } + + private AuthenticatedAuthorizationManager(AbstractAuthorizationStrategy authorizationStrategy) { + this.authorizationStrategy = authorizationStrategy; + } + + /** + * Sets the {@link AuthenticationTrustResolver} to be used. Default is + * {@link AuthenticationTrustResolverImpl}. Cannot be null. + * @param trustResolver the {@link AuthenticationTrustResolver} to use + * @since 5.8 + */ + public void setTrustResolver(AuthenticationTrustResolver trustResolver) { + this.authorizationStrategy.setTrustResolver(trustResolver); + } /** * Creates an instance of {@link AuthenticatedAuthorizationManager}. @@ -43,24 +72,98 @@ public static AuthenticatedAuthorizationManager authenticated() { } /** - * Determines if the current user is authorized by evaluating if the - * {@link Authentication} is not anonymous and authenticated. + * Creates an instance of {@link AuthenticatedAuthorizationManager} that determines if + * the {@link Authentication} is authenticated without using remember me. + * @param the type of object being authorized + * @return the new instance + * @since 5.8 + */ + public static AuthenticatedAuthorizationManager fullyAuthenticated() { + return new AuthenticatedAuthorizationManager<>(new FullyAuthenticatedAuthorizationStrategy()); + } + + /** + * Creates an instance of {@link AuthenticatedAuthorizationManager} that determines if + * the {@link Authentication} is authenticated using remember me. + * @param the type of object being authorized + * @return the new instance + * @since 5.8 + */ + public static AuthenticatedAuthorizationManager rememberMe() { + return new AuthenticatedAuthorizationManager<>(new RememberMeAuthorizationStrategy()); + } + + /** + * Creates an instance of {@link AuthenticatedAuthorizationManager} that determines if + * the {@link Authentication} is anonymous. + * @param the type of object being authorized + * @return the new instance + * @since 5.8 + */ + public static AuthenticatedAuthorizationManager anonymous() { + return new AuthenticatedAuthorizationManager<>(new AnonymousAuthorizationStrategy()); + } + + /** + * Determines if the current user is authorized according to the given strategy. * @param authentication the {@link Supplier} of the {@link Authentication} to check * @param object the {@link T} object to check * @return an {@link AuthorizationDecision} */ @Override public AuthorizationDecision check(Supplier authentication, T object) { - boolean granted = isGranted(authentication.get()); + boolean granted = this.authorizationStrategy.isGranted(authentication.get()); return new AuthorizationDecision(granted); } - private boolean isGranted(Authentication authentication) { - return authentication != null && isNotAnonymous(authentication) && authentication.isAuthenticated(); + private abstract static class AbstractAuthorizationStrategy { + + AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + private void setTrustResolver(AuthenticationTrustResolver trustResolver) { + Assert.notNull(trustResolver, "trustResolver cannot be null"); + this.trustResolver = trustResolver; + } + + abstract boolean isGranted(Authentication authentication); + } - private boolean isNotAnonymous(Authentication authentication) { - return !this.trustResolver.isAnonymous(authentication); + private static class AuthenticatedAuthorizationStrategy extends AbstractAuthorizationStrategy { + + @Override + boolean isGranted(Authentication authentication) { + return authentication != null && !this.trustResolver.isAnonymous(authentication) + && authentication.isAuthenticated(); + } + + } + + private static final class FullyAuthenticatedAuthorizationStrategy extends AuthenticatedAuthorizationStrategy { + + @Override + boolean isGranted(Authentication authentication) { + return super.isGranted(authentication) && !this.trustResolver.isRememberMe(authentication); + } + + } + + private static final class AnonymousAuthorizationStrategy extends AbstractAuthorizationStrategy { + + @Override + boolean isGranted(Authentication authentication) { + return this.trustResolver.isAnonymous(authentication); + } + + } + + private static final class RememberMeAuthorizationStrategy extends AbstractAuthorizationStrategy { + + @Override + boolean isGranted(Authentication authentication) { + return this.trustResolver.isRememberMe(authentication); + } + } } diff --git a/core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java index 5bcfed5c517..98914bffc5b 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,13 @@ package org.springframework.security.authorization; +import java.util.Collections; import java.util.function.Supplier; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -74,4 +76,84 @@ public void authenticatedWhenUserNotAuthenticatedThenDeniedDecision() { assertThat(manager.check(() -> authentication, object).isGranted()).isFalse(); } + @Test + public void authenticatedWhenUserRememberMeThenGrantedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.authenticated(); + Supplier authentication = () -> new RememberMeAuthenticationToken("user", "password", + Collections.emptyList()); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void fullyAuthenticatedWhenUserNotAnonymousAndNotRememberMeThenGrantedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.fullyAuthenticated(); + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN", + "ROLE_USER"); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void fullyAuthenticatedWhenUserNullThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.fullyAuthenticated(); + Supplier authentication = () -> null; + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void fullyAuthenticatedWhenUserRememberMeThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.fullyAuthenticated(); + Supplier authentication = () -> new RememberMeAuthenticationToken("user", "password", + Collections.emptyList()); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void fullyAuthenticatedWhenUserAnonymousThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.fullyAuthenticated(); + Supplier authentication = () -> new AnonymousAuthenticationToken("key", "principal", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void anonymousWhenUserAnonymousThenGrantedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.anonymous(); + Supplier authentication = () -> new AnonymousAuthenticationToken("key", "principal", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void anonymousWhenUserNotAnonymousThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.anonymous(); + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN", + "ROLE_USER"); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void rememberMeWhenUserRememberMeThenGrantedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.rememberMe(); + Supplier authentication = () -> new RememberMeAuthenticationToken("user", "password", + Collections.emptyList()); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void rememberMeWhenUserNotRememberMeThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.rememberMe(); + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN", + "ROLE_USER"); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + }