Revert unnecessary commits from main
Issue gh-15016
This commit is contained in:
@@ -248,6 +248,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("getFilesToDeserialize")
|
||||
@Disabled("The feature is only supported for versions >= 6.3")
|
||||
void shouldBeAbleToDeserializeClassFromPreviousVersion(Path filePath) {
|
||||
try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile());
|
||||
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.authorization.AuthorizationProxyFactory;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link PrePostMethodSecurityConfiguration}.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@SecurityTestExecutionListeners
|
||||
public class AuthorizationProxyConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
AuthorizationProxyFactory proxyFactory;
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void proxyWhenNotPreAuthorizedThenDenies() {
|
||||
this.spring.register(DefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::makeToast)
|
||||
.withMessage("Access Denied");
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::extractBread)
|
||||
.withMessage("Access Denied");
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void proxyWhenPreAuthorizedThenAllows() {
|
||||
this.spring.register(DefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
toaster.makeToast();
|
||||
assertThat(toaster.extractBread()).isEqualTo("yummy");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proxyReactiveWhenNotPreAuthorizedThenDenies() {
|
||||
this.spring.register(ReactiveDefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
Authentication user = TestAuthentication.authenticatedUser();
|
||||
StepVerifier
|
||||
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
StepVerifier
|
||||
.create(toaster.reactiveExtractBread().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proxyReactiveWhenPreAuthorizedThenAllows() {
|
||||
this.spring.register(ReactiveDefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
Authentication admin = TestAuthentication.authenticatedAdmin();
|
||||
StepVerifier
|
||||
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(admin)))
|
||||
.expectNext()
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
@Configuration
|
||||
static class DefaultsConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableReactiveMethodSecurity
|
||||
@Configuration
|
||||
static class ReactiveDefaultsConfig {
|
||||
|
||||
}
|
||||
|
||||
static class Toaster {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
void makeToast() {
|
||||
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
String extractBread() {
|
||||
return "yummy";
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
Mono<Void> reactiveMakeToast() {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
Mono<String> reactiveExtractBread() {
|
||||
return Mono.just("yummy");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,8 +18,6 @@ package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -47,20 +45,4 @@ public class Authz {
|
||||
return message != null && message.contains(authentication.getName());
|
||||
}
|
||||
|
||||
public AuthorizationResult checkResult(boolean result) {
|
||||
return new AuthzResult(result);
|
||||
}
|
||||
|
||||
public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
|
||||
return Mono.just(checkResult(result));
|
||||
}
|
||||
|
||||
public static class AuthzResult extends AuthorizationDecision {
|
||||
|
||||
public AuthzResult(boolean granted) {
|
||||
super(granted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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,42 +16,23 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.annotation.security.DenyAll;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.parameters.P;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@MethodSecurityService.Mask("classmask")
|
||||
public interface MethodSecurityService {
|
||||
|
||||
@PreAuthorize("denyAll")
|
||||
@@ -72,9 +53,6 @@ public interface MethodSecurityService {
|
||||
@RolesAllowed("ADMIN")
|
||||
String jsr250RolesAllowed();
|
||||
|
||||
@RolesAllowed("USER")
|
||||
String jsr250RolesAllowedUser();
|
||||
|
||||
@Secured({ "ROLE_USER", "RUN_AS_SUPER" })
|
||||
Authentication runAs();
|
||||
|
||||
@@ -90,9 +68,6 @@ public interface MethodSecurityService {
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
void preAuthorizeAdmin();
|
||||
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
void preAuthorizeUser();
|
||||
|
||||
@PreAuthorize("hasPermission(#object,'read')")
|
||||
String hasPermission(String object);
|
||||
|
||||
@@ -115,248 +90,8 @@ public interface MethodSecurityService {
|
||||
@PostAuthorize("returnObject.size == 2")
|
||||
List<String> manyAnnotations(List<String> array);
|
||||
|
||||
@PreFilter("filterObject != 'DropOnPreFilter'")
|
||||
@PreAuthorize("#list.remove('DropOnPreAuthorize')")
|
||||
@Secured("ROLE_SECURED")
|
||||
@RolesAllowed("JSR250")
|
||||
@PostAuthorize("#list.remove('DropOnPostAuthorize')")
|
||||
@PostFilter("filterObject != 'DropOnPostFilter'")
|
||||
List<String> allAnnotations(List<String> list);
|
||||
|
||||
@RequireUserRole
|
||||
@RequireAdminRole
|
||||
void repeatedAnnotations();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StartMaskingHandlerChild.class)
|
||||
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
String preAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = CardNumberMaskingPostProcessor.class)
|
||||
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = PostMaskingPostProcessor.class)
|
||||
String postAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
String preAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
String preAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@NullDenied(role = "ADMIN")
|
||||
String postAuthorizeDeniedWithNullDenied();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
String postAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
String postAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
String preAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask(returnObject)")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
String postAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@AuthorizeReturnObject
|
||||
UserRecordWithEmailProtected getUserRecordWithEmailProtected();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = UserFallbackDeniedHandler.class)
|
||||
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();
|
||||
|
||||
@PreAuthorize("@authz.checkResult(#result)")
|
||||
@PostAuthorize("@authz.checkResult(!#result)")
|
||||
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
|
||||
String checkCustomResult(boolean result);
|
||||
|
||||
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StartMaskingHandlerChild extends StarMaskingHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return super.handleDeniedInvocation(methodInvocation, result) + "-child";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationHandler(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return handle(methodInvocation, authorizationResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationPostProcessor(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation mi, AuthorizationResult authorizationResult) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskValueResolver {
|
||||
|
||||
DefaultMethodSecurityExpressionHandler expressionHandler;
|
||||
|
||||
MaskValueResolver(ApplicationContext context) {
|
||||
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
|
||||
this.expressionHandler.setApplicationContext(context);
|
||||
}
|
||||
|
||||
String resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
|
||||
if (StringUtils.hasText(mask.value())) {
|
||||
return mask.value();
|
||||
}
|
||||
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
|
||||
EvaluationContext evaluationContext = this.expressionHandler
|
||||
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
|
||||
if (returnObject != null) {
|
||||
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
|
||||
}
|
||||
return expression.getValue(evaluationContext, String.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PostMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
static String MASK = "****-****-****-";
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult contextObject, AuthorizationResult result) {
|
||||
String cardNumber = (String) contextObject.getResult();
|
||||
return MASK + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NullPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface Mask {
|
||||
|
||||
String value() default "";
|
||||
|
||||
String expression() default "";
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@PostAuthorize("hasRole('{role}')")
|
||||
@HandleAuthorizationDenied(handlerClass = NullPostProcessor.class)
|
||||
@interface NullDenied {
|
||||
|
||||
String role();
|
||||
|
||||
}
|
||||
|
||||
class UserFallbackDeniedHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
private static final UserRecordWithEmailProtected FALLBACK = new UserRecordWithEmailProtected("Protected",
|
||||
"Protected");
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,14 +28,4 @@ public class MethodSecurityServiceConfig {
|
||||
return new MethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMethodSecurityService reactiveService() {
|
||||
return new ReactiveMethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Authz authz() {
|
||||
return new Authz();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -18,8 +18,6 @@ package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
@@ -58,11 +56,6 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String jsr250RolesAllowedUser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication runAs() {
|
||||
return SecurityContextHolder.getContext().getAuthentication();
|
||||
@@ -80,10 +73,6 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
public void preAuthorizeAdmin() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preAuthorizeUser() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizePermitAll() {
|
||||
return null;
|
||||
@@ -119,88 +108,8 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
return object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> allAnnotations(List<String> list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void repeatedAnnotations() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
|
||||
return cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeThrowAccessDeniedManually() {
|
||||
throw new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeThrowAccessDeniedManually() {
|
||||
throw new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeDeniedWithNullDenied() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserRecordWithEmailProtected getUserRecordWithEmailProtected() {
|
||||
return new UserRecordWithEmailProtected("username", "useremail@example.com");
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
|
||||
return new UserRecordWithEmailProtected("username", "useremail@example.com");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String checkCustomResult(boolean result) {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration;
|
||||
|
||||
public class MyMasker {
|
||||
|
||||
public String getMask(String value) {
|
||||
return value + "-masked";
|
||||
}
|
||||
|
||||
public String getMask() {
|
||||
return "mask";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -17,14 +17,9 @@
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -41,7 +36,6 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.AdviceMode;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
@@ -52,24 +46,12 @@ import org.springframework.security.access.annotation.ExpressionProtectedBusines
|
||||
import org.springframework.security.access.annotation.Jsr250BusinessServiceImpl;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
|
||||
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
@@ -88,12 +70,9 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
@@ -460,6 +439,7 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse();
|
||||
}
|
||||
|
||||
// gh-13572
|
||||
@Test
|
||||
public void configureWhenBeanOverridingDisallowedThenWorks() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, BusinessServiceConfig.class)
|
||||
@@ -467,506 +447,10 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
.autowire();
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void methodSecurityAdminWhenRoleHierarchyBeanAvailableThenUses() {
|
||||
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void methodSecurityUserWhenRoleHierarchyBeanAvailableThenUses() {
|
||||
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void methodSecurityAdminWhenAuthorizationEventPublisherBeanAvailableThenUses() {
|
||||
this.spring
|
||||
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
|
||||
AuthorizationEventPublisherConfig.class)
|
||||
.autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void methodSecurityUserWhenAuthorizationEventPublisherBeanAvailableThenUses() {
|
||||
this.spring
|
||||
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
|
||||
AuthorizationEventPublisherConfig.class)
|
||||
.autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPreFilterThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPreFilterConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(5);
|
||||
assertThat(filtered).containsExactly("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPreAuthorizeThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPreAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(4);
|
||||
assertThat(filtered).containsExactly("DropOnPreAuthorize", "DropOnPostAuthorize", "DropOnPostFilter",
|
||||
"DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetSecuredThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetSecuredConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetJsr250WithInsufficientRolesThenFails() {
|
||||
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "SECURED")
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetJsr250ThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeWithInsufficientRolesThenFails() {
|
||||
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostFilterThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPostFilterConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(2);
|
||||
assertThat(filtered).containsExactly("DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnAfterAllOffsetConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(1);
|
||||
assertThat(filtered).containsExactly("DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.hasRole("USER")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.hasUserRole()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void methodWhenParameterizedAnnotationThenFails() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(service::placeholdersOnlyResolvedByMetaAnnotations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "SCOPE_message:read")
|
||||
public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.readMessage()).isEqualTo("message");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
public void methodWhenMultiplePlaceholdersHasRoleThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.readMessage()).isEqualTo("message");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
service.startsWithDave("daveMatthews");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> service.startsWithDave("jenniferHarper"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodWhenPreFilterMetaAnnotationThenFilters() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.parametersContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
|
||||
.containsExactly("dave");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodWhenPostFilterMetaAnnotationThenFilters() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.resultsContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
|
||||
.containsExactly("dave");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findByIdWhenAuthorizedResultThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.findById("1");
|
||||
assertThatNoException().isThrownBy(flight::getAltitude);
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findByIdWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.findById("1");
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findAllWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll().forEachRemaining((flight) -> {
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeWhenAuthorizedResultThenRemoves() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.remove("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findAllWhenPostFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll()
|
||||
.forEachRemaining((flight) -> assertThat(flight.getPassengers()).extracting(Passenger::getName)
|
||||
.doesNotContain("Kevin Mitnick"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findAllWhenPreFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll().forEachRemaining((flight) -> {
|
||||
flight.board(new ArrayList<>(List.of("John")));
|
||||
assertThat(flight.getPassengers()).extracting(Passenger::getName).doesNotContain("John");
|
||||
flight.board(new ArrayList<>(List.of("John Doe")));
|
||||
assertThat(flight.getPassengers()).extracting(Passenger::getName).contains("John Doe");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll().forEachRemaining((flight) -> {
|
||||
List<Passenger> passengers = flight.getPassengers();
|
||||
passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(passenger::getName));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
MethodSecurityService.CardNumberMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
|
||||
assertThat(cardNumber).isEqualTo("****-****-****-1111");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
|
||||
assertThat(cardNumber).isEqualTo("***");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class,
|
||||
MethodSecurityService.StartMaskingHandlerChild.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111");
|
||||
assertThat(cardNumber).isEqualTo("***-child");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenHandled() {
|
||||
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
assertThat(service.preAuthorizeThrowAccessDeniedManually()).isEqualTo("***");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenHandled() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.PostMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
assertThat(service.postAuthorizeThrowAccessDeniedManually()).isEqualTo("***");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
assertThat(result).isEqualTo("methodmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
assertThat(result).isEqualTo("classmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MetaAnnotationPlaceholderConfig.class,
|
||||
MethodSecurityService.NullPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeDeniedWithNullDenied();
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
assertThat(result).isEqualTo("methodmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
assertThat(result).isEqualTo("classmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("ok-masked");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("mask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenAuthorizedAndUserEmailIsProtectedAndNotAuthorizedThenReturnEmailMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
UserRecordWithEmailProtected.EmailMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
UserRecordWithEmailProtected user = service.getUserRecordWithEmailProtected();
|
||||
assertThat(user.email()).isEqualTo("use******@example.com");
|
||||
assertThat(user.name()).isEqualTo("username");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.UserFallbackDeniedHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
UserRecordWithEmailProtected user = service.getUserWithFallbackWhenUnauthorized();
|
||||
assertThat(user.email()).isEqualTo("Protected");
|
||||
assertThat(user.name()).isEqualTo("Protected");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
|
||||
.getBean(MethodAuthorizationDeniedHandler.class);
|
||||
assertThat(service.checkCustomResult(false)).isNull();
|
||||
verify(handler).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
clearInvocations(handler);
|
||||
assertThat(service.checkCustomResult(true)).isNull();
|
||||
verify(handler).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
private static Advisor returnAdvisor(int order) {
|
||||
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
|
||||
pointcut.setPattern(".*MethodSecurityServiceImpl.*");
|
||||
MethodInterceptor interceptor = (mi) -> mi.getArguments()[0];
|
||||
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, interceptor);
|
||||
advisor.setOrder(order);
|
||||
return advisor;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AuthzConfig {
|
||||
|
||||
@Bean
|
||||
Authz authz() {
|
||||
return new Authz();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCustomMethodSecurity
|
||||
static class CustomMethodSecurityServiceConfig {
|
||||
@@ -1143,341 +627,4 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
|
||||
static class RoleHierarchyConfig {
|
||||
|
||||
@Bean
|
||||
static RoleHierarchy roleHierarchy() {
|
||||
RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
|
||||
roleHierarchyImpl.setHierarchy("ROLE_ADMIN > ROLE_USER");
|
||||
return roleHierarchyImpl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPreFilterConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreFilter() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPreAuthorizeConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreAuthorize() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetSecuredConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforeSecured() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.SECURED.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetJsr250Config {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforeJsr250() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.JSR250.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPostAuthorizeConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreAuthorize() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPostFilterConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePostFilter() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnAfterAllOffsetConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnAfterAll() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET + 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity(offset = OffsetConfig.OFFSET, jsr250Enabled = true, securedEnabled = true)
|
||||
static class OffsetConfig {
|
||||
|
||||
static final int OFFSET = 2;
|
||||
|
||||
@Bean
|
||||
MethodSecurityService methodSecurityService() {
|
||||
return new MethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Authz authz() {
|
||||
return new Authz();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
static class MetaAnnotationPlaceholderConfig {
|
||||
|
||||
@Bean
|
||||
PrePostTemplateDefaults methodSecurityDefaults() {
|
||||
return new PrePostTemplateDefaults();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MetaAnnotationService metaAnnotationService() {
|
||||
return new MetaAnnotationService();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class MetaAnnotationService {
|
||||
|
||||
@RequireRole(role = "#role")
|
||||
boolean hasRole(String role) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@RequireRole(role = "'USER'")
|
||||
boolean hasUserRole() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole({role})")
|
||||
void placeholdersOnlyResolvedByMetaAnnotations() {
|
||||
}
|
||||
|
||||
@HasClaim(claim = "message:read", roles = { "'ADMIN'" })
|
||||
String readMessage() {
|
||||
return "message";
|
||||
}
|
||||
|
||||
@ResultStartsWith("dave")
|
||||
String startsWithDave(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@ParameterContains("dave")
|
||||
List<String> parametersContainDave(List<String> list) {
|
||||
return list;
|
||||
}
|
||||
|
||||
@ResultContains("dave")
|
||||
List<String> resultsContainDave(List<String> list) {
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasRole({role})")
|
||||
@interface RequireRole {
|
||||
|
||||
String role();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})")
|
||||
@interface HasClaim {
|
||||
|
||||
String claim();
|
||||
|
||||
String[] roles() default {};
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PostAuthorize("returnObject.startsWith('{value}')")
|
||||
@interface ResultStartsWith {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreFilter("filterObject.contains('{value}')")
|
||||
@interface ParameterContains {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PostFilter("filterObject.contains('{value}')")
|
||||
@interface ResultContains {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
@Configuration
|
||||
static class AuthorizeResultConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
|
||||
return (f) -> f.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
|
||||
}
|
||||
|
||||
@Bean
|
||||
FlightRepository flights() {
|
||||
FlightRepository flights = new FlightRepository();
|
||||
Flight one = new Flight("1", 35000d, 35);
|
||||
one.board(new ArrayList<>(List.of("Marie Curie", "Kevin Mitnick", "Ada Lovelace")));
|
||||
flights.save(one);
|
||||
Flight two = new Flight("2", 32000d, 72);
|
||||
two.board(new ArrayList<>(List.of("Albert Einstein")));
|
||||
flights.save(two);
|
||||
return flights;
|
||||
}
|
||||
|
||||
@Bean
|
||||
RoleHierarchy roleHierarchy() {
|
||||
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class FlightRepository {
|
||||
|
||||
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
|
||||
|
||||
Iterator<Flight> findAll() {
|
||||
return this.flights.values().iterator();
|
||||
}
|
||||
|
||||
Flight findById(String id) {
|
||||
return this.flights.get(id);
|
||||
}
|
||||
|
||||
Flight save(Flight flight) {
|
||||
this.flights.put(flight.getId(), flight);
|
||||
return flight;
|
||||
}
|
||||
|
||||
void remove(String id) {
|
||||
this.flights.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class Flight {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Double altitude;
|
||||
|
||||
private final Integer seats;
|
||||
|
||||
private final List<Passenger> passengers = new ArrayList<>();
|
||||
|
||||
Flight(String id, Double altitude, Integer seats) {
|
||||
this.id = id;
|
||||
this.altitude = altitude;
|
||||
this.seats = seats;
|
||||
}
|
||||
|
||||
String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
Double getAltitude() {
|
||||
return this.altitude;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('seating:read')")
|
||||
Integer getSeats() {
|
||||
return this.seats;
|
||||
}
|
||||
|
||||
@PostAuthorize("hasAuthority('seating:read')")
|
||||
@PostFilter("filterObject.name != 'Kevin Mitnick'")
|
||||
List<Passenger> getPassengers() {
|
||||
return this.passengers;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('seating:read')")
|
||||
@PreFilter("filterObject.contains(' ')")
|
||||
void board(List<String> passengers) {
|
||||
for (String passenger : passengers) {
|
||||
this.passengers.add(new Passenger(passenger));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Passenger {
|
||||
|
||||
String name;
|
||||
|
||||
public Passenger(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
static class CustomResultConfig {
|
||||
|
||||
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
|
||||
|
||||
@Bean
|
||||
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@SecurityTestExecutionListeners
|
||||
public class PrePostReactiveMethodSecurityConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.CardNumberMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
|
||||
.expectNext("****-****-****-1111")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
|
||||
.expectNext("***")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class,
|
||||
ReactiveMethodSecurityService.StartMaskingHandlerChild.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111"))
|
||||
.expectNext("***-child")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeDeniedMethodWithMaskAnnotation())
|
||||
.expectNext("methodmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeDeniedMethodWithNoMaskAnnotation())
|
||||
.expectNext("classmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.PostMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeThrowAccessDeniedManually()).expectNext("***").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenHandled() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeThrowAccessDeniedManually()).expectNext("***").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.NullPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeDeniedWithNullDenied()).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeDeniedMethodWithMaskAnnotation())
|
||||
.expectNext("methodmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeDeniedMethodWithNoMaskAnnotation())
|
||||
.expectNext("classmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean())
|
||||
.expectNext("ok-masked")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("mask").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
static class MethodSecurityServiceEnabledConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveMethodSecurityService methodSecurityService() {
|
||||
return new ReactiveMethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2019 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,53 +16,22 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.expression.SecurityExpressionRoot;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.intercept.method.MockMethodInvocation;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Tadaya Tsuyukubo
|
||||
@@ -72,13 +41,14 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired(required = false)
|
||||
@Autowired
|
||||
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;
|
||||
|
||||
@Test
|
||||
public void rolePrefixWithGrantedAuthorityDefaults() throws NoSuchMethodException {
|
||||
this.spring.register(WithRolePrefixConfiguration.class).autowire();
|
||||
Authentication authentication = TestAuthentication.authenticatedUser(authorities("CUSTOM_ABC"));
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
|
||||
"CUSTOM_ABC");
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
|
||||
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
|
||||
methodInvocation);
|
||||
@@ -92,7 +62,8 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
@Test
|
||||
public void rolePrefixWithDefaultConfig() throws NoSuchMethodException {
|
||||
this.spring.register(ReactiveMethodSecurityConfiguration.class).autowire();
|
||||
Authentication authentication = TestAuthentication.authenticatedUser(authorities("ROLE_ABC"));
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
|
||||
"ROLE_ABC");
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
|
||||
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
|
||||
methodInvocation);
|
||||
@@ -104,7 +75,8 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
@Test
|
||||
public void rolePrefixWithGrantedAuthorityDefaultsAndSubclassWithProxyingEnabled() throws NoSuchMethodException {
|
||||
this.spring.register(SubclassConfig.class).autowire();
|
||||
Authentication authentication = TestAuthentication.authenticatedUser(authorities("ROLE_ABC"));
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
|
||||
"ROLE_ABC");
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
|
||||
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
|
||||
methodInvocation);
|
||||
@@ -113,132 +85,6 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
assertThat(root.hasRole("ABC")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdWhenAuthorizedResultThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getAltitude)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getSeats)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getSeats)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getAltitude)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getSeats)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(2)
|
||||
.verifyComplete();
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getAltitude)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeWhenAuthorizedResultThenRemoves() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier.create(flights.remove("1").contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenPostFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getPassengers)
|
||||
.flatMap(Passenger::getName)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNext("Marie Curie", "Ada Lovelace", "Albert Einstein")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenPreFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap((flight) -> flight.board(Flux.just("John Doe", "John")).then(Mono.just(flight)))
|
||||
.flatMap(Flight::getPassengers)
|
||||
.flatMap(Passenger::getName)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNext("Marie Curie", "Ada Lovelace", "John Doe", "Albert Einstein", "John Doe")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getPassengers)
|
||||
.flatMap(Passenger::getName)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
|
||||
.getBean(MethodAuthorizationDeniedHandler.class);
|
||||
assertThat(service.checkCustomResult(false).block()).isNull();
|
||||
verify(handler).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
clearInvocations(handler);
|
||||
assertThat(service.checkCustomResult(true).block()).isNull();
|
||||
verify(handler).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
}
|
||||
|
||||
private static Consumer<User.UserBuilder> authorities(String... authorities) {
|
||||
return (builder) -> builder.authorities(authorities);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity // this imports ReactiveMethodSecurityConfiguration
|
||||
static class WithRolePrefixConfiguration {
|
||||
@@ -262,130 +108,4 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableReactiveMethodSecurity
|
||||
@Configuration
|
||||
static class AuthorizeResultConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
|
||||
return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
|
||||
}
|
||||
|
||||
@Bean
|
||||
FlightRepository flights() {
|
||||
FlightRepository flights = new FlightRepository();
|
||||
Flight one = new Flight("1", 35000d, 35);
|
||||
one.board(Flux.just("Marie Curie", "Kevin Mitnick", "Ada Lovelace")).block();
|
||||
flights.save(one).block();
|
||||
Flight two = new Flight("2", 32000d, 72);
|
||||
two.board(Flux.just("Albert Einstein")).block();
|
||||
flights.save(two).block();
|
||||
return flights;
|
||||
}
|
||||
|
||||
@Bean
|
||||
Function<Passenger, Mono<Boolean>> isNotKevin() {
|
||||
return (passenger) -> passenger.getName().map((name) -> !name.equals("Kevin Mitnick"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class FlightRepository {
|
||||
|
||||
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
|
||||
|
||||
Flux<Flight> findAll() {
|
||||
return Flux.fromIterable(this.flights.values());
|
||||
}
|
||||
|
||||
Mono<Flight> findById(String id) {
|
||||
return Mono.just(this.flights.get(id));
|
||||
}
|
||||
|
||||
Mono<Flight> save(Flight flight) {
|
||||
this.flights.put(flight.getId(), flight);
|
||||
return Mono.just(flight);
|
||||
}
|
||||
|
||||
Mono<Void> remove(String id) {
|
||||
this.flights.remove(id);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class Flight {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Double altitude;
|
||||
|
||||
private final Integer seats;
|
||||
|
||||
private final List<Passenger> passengers = new ArrayList<>();
|
||||
|
||||
Flight(String id, Double altitude, Integer seats) {
|
||||
this.id = id;
|
||||
this.altitude = altitude;
|
||||
this.seats = seats;
|
||||
}
|
||||
|
||||
String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
Mono<Double> getAltitude() {
|
||||
return Mono.just(this.altitude);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
|
||||
Mono<Integer> getSeats() {
|
||||
return Mono.just(this.seats);
|
||||
}
|
||||
|
||||
@PostAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
|
||||
@PostFilter("@isNotKevin.apply(filterObject)")
|
||||
Flux<Passenger> getPassengers() {
|
||||
return Flux.fromIterable(this.passengers);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
|
||||
@PreFilter("filterObject.contains(' ')")
|
||||
Mono<Void> board(Flux<String> passengers) {
|
||||
return passengers.doOnNext((passenger) -> this.passengers.add(new Passenger(passenger))).then();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Passenger {
|
||||
|
||||
String name;
|
||||
|
||||
public Passenger(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
public Mono<String> getName() {
|
||||
return Mono.just(this.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableReactiveMethodSecurity
|
||||
static class CustomResultConfig {
|
||||
|
||||
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
|
||||
|
||||
@Bean
|
||||
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ReactiveMethodSecurityService.Mask("classmask")
|
||||
public interface ReactiveMethodSecurityService {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StartMaskingHandlerChild.class)
|
||||
Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
Mono<String> preAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = CardNumberMaskingPostProcessor.class)
|
||||
Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = PostMaskingPostProcessor.class)
|
||||
Mono<String> postAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@NullDenied(role = "ADMIN")
|
||||
Mono<String> postAuthorizeDeniedWithNullDenied();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
Mono<String> preAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask(returnObject)")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
Mono<String> postAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@PreAuthorize("@authz.checkReactiveResult(#result)")
|
||||
@PostAuthorize("@authz.checkReactiveResult(!#result)")
|
||||
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
|
||||
Mono<String> checkCustomResult(boolean result);
|
||||
|
||||
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StartMaskingHandlerChild extends StarMaskingHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return super.handleDeniedInvocation(methodInvocation, result) + "-child";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationHandler(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationPostProcessor(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation mi, AuthorizationResult authorizationResult) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskValueResolver {
|
||||
|
||||
DefaultMethodSecurityExpressionHandler expressionHandler;
|
||||
|
||||
MaskValueResolver(ApplicationContext context) {
|
||||
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
|
||||
this.expressionHandler.setApplicationContext(context);
|
||||
}
|
||||
|
||||
Mono<String> resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
|
||||
if (StringUtils.hasText(mask.value())) {
|
||||
return Mono.just(mask.value());
|
||||
}
|
||||
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
|
||||
EvaluationContext evaluationContext = this.expressionHandler
|
||||
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
|
||||
if (returnObject != null) {
|
||||
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
|
||||
}
|
||||
return Mono.just(expression.getValue(evaluationContext, String.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PostMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
static String MASK = "****-****-****-";
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult contextObject, AuthorizationResult result) {
|
||||
String cardNumber = (String) contextObject.getResult();
|
||||
return MASK + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NullPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface Mask {
|
||||
|
||||
String value() default "";
|
||||
|
||||
String expression() default "";
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@PostAuthorize("hasRole('{value}')")
|
||||
@HandleAuthorizationDenied(handlerClass = NullPostProcessor.class)
|
||||
@interface NullDenied {
|
||||
|
||||
String role();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
|
||||
public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurityService {
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return Mono.just(cardNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
|
||||
return Mono.just(cardNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeThrowAccessDeniedManually() {
|
||||
return Mono.error(new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return Mono.just(cardNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeThrowAccessDeniedManually() {
|
||||
return Mono.error(new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeDeniedWithNullDenied() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> checkCustomResult(boolean result) {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
|
||||
public class UserRecordWithEmailProtected {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String email;
|
||||
|
||||
public UserRecordWithEmailProtected(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = EmailMaskingPostProcessor.class)
|
||||
public String email() {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
public static class EmailMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
String email = (String) methodInvocationResult.getResult();
|
||||
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration.issue14637;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaRepositories("org.springframework.security.config.annotation.method.configuration.issue14637.repo")
|
||||
@EnableTransactionManagement
|
||||
public class ApplicationConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
|
||||
return builder.setType(EmbeddedDatabaseType.HSQL).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
|
||||
vendorAdapter.setDatabase(Database.HSQL);
|
||||
vendorAdapter.setGenerateDdl(true);
|
||||
vendorAdapter.setShowSql(true);
|
||||
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
|
||||
factory.setJpaVendorAdapter(vendorAdapter);
|
||||
factory.setPackagesToScan(Entry.class.getPackage().getName());
|
||||
factory.setDataSource(dataSource());
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager() {
|
||||
JpaTransactionManager txManager = new JpaTransactionManager();
|
||||
txManager.setEntityManagerFactory(entityManagerFactory().getObject());
|
||||
return txManager;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration.issue14637;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.repo.EntryRepository;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = { ApplicationConfig.class, SecurityConfig.class })
|
||||
public class Issue14637Tests {
|
||||
|
||||
@Autowired
|
||||
private EntryRepository entries;
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void authenticateWhenInvalidPasswordThenBadCredentialsException() {
|
||||
Entry entry = new Entry();
|
||||
entry.setId(123L);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.entries.save(entry));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration.issue14637;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration.issue14637.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@Entity
|
||||
public class Entry {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.method.configuration.issue14637.repo;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public interface EntryRepository extends CrudRepository<Entry, String> {
|
||||
|
||||
@PreAuthorize("#entry.id == null")
|
||||
Entry save(Entry entry);
|
||||
|
||||
}
|
||||
@@ -47,9 +47,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordCheckResult;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordException;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@@ -63,8 +60,8 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
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.provisioning.UserDetailsManager;
|
||||
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
@@ -398,41 +395,6 @@ public class HttpSecurityConfigurationTests {
|
||||
this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordCompromisedThenUnauthorized() throws Exception {
|
||||
this.spring
|
||||
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
this.mockMvc.perform(formLogin().password("password"))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/login?error"), unauthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToResetPassword() throws Exception {
|
||||
this.spring
|
||||
.register(SecurityEnabledRedirectIfPasswordExceptionConfig.class, UserDetailsConfig.class,
|
||||
CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
this.mockMvc.perform(formLogin().password("password"))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/reset-password"), unauthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordNotCompromisedThenSuccess() throws Exception {
|
||||
this.spring
|
||||
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
UserDetailsManager userDetailsManager = this.spring.getContext().getBean(UserDetailsManager.class);
|
||||
UserDetails notCompromisedPwUser = User.withDefaultPasswordEncoder()
|
||||
.username("user2")
|
||||
.password("password2")
|
||||
.roles("USER")
|
||||
.build();
|
||||
userDetailsManager.createUser(notCompromisedPwUser);
|
||||
this.mockMvc.perform(formLogin().user("user2").password("password2"))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class NameController {
|
||||
|
||||
@@ -493,7 +455,7 @@ public class HttpSecurityConfigurationTests {
|
||||
static class UserDetailsConfig {
|
||||
|
||||
@Bean
|
||||
InMemoryUserDetailsManager userDetailsService() {
|
||||
UserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
@@ -770,52 +732,4 @@ public class HttpSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CompromisedPasswordCheckerConfig {
|
||||
|
||||
@Bean
|
||||
TestCompromisedPasswordChecker compromisedPasswordChecker() {
|
||||
return new TestCompromisedPasswordChecker();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
static class SecurityEnabledRedirectIfPasswordExceptionConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
return http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin((form) -> form
|
||||
.failureHandler((request, response, exception) -> {
|
||||
if (exception instanceof CompromisedPasswordException) {
|
||||
response.sendRedirect("/reset-password");
|
||||
return;
|
||||
}
|
||||
response.sendRedirect("/login?error");
|
||||
})
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestCompromisedPasswordChecker implements CompromisedPasswordChecker {
|
||||
|
||||
@Override
|
||||
public CompromisedPasswordCheckResult check(String password) {
|
||||
if ("password".equals(password)) {
|
||||
return new CompromisedPasswordCheckResult(true);
|
||||
}
|
||||
return new CompromisedPasswordCheckResult(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -38,6 +38,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
|
||||
@@ -50,7 +52,6 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
@@ -58,14 +59,17 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@@ -73,10 +77,13 @@ import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -320,47 +327,6 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
private void testTokenExchangeGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
|
||||
.willReturn(accessTokenResponse);
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(HttpServletRequest.class.getName(), this.request)
|
||||
.attribute(HttpServletResponse.class.getName(), this.response)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(TokenExchangeGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
|
||||
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
|
||||
}
|
||||
|
||||
private static OAuth2AccessToken getExpiredAccessToken() {
|
||||
Instant expiresAt = Instant.now().minusSeconds(60);
|
||||
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
|
||||
@@ -387,32 +353,37 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockAuthorizationCodeClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockRefreshTokenClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockClientCredentialsClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockPasswordClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockJwtBearerClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
|
||||
return mock(DefaultOAuth2UserService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
|
||||
return mock(OidcUserService.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -429,35 +400,28 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
@Bean
|
||||
RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider() {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockRefreshTokenClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockClientCredentialsClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordOAuth2AuthorizedClientProvider passwordProvider() {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockPasswordClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider() {
|
||||
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockJwtBearerClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@@ -465,10 +429,21 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
abstract static class OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.oauth2Client(Customizer.withDefaults());
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clientRegistrationRepository() {
|
||||
// @formatter:off
|
||||
return new InMemoryClientRegistrationRepository(
|
||||
return new InMemoryClientRegistrationRepository(Arrays.asList(
|
||||
CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
.clientId("google-client-id")
|
||||
.clientSecret("google-client-secret")
|
||||
@@ -488,15 +463,7 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
.clientId("okta-client-id")
|
||||
.clientSecret("okta-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
|
||||
.build(),
|
||||
ClientRegistration.withRegistrationId("auth0")
|
||||
.clientName("Auth0")
|
||||
.clientId("auth0-client-id")
|
||||
.clientSecret("auth0-client-secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.scope("user.read", "user.write")
|
||||
.build());
|
||||
.build()));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@@ -527,11 +494,51 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
|
||||
implements OAuth2AccessTokenResponseClient<T> {
|
||||
private static class MockAuthorizationCodeClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) {
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockRefreshTokenClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockClientCredentialsClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockPasswordClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@@ -545,17 +543,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(request).andExpect(status().isOk());
|
||||
request = get("/user/deny");
|
||||
this.mvc.perform(request).andExpect(status().isUnauthorized());
|
||||
|
||||
UserDetails user = TestAuthentication.withUsername("taehong").build();
|
||||
Authentication authentication = TestAuthentication.authenticated(user);
|
||||
request = get("/v2/user/{username}", user.getUsername()).with(authentication(authentication));
|
||||
this.mvc.perform(request).andExpect(status().isOk());
|
||||
|
||||
request = get("/v2/user/{username}", "withNoAuthentication");
|
||||
this.mvc.perform(request).andExpect(status().isUnauthorized());
|
||||
|
||||
request = get("/v2/user/{username}", "another").with(authentication(authentication));
|
||||
this.mvc.perform(request).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
private static RequestPostProcessor remoteAddress(String remoteAddress) {
|
||||
@@ -609,20 +596,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenNotConfigAndAuthenticatedThenRespondsWithForbidden() throws Exception {
|
||||
this.spring.register(NotConfig.class, BasicController.class).autowire();
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/").with(user("user"));
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenNotConfigAndNotAuthenticatedThenRespondsWithOk() throws Exception {
|
||||
this.spring.register(NotConfig.class, BasicController.class).autowire();
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/");
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class GrantedAuthorityDefaultHasRoleConfig {
|
||||
@@ -1080,7 +1053,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.requestMatchers("/user/{username}").access(new WebExpressionAuthorizationManager("#username == 'user'"))
|
||||
.requestMatchers("/v2/user/{username}").hasVariable("username").equalTo(Authentication::getName)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
@@ -1094,11 +1066,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
return username;
|
||||
}
|
||||
|
||||
@RequestMapping("/v2/user/{username}")
|
||||
String pathV2(@PathVariable("username") String username) {
|
||||
return username;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1169,24 +1136,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class NotConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain chain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().not().authenticated()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AuthorizationEventPublisherConfig {
|
||||
|
||||
|
||||
@@ -1,589 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.web.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.JwtBearerReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for
|
||||
* {@link ReactiveOAuth2ClientConfiguration.ReactiveOAuth2AuthorizedClientManagerConfiguration}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class ReactiveOAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
private static ReactiveOAuth2AccessTokenResponseClient<? super AbstractOAuth2AuthorizationGrantRequest> MOCK_RESPONSE_CLIENT;
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
||||
|
||||
@Autowired
|
||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider;
|
||||
|
||||
private MockServerWebExchange exchange;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
MOCK_RESPONSE_CLIENT = mock(ReactiveOAuth2AccessTokenResponseClient.class);
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
|
||||
this.exchange = MockServerWebExchange.builder(request).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadContextWhenOAuth2ClientEnabledThenConfigured() {
|
||||
this.spring.register(MinimalOAuth2ClientConfig.class).autowire();
|
||||
assertThat(this.authorizedClientManager).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenAuthorizationCodeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId("google")
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
|
||||
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
|
||||
.extracting(OAuth2AuthorizationException::getError)
|
||||
.extracting(OAuth2Error::getErrorCode)
|
||||
.isEqualTo("client_authorization_required");
|
||||
// @formatter:on
|
||||
|
||||
verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenAuthorizedClientServiceBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientServiceConfig.class).autowire();
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId("google")
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
|
||||
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
|
||||
.extracting(OAuth2AuthorizationException::getError)
|
||||
.extracting(OAuth2Error::getErrorCode)
|
||||
.isEqualTo("client_authorization_required");
|
||||
// @formatter:on
|
||||
|
||||
verify(this.authorizedClientService).loadAuthorizedClient(authorizeRequest.getClientRegistrationId(),
|
||||
authentication.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenRefreshTokenAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testRefreshTokenGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenRefreshTokenAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testRefreshTokenGrant();
|
||||
}
|
||||
|
||||
private void testRefreshTokenGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google")
|
||||
.block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
|
||||
authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken());
|
||||
this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.exchange)
|
||||
.block();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2RefreshTokenGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2RefreshTokenGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
OAuth2RefreshTokenGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN);
|
||||
assertThat(grantRequest.getAccessToken()).isEqualTo(existingAuthorizedClient.getAccessToken());
|
||||
assertThat(grantRequest.getRefreshToken()).isEqualTo(existingAuthorizedClient.getRefreshToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientCredentialsAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testClientCredentialsGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientCredentialsAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testClientCredentialsGrant();
|
||||
}
|
||||
|
||||
private void testClientCredentialsGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2ClientCredentialsGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github")
|
||||
.block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2ClientCredentialsGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2ClientCredentialsGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
OAuth2ClientCredentialsGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenPasswordAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testPasswordGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenPasswordAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testPasswordGrant();
|
||||
}
|
||||
|
||||
private void testPasswordGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2PasswordGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook")
|
||||
.block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body("username=user&password=password");
|
||||
this.exchange = MockServerWebExchange.builder(request).build();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2PasswordGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2PasswordGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
OAuth2PasswordGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD);
|
||||
assertThat(grantRequest.getUsername()).isEqualTo("user");
|
||||
assertThat(grantRequest.getPassword()).isEqualTo("password");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenJwtBearerAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testJwtBearerGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenJwtBearerAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testJwtBearerGrant();
|
||||
}
|
||||
|
||||
private void testJwtBearerGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(JwtBearerGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta").block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<JwtBearerGrantRequest> grantRequestCaptor = ArgumentCaptor.forClass(JwtBearerGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
JwtBearerGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
|
||||
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
private void testTokenExchangeGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0").block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(TokenExchangeGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
|
||||
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
|
||||
}
|
||||
|
||||
private static OAuth2AccessToken getExpiredAccessToken() {
|
||||
Instant expiresAt = Instant.now().minusSeconds(60);
|
||||
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
|
||||
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "scopes", issuedAt, expiresAt,
|
||||
new HashSet<>(Arrays.asList("read", "write")));
|
||||
}
|
||||
|
||||
private static Jwt getJwt() {
|
||||
Instant issuedAt = Instant.now();
|
||||
return new Jwt("token", issuedAt, issuedAt.plusSeconds(300),
|
||||
Collections.singletonMap(JoseHeaderNames.ALG, "RS256"),
|
||||
Collections.singletonMap(JwtClaimNames.SUB, "user"));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class MinimalOAuth2ClientConfig extends OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
|
||||
return new WebSessionServerOAuth2AuthorizedClientRepository();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class CustomAuthorizedClientServiceConfig extends OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AuthorizedClientService authorizedClientService() {
|
||||
ReactiveOAuth2AuthorizedClientService authorizedClientService = mock(
|
||||
ReactiveOAuth2AuthorizedClientService.class);
|
||||
given(authorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(Mono.empty());
|
||||
return authorizedClientService;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class CustomAccessTokenResponseClientsConfig extends MinimalOAuth2ClientConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenAccessResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class CustomAuthorizedClientProvidersConfig extends MinimalOAuth2ClientConfig {
|
||||
|
||||
@Bean
|
||||
AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCode() {
|
||||
return spy(new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider());
|
||||
}
|
||||
|
||||
@Bean
|
||||
RefreshTokenReactiveOAuth2AuthorizedClientProvider refreshToken() {
|
||||
RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientCredentialsReactiveOAuth2AuthorizedClientProvider clientCredentials() {
|
||||
ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordReactiveOAuth2AuthorizedClientProvider password() {
|
||||
PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearer() {
|
||||
JwtBearerReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchange() {
|
||||
TokenExchangeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract static class OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||||
// @formatter:off
|
||||
return new InMemoryReactiveClientRegistrationRepository(
|
||||
CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
.clientId("google-client-id")
|
||||
.clientSecret("google-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.build(),
|
||||
CommonOAuth2Provider.GITHUB.getBuilder("github")
|
||||
.clientId("github-client-id")
|
||||
.clientSecret("github-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.build(),
|
||||
CommonOAuth2Provider.FACEBOOK.getBuilder("facebook")
|
||||
.clientId("facebook-client-id")
|
||||
.clientSecret("facebook-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.build(),
|
||||
CommonOAuth2Provider.OKTA.getBuilder("okta")
|
||||
.clientId("okta-client-id")
|
||||
.clientSecret("okta-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
|
||||
.build(),
|
||||
ClientRegistration.withRegistrationId("auth0")
|
||||
.clientName("Auth0")
|
||||
.clientId("auth0-client-id")
|
||||
.clientSecret("auth0-client-secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.scope("user.read", "user.write")
|
||||
.build());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
Consumer<DefaultReactiveOAuth2AuthorizedClientManager> authorizedClientManagerConsumer() {
|
||||
return (authorizedClientManager) -> authorizedClientManager
|
||||
.setContextAttributesMapper((authorizeRequest) -> {
|
||||
ServerWebExchange exchange = Objects
|
||||
.requireNonNull(authorizeRequest.getAttribute(ServerWebExchange.class.getName()));
|
||||
return exchange.getFormData().map((parameters) -> {
|
||||
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||
|
||||
Map<String, Object> attributes = Collections.emptyMap();
|
||||
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
|
||||
attributes = new HashMap<>();
|
||||
attributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
|
||||
attributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
|
||||
implements ReactiveOAuth2AccessTokenResponseClient<T> {
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AccessTokenResponse> getTokenResponse(T grantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(grantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,39 +16,16 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordCheckResult;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordException;
|
||||
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServerHttpSecurityConfiguration}.
|
||||
@@ -60,16 +37,6 @@ public class ServerHttpSecurityConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
WebTestClient webClient;
|
||||
|
||||
@Autowired
|
||||
void setup(ApplicationContext context) {
|
||||
if (!context.containsBean(WebHttpHandlerBuilder.WEB_HANDLER_BEAN_NAME)) {
|
||||
return;
|
||||
}
|
||||
this.webClient = WebTestClient.bindToApplicationContext(context).configureClient().build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadConfigWhenReactiveUserDetailsServiceConfiguredThenServerHttpSecurityExists() {
|
||||
this.spring
|
||||
@@ -90,151 +57,9 @@ public class ServerHttpSecurityConfigurationTests {
|
||||
assertThat(serverHttpSecurity).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordCompromisedThenUnauthorized() {
|
||||
this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
// @formatter:off
|
||||
this.webClient.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().location("/login?error");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordNotCompromisedThenUnauthorized() {
|
||||
this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "admin");
|
||||
data.add("password", "password2");
|
||||
// @formatter:off
|
||||
this.webClient.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().location("/");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToResetPassword() {
|
||||
this.spring
|
||||
.register(FormLoginRedirectToResetPasswordConfig.class, UserDetailsConfig.class,
|
||||
CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
// @formatter:off
|
||||
this.webClient.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().location("/reset-password");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class SubclassConfig extends ServerHttpSecurityConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
static class FormLoginConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchange) -> exchange
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
static class FormLoginRedirectToResetPasswordConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchange) -> exchange
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.formLogin((form) -> form
|
||||
.authenticationFailureHandler((webFilterExchange, exception) -> {
|
||||
String redirectUrl = "/login?error";
|
||||
if (exception instanceof CompromisedPasswordException) {
|
||||
redirectUrl = "/reset-password";
|
||||
}
|
||||
return new DefaultServerRedirectStrategy().sendRedirect(webFilterExchange.getExchange(), URI.create(redirectUrl));
|
||||
})
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class UserDetailsConfig {
|
||||
|
||||
@Bean
|
||||
MapReactiveUserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails user = PasswordEncodedUser.user();
|
||||
UserDetails admin = User.withDefaultPasswordEncoder()
|
||||
.username("admin")
|
||||
.password("password2")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
// @formatter:on
|
||||
return new MapReactiveUserDetailsService(user, admin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CompromisedPasswordCheckerConfig {
|
||||
|
||||
@Bean
|
||||
TestReactivePasswordChecker compromisedPasswordChecker() {
|
||||
return new TestReactivePasswordChecker();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestReactivePasswordChecker implements ReactiveCompromisedPasswordChecker {
|
||||
|
||||
@Override
|
||||
public Mono<CompromisedPasswordCheckResult> check(String password) {
|
||||
if ("password".equals(password)) {
|
||||
return Mono.just(new CompromisedPasswordCheckResult(true));
|
||||
}
|
||||
return Mono.just(new CompromisedPasswordCheckResult(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class XsdDocumentedTests {
|
||||
|
||||
String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd";
|
||||
|
||||
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.3.xsd";
|
||||
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.2.xsd";
|
||||
|
||||
XmlSupport xml = new XmlSupport();
|
||||
|
||||
@@ -151,8 +151,8 @@ public class XsdDocumentedTests {
|
||||
.list((dir, name) -> name.endsWith(".xsd"));
|
||||
// @formatter:on
|
||||
assertThat(schemas.length)
|
||||
.withFailMessage("the count is equal to 25, if not then schemaDocument needs updating")
|
||||
.isEqualTo(25);
|
||||
.withFailMessage("the count is equal to 24, if not then schemaDocument needs updating")
|
||||
.isEqualTo(24);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -49,20 +49,18 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@@ -318,47 +316,6 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
|
||||
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.configLocations(xml("clients")).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.configLocations(xml("providers")).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
private void testTokenExchangeGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
|
||||
.willReturn(accessTokenResponse);
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(HttpServletRequest.class.getName(), this.request)
|
||||
.attribute(HttpServletResponse.class.getName(), this.response)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(TokenExchangeGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
|
||||
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
|
||||
}
|
||||
|
||||
private static OAuth2AccessToken getExpiredAccessToken() {
|
||||
Instant expiresAt = Instant.now().minusSeconds(60);
|
||||
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
|
||||
@@ -399,14 +356,6 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
|
||||
.clientId("okta-client-id")
|
||||
.clientSecret("okta-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
|
||||
.build(),
|
||||
ClientRegistration.withRegistrationId("auth0")
|
||||
.clientName("Auth0")
|
||||
.clientId("auth0-client-id")
|
||||
.clientSecret("auth0-client-secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.scope("user.read", "user.write")
|
||||
.build());
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -429,65 +378,95 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
|
||||
});
|
||||
}
|
||||
|
||||
public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCode() {
|
||||
public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider() {
|
||||
return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider());
|
||||
}
|
||||
|
||||
public static RefreshTokenOAuth2AuthorizedClientProvider refreshToken() {
|
||||
public static RefreshTokenOAuth2AuthorizedClientProvider refreshTokenAuthorizedClientProvider() {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(refreshTokenAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
public static MockRefreshTokenClient refreshTokenAccessTokenResponseClient() {
|
||||
return new MockRefreshTokenClient();
|
||||
}
|
||||
|
||||
public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentials() {
|
||||
public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsAuthorizedClientProvider() {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(clientCredentialsAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockClientCredentialsClient();
|
||||
}
|
||||
|
||||
public static PasswordOAuth2AuthorizedClientProvider password() {
|
||||
public static PasswordOAuth2AuthorizedClientProvider passwordAuthorizedClientProvider() {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(passwordAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockPasswordClient();
|
||||
}
|
||||
|
||||
public static JwtBearerOAuth2AuthorizedClientProvider jwtBearer() {
|
||||
public static JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockJwtBearerClient();
|
||||
}
|
||||
|
||||
public static TokenExchangeOAuth2AuthorizedClientProvider tokenExchange() {
|
||||
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
|
||||
implements OAuth2AccessTokenResponseClient<T> {
|
||||
private static class MockAuthorizationCodeClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) {
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockRefreshTokenClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockClientCredentialsClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockPasswordClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.security.config.saml2;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -24,21 +23,16 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link RelyingPartyRegistrationsBeanDefinitionParser}.
|
||||
@@ -124,7 +118,6 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
// @formatter:on
|
||||
|
||||
@Autowired
|
||||
@Qualifier("registrations")
|
||||
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
@@ -275,19 +268,6 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWhenRelayStateResolverThenUses() {
|
||||
this.spring.configLocations(xml("RelayStateResolver")).autowire();
|
||||
Converter<HttpServletRequest, String> relayStateResolver = this.spring.getContext().getBean(Converter.class);
|
||||
OpenSaml4AuthenticationRequestResolver authenticationRequestResolver = this.spring.getContext()
|
||||
.getBean(OpenSaml4AuthenticationRequestResolver.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setRequestURI("/saml2/authenticate/one");
|
||||
request.setServletPath("/saml2/authenticate/one");
|
||||
authenticationRequestResolver.resolve(request);
|
||||
verify(relayStateResolver).convert(request);
|
||||
}
|
||||
|
||||
private static MockResponse xmlResponse(String xml) {
|
||||
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).setBody(xml);
|
||||
}
|
||||
|
||||
@@ -56,11 +56,9 @@ import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
|
||||
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
|
||||
@@ -594,7 +592,6 @@ public class ServerHttpSecurityTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
|
||||
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
|
||||
@@ -616,11 +613,8 @@ public class ServerHttpSecurityTests {
|
||||
OAuth2LoginAuthenticationWebFilter authenticationWebFilter = getWebFilter(securityFilterChain,
|
||||
OAuth2LoginAuthenticationWebFilter.class)
|
||||
.get();
|
||||
DelegatingServerAuthenticationSuccessHandler handler = (DelegatingServerAuthenticationSuccessHandler) ReflectionTestUtils
|
||||
.getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
List<ServerAuthenticationSuccessHandler> delegates = (List<ServerAuthenticationSuccessHandler>) ReflectionTestUtils
|
||||
.getField(handler, "delegates");
|
||||
assertThat(ReflectionTestUtils.getField(delegates.get(0), "requestCache")).isSameAs(requestCache);
|
||||
Object handler = ReflectionTestUtils.getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
assertThat(ReflectionTestUtils.getField(handler, "requestCache")).isSameAs(requestCache);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,587 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
|
||||
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.SessionLimit;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
|
||||
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;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
public class SessionManagementSpecTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
WebTestClient client;
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext context) {
|
||||
this.client = WebTestClient.bindToApplicationContext(context).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionPreventsLoginThenSecondLoginFails() {
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// second login should fail
|
||||
ResponseCookie secondLoginSessionCookie = this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
assertThat(secondLoginSessionCookie).isNull();
|
||||
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpBasicWhenUsingSavingAuthenticationInWebSessionAndPreventLoginThenSecondRequestFails() {
|
||||
this.spring.register(ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
// first request be successful
|
||||
ResponseCookie sessionCookie = this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
// request with no session should fail
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isUnauthorized();
|
||||
|
||||
// request with session obtained from first request should be successful
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.cookie(sessionCookie.getName(), sessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionPerAuthenticationThenUserLoginFailsAndAdminLoginSucceeds() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginConfig.sessionLimit = (authentication) -> {
|
||||
if (authentication.getName().equals("admin")) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.just(1);
|
||||
};
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
MultiValueMap<String, String> adminCreds = new LinkedMultiValueMap<>();
|
||||
adminCreds.add("username", "admin");
|
||||
adminCreds.add("password", "password");
|
||||
|
||||
ResponseCookie userFirstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie adminFirstLoginSessionCookie = loginReturningCookie(adminCreds);
|
||||
// second user login should fail
|
||||
this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error");
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(userFirstLoginSessionCookie.getName(), userFirstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ResponseCookie adminSecondLoginSessionCookie = loginReturningCookie(adminCreds);
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(adminFirstLoginSessionCookie.getName(), adminFirstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(adminSecondLoginSessionCookie.getName(), adminSecondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(1);
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// first login should not be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
|
||||
// second login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionDoesNotPreventLoginThenLeastRecentlyUsedSessionIsInvalidated() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(2);
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// update last access time for first request
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
ResponseCookie thirdLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// second login should be invalid, it is the least recently used session
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
|
||||
// first login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
// third login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(thirdLoginSessionCookie.getName(), thirdLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void oauth2LoginWhenMaxSessionsThenPreventLogin() {
|
||||
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
|
||||
OAuth2LoginConcurrentSessionsConfig.preventLogin = true;
|
||||
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
|
||||
prepareOAuth2Config();
|
||||
// @formatter:off
|
||||
ResponseCookie sessionCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectHeader().location("/login?error");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(sessionCookie.getName(), sessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("ok");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void oauth2LoginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
|
||||
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
|
||||
OAuth2LoginConcurrentSessionsConfig.preventLogin = false;
|
||||
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
|
||||
prepareOAuth2Config();
|
||||
// @formatter:off
|
||||
ResponseCookie firstLoginCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
ResponseCookie secondLoginCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(firstLoginCookie.getName(), firstLoginCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isFound()
|
||||
.expectHeader().location("/login");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(secondLoginCookie.getName(), secondLoginCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("ok");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenAuthenticationSuccessHandlerOverriddenThenConcurrentSessionHandlersBackOff() {
|
||||
this.spring.register(ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig.class).autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
// first login should be successful
|
||||
login(data).expectStatus().isFound().expectHeader().location("/");
|
||||
// second login should be successful, there should be no concurrent session
|
||||
// control
|
||||
login(data).expectStatus().isFound().expectHeader().location("/");
|
||||
}
|
||||
|
||||
private void prepareOAuth2Config() {
|
||||
OAuth2LoginConcurrentSessionsConfig config = this.spring.getContext()
|
||||
.getBean(OAuth2LoginConcurrentSessionsConfig.class);
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
ReactiveAuthenticationManager manager = config.manager;
|
||||
ServerOAuth2AuthorizationRequestResolver resolver = config.resolver;
|
||||
OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
|
||||
OAuth2User user = TestOAuth2Users.create();
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes();
|
||||
OAuth2LoginAuthenticationToken result = new OAuth2LoginAuthenticationToken(
|
||||
TestClientRegistrations.clientRegistration().build(), exchange, user, user.getAuthorities(),
|
||||
accessToken);
|
||||
given(converter.convert(any())).willReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c")));
|
||||
given(manager.authenticate(any())).willReturn(Mono.just(result));
|
||||
given(resolver.resolve(any())).willReturn(Mono.empty());
|
||||
}
|
||||
|
||||
private ResponseCookie loginReturningCookie(MultiValueMap<String, String> data) {
|
||||
return login(data).expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
}
|
||||
|
||||
private WebTestClient.ResponseSpec login(MultiValueMap<String, String> data) {
|
||||
return this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsMaxSessionPreventsLoginConfig {
|
||||
|
||||
static SessionLimit sessionLimit = SessionLimit.of(1);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(sessionLimit)
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class OAuth2LoginConcurrentSessionsConfig {
|
||||
|
||||
static int maxSessions = 1;
|
||||
|
||||
static boolean preventLogin = true;
|
||||
|
||||
ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class);
|
||||
|
||||
ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
|
||||
|
||||
ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http,
|
||||
DefaultWebSessionManager webSessionManager) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2Login((oauth2Login) -> oauth2Login
|
||||
.authenticationConverter(this.authenticationConverter)
|
||||
.authenticationManager(this.manager)
|
||||
.authorizationRequestResolver(this.resolver)
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(maxSessions))
|
||||
.maximumSessionsExceededHandler(preventLogin
|
||||
? new PreventLoginServerMaximumSessionsExceededHandler()
|
||||
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.getSessionStore()))
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||||
return new InMemoryReactiveClientRegistrationRepository(
|
||||
TestClientRegistrations.clientCredentials().build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsMaxSessionPreventsLoginFalseConfig {
|
||||
|
||||
static SessionLimit sessionLimit = SessionLimit.of(1);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(sessionLimit)
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin((login) -> login
|
||||
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(1))
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.httpBasic((basic) -> basic
|
||||
.securityContextRepository(new WebSessionServerSecurityContextRepository())
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(1))
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ ReactiveAuthenticationTestConfiguration.class, DefaultController.class })
|
||||
static class Config {
|
||||
|
||||
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
|
||||
DefaultWebSessionManager webSessionManager() {
|
||||
return new DefaultWebSessionManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveSessionRegistry reactiveSessionRegistry() {
|
||||
return new InMemoryReactiveSessionRegistry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class DefaultController {
|
||||
|
||||
@GetMapping("/")
|
||||
String index() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user