Skip to content

Commit 6443fba

Browse files
committed
Add Method Security
1 parent cbe9c76 commit 6443fba

File tree

4 files changed

+99
-50
lines changed

4 files changed

+99
-50
lines changed

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

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,25 @@
2121
import java.util.List;
2222
import java.util.function.Supplier;
2323

24-
import org.jspecify.annotations.Nullable;
2524
import org.junit.jupiter.api.Test;
2625
import org.junit.jupiter.api.extension.ExtendWith;
2726

2827
import org.springframework.beans.factory.annotation.Autowired;
2928
import org.springframework.context.annotation.Bean;
3029
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.security.access.prepost.PreAuthorize;
3131
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
32+
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
3233
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
34+
import org.springframework.security.authorization.AuthorityAuthorizationManager;
35+
import org.springframework.security.authorization.AuthorizationDecision;
3336
import org.springframework.security.authorization.AuthorizationManager;
37+
import org.springframework.security.authorization.AuthorizationManagers;
3438
import org.springframework.security.authorization.AuthorizationResult;
3539
import org.springframework.security.config.Customizer;
3640
import org.springframework.security.config.ObjectPostProcessor;
3741
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
42+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
3843
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3944
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
4045
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
@@ -47,10 +52,9 @@
4752
import org.springframework.security.core.context.SecurityContextChangedListener;
4853
import org.springframework.security.core.context.SecurityContextHolderStrategy;
4954
import org.springframework.security.core.userdetails.PasswordEncodedUser;
55+
import org.springframework.security.core.userdetails.User;
5056
import org.springframework.security.core.userdetails.UserDetails;
5157
import org.springframework.security.core.userdetails.UserDetailsService;
52-
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
53-
import org.springframework.security.crypto.password.PasswordEncoder;
5458
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
5559
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders;
5660
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
@@ -65,6 +69,8 @@
6569
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
6670
import org.springframework.security.web.savedrequest.RequestCache;
6771
import org.springframework.test.web.servlet.MockMvc;
72+
import org.springframework.web.bind.annotation.GetMapping;
73+
import org.springframework.web.bind.annotation.RestController;
6874
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
6975

7076
import static org.hamcrest.Matchers.containsString;
@@ -78,6 +84,7 @@
7884
import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication;
7985
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
8086
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
87+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
8188
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
8289
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8390
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -409,57 +416,58 @@ public void configureWhenPortResolverBeanThenPortResolverUsed() throws Exception
409416

410417
@Test
411418
void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
412-
this.spring.register(MfaDslConfig.class).autowire();
419+
this.spring.register(MfaDslConfig.class, UserConfig.class).autowire();
413420
UserDetails user = PasswordEncodedUser.user();
414-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
421+
this.mockMvc.perform(get("/profile").with(user(user)))
415422
.andExpect(status().is3xxRedirection())
416423
.andExpect(redirectedUrl("http://localhost/login"));
417424
this.mockMvc
418-
.perform(post("/ott/generate").param("username", "user")
419-
.with(SecurityMockMvcRequestPostProcessors.user(user))
425+
.perform(post("/ott/generate").param("username", "rod")
426+
.with(user(user))
420427
.with(SecurityMockMvcRequestPostProcessors.csrf()))
421428
.andExpect(status().is3xxRedirection())
422429
.andExpect(redirectedUrl("/ott/sent"));
423430
this.mockMvc
424-
.perform(post("/login").param("username", user.getUsername())
425-
.param("password", user.getPassword())
431+
.perform(post("/login").param("username", "rod")
432+
.param("password", "password")
426433
.with(SecurityMockMvcRequestPostProcessors.csrf()))
427434
.andExpect(status().is3xxRedirection())
428435
.andExpect(redirectedUrl("/"));
429436
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build();
430-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
437+
this.mockMvc.perform(get("/profile").with(user(user)))
431438
.andExpect(status().is3xxRedirection())
432439
.andExpect(redirectedUrl("http://localhost/login"));
433440
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
434-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
441+
this.mockMvc.perform(get("/profile").with(user(user)))
435442
.andExpect(status().isOk())
436443
.andExpect(content().string(containsString("/ott/generate")));
437444
user = PasswordEncodedUser.withUserDetails(user)
438445
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
439446
.build();
440-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
441-
.andExpect(status().isNotFound());
447+
this.mockMvc.perform(get("/profile").with(user(user))).andExpect(status().isNotFound());
442448
}
443449

444450
@Test
445451
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
446-
this.spring.register(MfaDslX509Config.class).autowire();
447-
this.mockMvc.perform(get("/")).andExpect(status().isForbidden());
452+
this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicController.class).autowire();
453+
this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden());
454+
this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build())))
455+
.andExpect(status().isForbidden());
448456
this.mockMvc.perform(get("/login")).andExpect(status().isOk());
449-
this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
457+
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
450458
.andExpect(status().is3xxRedirection())
451459
.andExpect(redirectedUrl("http://localhost/login"));
452-
UserDetails user = PasswordEncodedUser.withUsername("rod")
453-
.password("password")
454-
.authorities("AUTHN_FORM")
455-
.build();
456460
this.mockMvc
457-
.perform(post("/login").param("username", user.getUsername())
458-
.param("password", user.getPassword())
461+
.perform(post("/login").param("username", "rod")
462+
.param("password", "password")
459463
.with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))
460464
.with(SecurityMockMvcRequestPostProcessors.csrf()))
461465
.andExpect(status().is3xxRedirection())
462466
.andExpect(redirectedUrl("/"));
467+
UserDetails authorized = PasswordEncodedUser.withUsername("rod")
468+
.authorities("profile:read", "FACTOR_X509", "FACTOR_PASSWORD")
469+
.build();
470+
this.mockMvc.perform(get("/profile").with(user(authorized))).andExpect(status().isOk());
463471
}
464472

465473
@Configuration
@@ -832,75 +840,101 @@ public <O> O postProcess(O object) {
832840
static class MfaDslConfig {
833841

834842
@Bean
835-
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
843+
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception {
836844
// @formatter:off
837845
http
838846
.formLogin(Customizer.withDefaults())
839847
.oneTimeTokenLogin(Customizer.withDefaults())
840848
.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"))
849+
.requestMatchers("/profile").access(authz.hasAuthority("profile:read"))
850+
.anyRequest().access(authz.authenticated())
845851
);
846852
return http.build();
847853
// @formatter:on
848854
}
849855

850856
@Bean
851-
UserDetailsService users() {
852-
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
857+
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
858+
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
853859
}
854860

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

865866
}
866867

867868
@Configuration
868869
@EnableWebSecurity
870+
@EnableMethodSecurity
869871
static class MfaDslX509Config {
870872

871873
@Bean
872-
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
874+
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception {
873875
// @formatter:off
874876
http
875-
.formLogin(Customizer.withDefaults())
876877
.x509(Customizer.withDefaults())
878+
.formLogin(Customizer.withDefaults())
877879
.authorizeHttpRequests((authorize) -> authorize
878-
.anyRequest().access(
879-
new HasAllAuthoritiesAuthorizationManager<>("FACTOR_X509", "FACTOR_PASSWORD")
880-
)
880+
.anyRequest().access(authz.authenticated())
881881
);
882882
return http.build();
883883
// @formatter:on
884884
}
885885

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

892906
}
893907

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

896921
private final Collection<String> authorities;
897922

898-
private HasAllAuthoritiesAuthorizationManager(String... authorities) {
923+
AuthorizationManagerFactory(String... authorities) {
899924
this.authorities = List.of(authorities);
900925
}
901926

902-
@Override
903-
public @Nullable AuthorizationResult authorize(Supplier<Authentication> authentication, C object) {
927+
public <T> AuthorizationManager<T> authenticated() {
928+
AuthenticatedAuthorizationManager<T> authenticated = AuthenticatedAuthorizationManager.authenticated();
929+
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authenticated);
930+
}
931+
932+
public <T> AuthorizationManager<T> hasAuthority(String authority) {
933+
AuthorityAuthorizationManager<T> authorized = AuthorityAuthorizationManager.hasAuthority(authority);
934+
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authorized);
935+
}
936+
937+
private <T> AuthorizationResult factors(Supplier<Authentication> authentication, T context) {
904938
List<String> authorities = authentication.get()
905939
.getAuthorities()
906940
.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+
@Nullable Supplier<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)