Polish Method Authorization Denied Handling
- Renamed @AuthorizationDeniedHandler to @HandleAuthorizationDenied - Merged the post processor interface into MethodAuthorizationDeniedHandler , it now has two methods handleDeniedInvocation and handleDeniedInvocationResult - @HandleAuthorizationDenied now handles AuthorizationDeniedException thrown from the method Issue gh-14601
This commit is contained in:
@@ -2256,14 +2256,13 @@ You can also add the Spring Boot property `spring.jackson.default-property-inclu
|
||||
[[fallback-values-authorization-denied]]
|
||||
== Providing Fallback Values When Authorization is Denied
|
||||
|
||||
There are some scenarios where you may not wish to throw an `AccessDeniedException` when a method is invoked without the required permissions.
|
||||
Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where access denied happened before invoking the method.
|
||||
There are some scenarios where you may not wish to throw an `AuthorizationDeniedException` when a method is invoked without the required permissions.
|
||||
Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where authorization denied happened before invoking the method.
|
||||
|
||||
Spring Security provides support for handling and post-processing method access denied by combining {security-api-url}org/springframework/security/authorization/method/AuthorizationDeniedHandler.html[`@AuthorizationDeniedHandler`] with the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> respectively.
|
||||
Spring Security provides support for handling authorization denied on method invocation by using the {security-api-url}org/springframework/security/authorization/method/HandleAuthorizationDenied.html[`@HandleAuthorizationDenied`].
|
||||
The handler works for denied authorizations that happened in the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> as well as {security-api-url}org/springframework/security/authorization/AuthorizationDeniedException.html[`AuthorizationDeniedException`] thrown from the method invocation itself.
|
||||
|
||||
=== Using with `@PreAuthorize`
|
||||
|
||||
Let's consider the example from the <<authorize-object,previous section>>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@AuthorizationDeniedHandler`:
|
||||
Let's consider the example from the <<authorize-object,previous section>>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@HandleAuthorizationDenied`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -2274,7 +2273,7 @@ Java::
|
||||
public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
|
||||
|
||||
@Override
|
||||
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2295,7 +2294,7 @@ public class User {
|
||||
// ...
|
||||
|
||||
@PreAuthorize(value = "hasAuthority('user:read')")
|
||||
@AuthorizationDeniedHandler(handlerClass = NullMethodAuthorizationDeniedHandler.class)
|
||||
@HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
|
||||
public String getEmail() {
|
||||
return this.email;
|
||||
}
|
||||
@@ -2308,7 +2307,7 @@ Kotlin::
|
||||
----
|
||||
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { <1>
|
||||
|
||||
override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
||||
override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2325,13 +2324,13 @@ class SecurityConfig {
|
||||
|
||||
}
|
||||
|
||||
class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3>
|
||||
class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3>
|
||||
----
|
||||
======
|
||||
|
||||
<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a `null` value
|
||||
<2> Register the `NullMethodAuthorizationDeniedHandler` as a bean
|
||||
<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
|
||||
<3> Annotate the method with `@HandleAuthorizationDenied` and pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
|
||||
|
||||
And then you can verify that a `null` value is returned instead of the `AccessDeniedException`:
|
||||
|
||||
@@ -2371,9 +2370,12 @@ fun getEmailWhenProxiedThenNullEmail() {
|
||||
----
|
||||
======
|
||||
|
||||
=== Using with `@PostAuthorize`
|
||||
=== Using the Denied Result From the Method Invocation
|
||||
|
||||
The same can be achieved with `@PostAuthorize`, however, since `@PostAuthorize` checks are performed after the method is invoked, we have access to the resulting value of the invocation, allowing you to provide fallback values based on the unauthorized results.
|
||||
There are some scenarios where you might want to return a secure result derived from the denied result.
|
||||
For example, if a user is not authorized to see email addresses, you might want to apply some masking on the original email address, i.e. _useremail@example.com_ would become _use\\******@example.com_.
|
||||
|
||||
For those scenarios, you can override the `handleDeniedInvocationResult` from the `MethodAuthorizationDeniedHandler`, which has the {security-api-url}org/springframework/security/authorization/method/MethodInvocationResult.html[`MethodInvocationResult`] as an argument.
|
||||
Let's continue with the previous example, but instead of returning `null`, we will return a masked value of the email:
|
||||
|
||||
[tabs]
|
||||
@@ -2382,10 +2384,15 @@ Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class EmailMaskingMethodAuthorizationDeniedPostProcessor implements MethodAuthorizationDeniedPostProcessor { <1>
|
||||
public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
|
||||
|
||||
@Override
|
||||
public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
|
||||
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*");
|
||||
}
|
||||
@@ -2397,8 +2404,8 @@ public class EmailMaskingMethodAuthorizationDeniedPostProcessor implements Metho
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean <2>
|
||||
public EmailMaskingMethodAuthorizationDeniedPostProcessor emailMaskingMethodAuthorizationDeniedPostProcessor() {
|
||||
return new EmailMaskingMethodAuthorizationDeniedPostProcessor();
|
||||
public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() {
|
||||
return new EmailMaskingMethodAuthorizationDeniedHandler();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2407,7 +2414,7 @@ public class User {
|
||||
// ...
|
||||
|
||||
@PostAuthorize(value = "hasAuthority('user:read')")
|
||||
@AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor.class)
|
||||
@HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class)
|
||||
public String getEmail() {
|
||||
return this.email;
|
||||
}
|
||||
@@ -2418,9 +2425,13 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class EmailMaskingMethodAuthorizationDeniedPostProcessor : MethodAuthorizationDeniedPostProcessor {
|
||||
class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler {
|
||||
|
||||
override fun postProcessResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
|
||||
override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
||||
return "***"
|
||||
}
|
||||
|
||||
override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
|
||||
val email = methodInvocationResult.result as String
|
||||
return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
|
||||
}
|
||||
@@ -2432,22 +2443,27 @@ class EmailMaskingMethodAuthorizationDeniedPostProcessor : MethodAuthorizationDe
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun emailMaskingMethodAuthorizationDeniedPostProcessor(): EmailMaskingMethodAuthorizationDeniedPostProcessor {
|
||||
return EmailMaskingMethodAuthorizationDeniedPostProcessor()
|
||||
fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler {
|
||||
return EmailMaskingMethodAuthorizationDeniedHandler()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor::class) val email:String) <3>
|
||||
class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) <3>
|
||||
----
|
||||
======
|
||||
|
||||
<1> Create an implementation of `MethodAuthorizationDeniedPostProcessor` that returns a masked value of the unauthorized result value
|
||||
<2> Register the `EmailMaskingMethodAuthorizationDeniedPostProcessor` as a bean
|
||||
<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `EmailMaskingMethodAuthorizationDeniedPostProcessor` to the `postProcessorClass` attribute
|
||||
<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a masked value of the unauthorized result value
|
||||
<2> Register the `EmailMaskingMethodAuthorizationDeniedHandler` as a bean
|
||||
<3> Annotate the method with `@HandleAuthorizationDenied` and pass the `EmailMaskingMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
|
||||
|
||||
And then you can verify that a masked email is returned instead of an `AccessDeniedException`:
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Since you have access to the original denied value, make sure that you correctly handle it and do not return it to the caller.
|
||||
====
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
@@ -2481,20 +2497,20 @@ fun getEmailWhenProxiedThenMaskedEmail() {
|
||||
----
|
||||
======
|
||||
|
||||
When implementing the `MethodAuthorizationDeniedHandler` or the `MethodAuthorizationDeniedPostProcessor` you have a few options on what you can return:
|
||||
When implementing the `MethodAuthorizationDeniedHandler` you have a few options on what type you can return:
|
||||
|
||||
- A `null` value.
|
||||
- A non-null value, respecting the method's return type.
|
||||
- Throw an exception, usually an instance of `AccessDeniedException`. This is the default behavior.
|
||||
- Throw an exception, usually an instance of `AuthorizationDeniedException`. This is the default behavior.
|
||||
- A `Mono` type for reactive applications.
|
||||
|
||||
Note that since the handler and the post-processor must be registered as beans, you can inject dependencies into them if you need a more complex logic.
|
||||
Note that since the handler must be registered as beans in your application context, you can inject dependencies into them if you need a more complex logic.
|
||||
In addition to that, you have available the `MethodInvocation` or the `MethodInvocationResult`, as well as the `AuthorizationResult` for more details related to the authorization decision.
|
||||
|
||||
[[deciding-return-based-parameters]]
|
||||
=== Deciding What to Return Based on Available Parameters
|
||||
|
||||
Consider a scenario where there might be multiple mask values for different methods, it would be not so productive if we had to create a handler or post-processor for each of those methods, although it is perfectly fine to do that.
|
||||
Consider a scenario where there might be multiple mask values for different methods, it would be not so productive if we had to create a handler for each of those methods, although it is perfectly fine to do that.
|
||||
In such cases, we can use the information passed via parameters to decide what to do.
|
||||
For example, we can create a custom `@Mask` annotation and a handler that detects that annotation to decide what mask value to return:
|
||||
|
||||
@@ -2517,7 +2533,7 @@ public @interface Mask {
|
||||
public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
|
||||
return mask.value();
|
||||
}
|
||||
@@ -2539,14 +2555,14 @@ public class SecurityConfig {
|
||||
public class MyService {
|
||||
|
||||
@PreAuthorize(value = "hasAuthority('user:read')")
|
||||
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
|
||||
@Mask("***")
|
||||
public String foo() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
@PreAuthorize(value = "hasAuthority('user:read')")
|
||||
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
|
||||
@Mask("???")
|
||||
public String bar() {
|
||||
return "bar";
|
||||
@@ -2567,7 +2583,7 @@ annotation class Mask(val value: String)
|
||||
|
||||
class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {
|
||||
|
||||
override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
||||
override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
||||
val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
|
||||
return mask.value
|
||||
}
|
||||
@@ -2589,14 +2605,14 @@ class SecurityConfig {
|
||||
class MyService {
|
||||
|
||||
@PreAuthorize(value = "hasAuthority('user:read')")
|
||||
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
|
||||
@Mask("***")
|
||||
fun foo(): String {
|
||||
return "foo"
|
||||
}
|
||||
|
||||
@PreAuthorize(value = "hasAuthority('user:read')")
|
||||
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
|
||||
@Mask("???")
|
||||
fun bar(): String {
|
||||
return "bar"
|
||||
@@ -2653,8 +2669,8 @@ fun barWhenDeniedThenReturnQuestionMarks() {
|
||||
|
||||
=== Combining with Meta Annotation Support
|
||||
|
||||
You can also combine the `@AuthorizationDeniedHandler` with other annotations in order to reduce and simplify the annotations in a method.
|
||||
Let's consider the <<deciding-return-based-parameters,example from the previous section>> and merge `@AuthorizationDeniedHandler` with `@Mask`:
|
||||
You can also combine the `@HandleAuthorizationDenied` with other annotations in order to reduce and simplify the annotations in a method.
|
||||
Let's consider the <<deciding-return-based-parameters,example from the previous section>> and merge `@HandleAuthorizationDenied` with `@Mask`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -2664,7 +2680,7 @@ Java::
|
||||
----
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
|
||||
public @interface Mask {
|
||||
|
||||
String value();
|
||||
@@ -2683,7 +2699,7 @@ Kotlin::
|
||||
----
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
|
||||
annotation class Mask(val value: String)
|
||||
|
||||
@Mask("***")
|
||||
|
||||
Reference in New Issue
Block a user