Skip to content

Commit 3a97861

Browse files
committed
Add Method Security
1 parent 09f355d commit 3a97861

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;
@@ -64,6 +69,8 @@
6469
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
6570
import org.springframework.security.web.savedrequest.RequestCache;
6671
import org.springframework.test.web.servlet.MockMvc;
72+
import org.springframework.web.bind.annotation.GetMapping;
73+
import org.springframework.web.bind.annotation.RestController;
6774
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
6875

6976
import static org.hamcrest.Matchers.containsString;
@@ -77,6 +84,7 @@
7784
import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication;
7885
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
7986
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
87+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
8088
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
8189
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8290
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -401,57 +409,58 @@ public void configureWhenRegisteringObjectPostProcessorThenInvokedOnExceptionTra
401409

402410
@Test
403411
void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
404-
this.spring.register(MfaDslConfig.class).autowire();
412+
this.spring.register(MfaDslConfig.class, UserConfig.class).autowire();
405413
UserDetails user = PasswordEncodedUser.user();
406-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
414+
this.mockMvc.perform(get("/profile").with(user(user)))
407415
.andExpect(status().is3xxRedirection())
408416
.andExpect(redirectedUrl("http://localhost/login"));
409417
this.mockMvc
410-
.perform(post("/ott/generate").param("username", "user")
411-
.with(SecurityMockMvcRequestPostProcessors.user(user))
418+
.perform(post("/ott/generate").param("username", "rod")
419+
.with(user(user))
412420
.with(SecurityMockMvcRequestPostProcessors.csrf()))
413421
.andExpect(status().is3xxRedirection())
414422
.andExpect(redirectedUrl("/ott/sent"));
415423
this.mockMvc
416-
.perform(post("/login").param("username", user.getUsername())
417-
.param("password", user.getPassword())
424+
.perform(post("/login").param("username", "rod")
425+
.param("password", "password")
418426
.with(SecurityMockMvcRequestPostProcessors.csrf()))
419427
.andExpect(status().is3xxRedirection())
420428
.andExpect(redirectedUrl("/"));
421429
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build();
422-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
430+
this.mockMvc.perform(get("/profile").with(user(user)))
423431
.andExpect(status().is3xxRedirection())
424432
.andExpect(redirectedUrl("http://localhost/login"));
425433
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
426-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
434+
this.mockMvc.perform(get("/profile").with(user(user)))
427435
.andExpect(status().isOk())
428436
.andExpect(content().string(containsString("/ott/generate")));
429437
user = PasswordEncodedUser.withUserDetails(user)
430438
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
431439
.build();
432-
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.user(user)))
433-
.andExpect(status().isNotFound());
440+
this.mockMvc.perform(get("/profile").with(user(user))).andExpect(status().isNotFound());
434441
}
435442

436443
@Test
437444
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
438-
this.spring.register(MfaDslX509Config.class).autowire();
439-
this.mockMvc.perform(get("/")).andExpect(status().isForbidden());
445+
this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicController.class).autowire();
446+
this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden());
447+
this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build())))
448+
.andExpect(status().isForbidden());
440449
this.mockMvc.perform(get("/login")).andExpect(status().isOk());
441-
this.mockMvc.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
450+
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
442451
.andExpect(status().is3xxRedirection())
443452
.andExpect(redirectedUrl("http://localhost/login"));
444-
UserDetails user = PasswordEncodedUser.withUsername("rod")
445-
.password("password")
446-
.authorities("AUTHN_FORM")
447-
.build();
448453
this.mockMvc
449-
.perform(post("/login").param("username", user.getUsername())
450-
.param("password", user.getPassword())
454+
.perform(post("/login").param("username", "rod")
455+
.param("password", "password")
451456
.with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))
452457
.with(SecurityMockMvcRequestPostProcessors.csrf()))
453458
.andExpect(status().is3xxRedirection())
454459
.andExpect(redirectedUrl("/"));
460+
UserDetails authorized = PasswordEncodedUser.withUsername("rod")
461+
.authorities("profile:read", "FACTOR_X509", "FACTOR_PASSWORD")
462+
.build();
463+
this.mockMvc.perform(get("/profile").with(user(authorized))).andExpect(status().isOk());
455464
}
456465

457466
@Configuration
@@ -795,75 +804,102 @@ public <O> O postProcess(O object) {
795804
static class MfaDslConfig {
796805

797806
@Bean
798-
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
807+
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception {
799808
// @formatter:off
800809
http
801810
.formLogin(Customizer.withDefaults())
802811
.oneTimeTokenLogin(Customizer.withDefaults())
803812
.authorizeHttpRequests((authorize) -> authorize
804-
.requestMatchers("/profile").access(
805-
new HasAllAuthoritiesAuthorizationManager<>("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
806-
)
807-
.anyRequest().access(new HasAllAuthoritiesAuthorizationManager<>("FACTOR_PASSWORD", "FACTOR_OTT"))
813+
.requestMatchers("/profile").access(authz.hasAuthority("profile:read"))
814+
.anyRequest().access(authz.authenticated())
808815
);
809816
return http.build();
810817
// @formatter:on
811818
}
812819

813820
@Bean
814-
UserDetailsService users() {
815-
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
821+
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
822+
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
816823
}
817824

818825
@Bean
819-
PasswordEncoder encoder() {
820-
return NoOpPasswordEncoder.getInstance();
821-
}
822-
823-
@Bean
824-
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
825-
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
826+
AuthorizationManagerFactory authz() {
827+
return new AuthorizationManagerFactory("FACTOR_PASSWORD", "FACTOR_OTT");
826828
}
827829

828830
}
829831

830832
@Configuration
831833
@EnableWebSecurity
834+
@EnableMethodSecurity
832835
static class MfaDslX509Config {
833836

834837
@Bean
835-
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
838+
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory authz) throws Exception {
836839
// @formatter:off
837840
http
838-
.formLogin(Customizer.withDefaults())
839841
.x509(Customizer.withDefaults())
842+
.formLogin(Customizer.withDefaults())
840843
.authorizeHttpRequests((authorize) -> authorize
841-
.anyRequest().access(
842-
new HasAllAuthoritiesAuthorizationManager<>("FACTOR_X509", "FACTOR_PASSWORD")
843-
)
844+
.anyRequest().access(authz.authenticated())
844845
);
845846
return http.build();
846847
// @formatter:on
847848
}
848849

849850
@Bean
850-
UserDetailsService users() {
851-
return new InMemoryUserDetailsManager(
852-
PasswordEncodedUser.withUsername("rod").password("{noop}password").build());
851+
AuthorizationManagerFactory authz() {
852+
return new AuthorizationManagerFactory("FACTOR_X509", "FACTOR_PASSWORD");
853+
}
854+
855+
}
856+
857+
@Configuration
858+
static class UserConfig {
859+
860+
@Bean
861+
UserDetails rod() {
862+
return PasswordEncodedUser.withUsername("rod").password("password").build();
863+
}
864+
865+
@Bean
866+
UserDetailsService users(UserDetails user) {
867+
return new InMemoryUserDetailsManager(user);
853868
}
854869

855870
}
856871

857-
private static final class HasAllAuthoritiesAuthorizationManager<C> implements AuthorizationManager<C> {
872+
@RestController
873+
static class BasicController {
874+
875+
@GetMapping("/profile")
876+
@PreAuthorize("@authz.hasAuthority('profile:read')")
877+
String profile() {
878+
return "profile";
879+
}
880+
881+
}
882+
883+
public static class AuthorizationManagerFactory {
858884

859885
private final Collection<String> authorities;
860886

861-
private HasAllAuthoritiesAuthorizationManager(String... authorities) {
887+
AuthorizationManagerFactory(String... authorities) {
862888
this.authorities = List.of(authorities);
863889
}
864890

865-
@Override
866-
public @Nullable AuthorizationResult authorize(Supplier<Authentication> authentication, C object) {
891+
public <T> AuthorizationManager<T> authenticated() {
892+
AuthenticatedAuthorizationManager<T> authenticated = AuthenticatedAuthorizationManager.authenticated();
893+
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authenticated);
894+
}
895+
896+
public <T> AuthorizationManager<T> hasAuthority(String authority) {
897+
AuthorityAuthorizationManager<T> authorized = AuthorityAuthorizationManager.hasAuthority(authority);
898+
return AuthorizationManagers.allOf(new AuthorizationDecision(false), this::factors, authorized);
899+
}
900+
901+
private AuthorizationResult factors(Supplier<? extends @Nullable Authentication> authentication,
902+
Object context) {
867903
List<String> authorities = authentication.get()
868904
.getAuthorities()
869905
.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)