Skip to content

Commit a69e16f

Browse files
committed
Add Method Security
1 parent 710fafc commit a69e16f

File tree

4 files changed

+100
-49
lines changed

4 files changed

+100
-49
lines changed

config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,19 @@
2828
import org.springframework.beans.factory.annotation.Autowired;
2929
import org.springframework.context.annotation.Bean;
3030
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.security.access.prepost.PreAuthorize;
3132
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
33+
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
3234
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
35+
import org.springframework.security.authorization.AuthorityAuthorizationManager;
36+
import org.springframework.security.authorization.AuthorizationDecision;
3337
import org.springframework.security.authorization.AuthorizationManager;
38+
import org.springframework.security.authorization.AuthorizationManagers;
3439
import org.springframework.security.authorization.AuthorizationResult;
3540
import org.springframework.security.config.Customizer;
3641
import org.springframework.security.config.ObjectPostProcessor;
3742
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
43+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
3844
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3945
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
4046
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
@@ -47,10 +53,9 @@
4753
import org.springframework.security.core.context.SecurityContextChangedListener;
4854
import org.springframework.security.core.context.SecurityContextHolderStrategy;
4955
import org.springframework.security.core.userdetails.PasswordEncodedUser;
56+
import org.springframework.security.core.userdetails.User;
5057
import org.springframework.security.core.userdetails.UserDetails;
5158
import org.springframework.security.core.userdetails.UserDetailsService;
52-
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
53-
import org.springframework.security.crypto.password.PasswordEncoder;
5459
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
5560
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders;
5661
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
@@ -65,6 +70,8 @@
6570
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
6671
import org.springframework.security.web.savedrequest.RequestCache;
6772
import org.springframework.test.web.servlet.MockMvc;
73+
import org.springframework.web.bind.annotation.GetMapping;
74+
import org.springframework.web.bind.annotation.RestController;
6875
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
6976

7077
import static org.hamcrest.Matchers.containsString;
@@ -78,6 +85,7 @@
7885
import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication;
7986
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
8087
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
88+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
8189
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
8290
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8391
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -409,57 +417,58 @@ public void configureWhenPortResolverBeanThenPortResolverUsed() throws Exception
409417

410418
@Test
411419
void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
412-
this.spring.register(MfaDslConfig.class).autowire();
420+
this.spring.register(MfaDslConfig.class, UserConfig.class).autowire();
413421
UserDetails user = PasswordEncodedUser.user();
414-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
422+
this.mockMvc.perform(get("/profile").with(user(user)))
415423
.andExpect(status().is3xxRedirection())
416424
.andExpect(redirectedUrl("http://localhost/login"));
417425
this.mockMvc
418-
.perform(post("/ott/generate").param("username", "user")
419-
.with(SecurityMockMvcRequestPostProcessors.user(user))
426+
.perform(post("/ott/generate").param("username", "rod")
427+
.with(user(user))
420428
.with(SecurityMockMvcRequestPostProcessors.csrf()))
421429
.andExpect(status().is3xxRedirection())
422430
.andExpect(redirectedUrl("/ott/sent"));
423431
this.mockMvc
424-
.perform(post("/login").param("username", user.getUsername())
425-
.param("password", user.getPassword())
432+
.perform(post("/login").param("username", "rod")
433+
.param("password", "password")
426434
.with(SecurityMockMvcRequestPostProcessors.csrf()))
427435
.andExpect(status().is3xxRedirection())
428436
.andExpect(redirectedUrl("/"));
429437
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build();
430-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
438+
this.mockMvc.perform(get("/profile").with(user(user)))
431439
.andExpect(status().is3xxRedirection())
432440
.andExpect(redirectedUrl("http://localhost/login"));
433441
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
434-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
442+
this.mockMvc.perform(get("/profile").with(user(user)))
435443
.andExpect(status().isOk())
436444
.andExpect(content().string(containsString("/ott/generate")));
437445
user = PasswordEncodedUser.withUserDetails(user)
438446
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
439447
.build();
440-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
441-
.andExpect(status().isNotFound());
448+
this.mockMvc.perform(get("/profile").with(user(user))).andExpect(status().isNotFound());
442449
}
443450

444451
@Test
445452
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
446-
this.spring.register(MfaDslX509Config.class).autowire();
447-
this.mockMvc.perform(get("/")).andExpect(status().isForbidden());
453+
this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicController.class).autowire();
454+
this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden());
455+
this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build())))
456+
.andExpect(status().isForbidden());
448457
this.mockMvc.perform(get("/login")).andExpect(status().isOk());
449-
this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
458+
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
450459
.andExpect(status().is3xxRedirection())
451460
.andExpect(redirectedUrl("http://localhost/login"));
452-
UserDetails user = PasswordEncodedUser.withUsername("rod")
453-
.password("password")
454-
.authorities("AUTHN_FORM")
455-
.build();
456461
this.mockMvc
457-
.perform(post("/login").param("username", user.getUsername())
458-
.param("password", user.getPassword())
462+
.perform(post("/login").param("username", "rod")
463+
.param("password", "password")
459464
.with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))
460465
.with(SecurityMockMvcRequestPostProcessors.csrf()))
461466
.andExpect(status().is3xxRedirection())
462467
.andExpect(redirectedUrl("/"));
468+
UserDetails authorized = PasswordEncodedUser.withUsername("rod")
469+
.authorities("profile:read", "FACTOR_X509", "FACTOR_PASSWORD")
470+
.build();
471+
this.mockMvc.perform(get("/profile").with(user(authorized))).andExpect(status().isOk());
463472
}
464473

465474
@Configuration
@@ -832,75 +841,102 @@ public <O> O postProcess(O object) {
832841
static class MfaDslConfig {
833842

834843
@Bean
835-
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
844+
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception {
836845
// @formatter:off
837846
http
838847
.formLogin(Customizer.withDefaults())
839848
.oneTimeTokenLogin(Customizer.withDefaults())
840849
.authorizeHttpRequests((authorize) -> authorize
841-
.requestMatchers("/profile").access(
842-
new HasAllAuthoritiesAuthorizationManager<>("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
843-
)
844-
.anyRequest().access(new HasAllAuthoritiesAuthorizationManager<>("FACTOR_PASSWORD", "FACTOR_OTT"))
850+
.requestMatchers("/profile").access(authz.hasAuthority("profile:read"))
851+
.anyRequest().access(authz.authenticated())
845852
);
846853
return http.build();
847854
// @formatter:on
848855
}
849856

850857
@Bean
851-
UserDetailsService users() {
852-
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
858+
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
859+
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
853860
}
854861

855862
@Bean
856-
PasswordEncoder encoder() {
857-
return NoOpPasswordEncoder.getInstance();
858-
}
859-
860-
@Bean
861-
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
862-
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
863+
AuthorizationManagerFactory authz() {
864+
return new AuthorizationManagerFactory("FACTOR_PASSWORD", "FACTOR_OTT");
863865
}
864866

865867
}
866868

867869
@Configuration
868870
@EnableWebSecurity
871+
@EnableMethodSecurity
869872
static class MfaDslX509Config {
870873

871874
@Bean
872-
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
875+
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception {
873876
// @formatter:off
874877
http
875-
.formLogin(Customizer.withDefaults())
876878
.x509(Customizer.withDefaults())
879+
.formLogin(Customizer.withDefaults())
877880
.authorizeHttpRequests((authorize) -> authorize
878-
.anyRequest().access(
879-
new HasAllAuthoritiesAuthorizationManager<>("FACTOR_X509", "FACTOR_PASSWORD")
880-
)
881+
.anyRequest().access(authz.authenticated())
881882
);
882883
return http.build();
883884
// @formatter:on
884885
}
885886

886887
@Bean
887-
UserDetailsService users() {
888-
return new InMemoryUserDetailsManager(
889-
PasswordEncodedUser.withUsername("rod").password("{noop}password").build());
888+
AuthorizationManagerFactory authz() {
889+
return new AuthorizationManagerFactory("FACTOR_X509", "FACTOR_PASSWORD");
890+
}
891+
892+
}
893+
894+
@Configuration
895+
static class UserConfig {
896+
897+
@Bean
898+
UserDetails rod() {
899+
return PasswordEncodedUser.withUsername("rod").password("password").build();
900+
}
901+
902+
@Bean
903+
UserDetailsService users(UserDetails user) {
904+
return new InMemoryUserDetailsManager(user);
890905
}
891906

892907
}
893908

894-
private static final class HasAllAuthoritiesAuthorizationManager<C> implements AuthorizationManager<C> {
909+
@RestController
910+
static class BasicController {
911+
912+
@GetMapping("/profile")
913+
@PreAuthorize("@authz.hasAuthority('profile:read')")
914+
String profile() {
915+
return "profile";
916+
}
917+
918+
}
919+
920+
public static class AuthorizationManagerFactory {
895921

896922
private final Collection<String> authorities;
897923

898-
private HasAllAuthoritiesAuthorizationManager(String... authorities) {
924+
AuthorizationManagerFactory(String... authorities) {
899925
this.authorities = List.of(authorities);
900926
}
901927

902-
@Override
903-
public @Nullable AuthorizationResult authorize(Supplier<Authentication> authentication, C object) {
928+
public <T> AuthorizationManager<T> authenticated() {
929+
AuthenticatedAuthorizationManager<T> authenticated = AuthenticatedAuthorizationManager.authenticated();
930+
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authenticated);
931+
}
932+
933+
public <T> AuthorizationManager<T> hasAuthority(String authority) {
934+
AuthorityAuthorizationManager<T> authorized = AuthorityAuthorizationManager.hasAuthority(authority);
935+
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authorized);
936+
}
937+
938+
private AuthorizationResult factors(Supplier<? extends @Nullable Authentication> authentication,
939+
Object context) {
904940
List<String> authorities = authentication.get()
905941
.getAuthorities()
906942
.stream()

core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,38 @@
1616

1717
package org.springframework.security.authorization.method;
1818

19+
import java.util.function.Supplier;
20+
1921
import org.jspecify.annotations.Nullable;
2022

2123
import org.springframework.expression.EvaluationContext;
2224
import org.springframework.expression.EvaluationException;
2325
import org.springframework.expression.Expression;
2426
import org.springframework.security.authorization.AuthorizationDeniedException;
27+
import org.springframework.security.authorization.AuthorizationManager;
2528
import org.springframework.security.authorization.AuthorizationResult;
2629
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
30+
import org.springframework.security.core.Authentication;
31+
import org.springframework.util.Assert;
2732

2833
final class ExpressionUtils {
2934

3035
private ExpressionUtils() {
3136
}
3237

3338
static @Nullable AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) {
39+
return evaluate(expr, ctx, () -> null, null);
40+
}
41+
42+
static <T> @Nullable AuthorizationResult evaluate(Expression expr, EvaluationContext ctx,
43+
Supplier<? extends @Nullable Authentication> authentication, @Nullable T context) {
3444
try {
3545
Object result = expr.getValue(ctx);
46+
if (result instanceof AuthorizationManager<?> manager) {
47+
Assert.notNull(authentication, "authentication supplier cannot be null");
48+
Assert.notNull(context, "context cannot be null");
49+
return ((AuthorizationManager<T>) manager).authorize(authentication, context);
50+
}
3651
if (result instanceof AuthorizationResult decision) {
3752
return decision;
3853
}

core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public void setApplicationContext(ApplicationContext context) {
9595
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
9696
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
9797
expressionHandler.setReturnObject(mi.getResult(), ctx);
98-
return ExpressionUtils.evaluate(attribute.getExpression(), ctx);
98+
return ExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi);
9999
}
100100

101101
@Override

core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public void setApplicationContext(ApplicationContext context) {
8585
return null;
8686
}
8787
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
88-
return ExpressionUtils.evaluate(attribute.getExpression(), ctx);
88+
return ExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi);
8989
}
9090

9191
@Override

0 commit comments

Comments
 (0)