Skip to content

Commit 705fa60

Browse files
committed
Document Method Security hasScope Support
Issue gh-18013 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
1 parent f2b7cb2 commit 705fa60

10 files changed

Lines changed: 258 additions & 0 deletions

File tree

docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,21 @@ fun getMessages(): List<Message> { }
949949
----
950950
======
951951

952+
[[method-security-has-scope]]
953+
=== Using `hasScope` in Method Security
954+
955+
Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:
956+
957+
include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]
958+
959+
and then doing:
960+
961+
include-code::./MessageService[tag=protected-method,indent=0]
962+
963+
If you are using xref:servlet/authentication/mfa.adoc[Spring Security's MFA feature], then you can supply its `AuthorizationManagerFactory` instance to ensure that your authentication factors are automatically checked as well by including it in your `DefaultOAuth2AuthorizationManagerFactory` constructor as follows:
964+
965+
include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]
966+
952967
[[oauth2resourceserver-jwt-authorization-extraction]]
953968
=== Extracting Authorities Manually
954969

docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,21 @@ fun getMessages(): List<Message?> {}
638638
----
639639
======
640640

641+
[[method-security-has-scope]]
642+
=== Using `hasScope` in Method Security
643+
644+
Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:
645+
646+
include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]
647+
648+
and then doing:
649+
650+
include-code::./MessageService[tag=protected-method,indent=0]
651+
652+
If you are using xref:servlet/authentication/mfa.adoc[Spring Security's MFA feature], then you can supply its `AuthorizationManagerFactory` instance to ensure that your authentication factors are automatically checked as well by including it in your `DefaultOAuth2AuthorizationManagerFactory` constructor as follows:
653+
654+
include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]
655+
641656
[[oauth2resourceserver-opaque-authorization-extraction]]
642657
=== Extracting Authorities Manually
643658

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
2+
3+
4+
import org.springframework.security.access.prepost.PreAuthorize;
5+
import org.springframework.stereotype.Service;
6+
7+
@Service
8+
class MessageService {
9+
10+
// tag::protected-method[]
11+
@PreAuthorize("@oauth2.hasScope('message:read')")
12+
String readMessage() {
13+
return "message";
14+
}
15+
// end::protected-method[]
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
6+
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
7+
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;
8+
9+
@Configuration
10+
@EnableMethodSecurity
11+
class MethodSecurityHasScopeConfiguration {
12+
// tag::declare-factory[]
13+
@Bean
14+
OAuth2AuthorizationManagerFactory<?> oauth2() {
15+
return new DefaultOAuth2AuthorizationManagerFactory<>();
16+
}
17+
// end::declare-factory[]
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.extension.ExtendWith;
5+
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.security.access.AccessDeniedException;
8+
import org.springframework.security.config.test.SpringTestContext;
9+
import org.springframework.security.config.test.SpringTestContextExtension;
10+
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
11+
import org.springframework.security.test.context.support.WithMockUser;
12+
import org.springframework.test.context.junit.jupiter.SpringExtension;
13+
14+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
15+
16+
@ExtendWith(SpringTestContextExtension.class)
17+
@ExtendWith(SpringExtension.class)
18+
@SecurityTestExecutionListeners
19+
public class MethodSecurityHasScopeConfigurationTests {
20+
public final SpringTestContext spring = new SpringTestContext(this).mockMvcAfterSpringSecurityOk();
21+
22+
@Autowired
23+
private MessageService messages;
24+
25+
@Test
26+
@WithMockUser(authorities = "SCOPE_message:read")
27+
void readMessageWhenMessageReadThenAllowed() {
28+
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
29+
this.messages.readMessage();
30+
}
31+
32+
@Test
33+
@WithMockUser
34+
void readMessageWhenNoScopeThenDenied() {
35+
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
36+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
37+
}
38+
39+
@Test
40+
@WithMockUser(authorities = { "SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509" })
41+
void mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
42+
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
43+
this.messages.readMessage();
44+
}
45+
46+
@Test
47+
@WithMockUser(authorities = { "SCOPE_message:read" })
48+
void mfaReadMessageWhenMessageReadThenDenied() {
49+
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
50+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
51+
}
52+
53+
@Test
54+
@WithMockUser
55+
void mfaReadMessageWhenNoScopeThenDenied() {
56+
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
57+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.authorization.AuthorizationManagerFactory;
6+
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
7+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
8+
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
9+
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;
10+
11+
@Configuration
12+
@EnableMethodSecurity
13+
@EnableMultiFactorAuthentication(authorities = { "FACTOR_BEARER", "FACTOR_X509" })
14+
class MethodSecurityHasScopeMfaConfiguration {
15+
// tag::declare-factory[]
16+
@Bean
17+
OAuth2AuthorizationManagerFactory<?> oauth2(AuthorizationManagerFactory<?> authz) {
18+
return new DefaultOAuth2AuthorizationManagerFactory<>(authz);
19+
}
20+
// end::declare-factory[]
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
2+
3+
import org.springframework.security.access.prepost.PreAuthorize
4+
import org.springframework.stereotype.Service
5+
6+
7+
@Service
8+
open class MessageService {
9+
// tag::protected-method[]
10+
@PreAuthorize("@oauth2.hasScope('message:read')")
11+
open fun readMessage(): String {
12+
return "message"
13+
}
14+
// end::protected-method[]
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
2+
3+
import org.springframework.context.annotation.Bean
4+
import org.springframework.context.annotation.Configuration
5+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
6+
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
7+
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory
8+
9+
@Configuration
10+
@EnableMethodSecurity
11+
open class MethodSecurityHasScopeConfiguration {
12+
// tag::declare-factory[]
13+
@Bean
14+
open fun oauth2(): OAuth2AuthorizationManagerFactory<Any> {
15+
return DefaultOAuth2AuthorizationManagerFactory()
16+
}
17+
// end::declare-factory[]
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
2+
3+
import org.assertj.core.api.Assertions
4+
import org.junit.jupiter.api.Test
5+
import org.junit.jupiter.api.extension.ExtendWith
6+
import org.springframework.beans.factory.annotation.Autowired
7+
import org.springframework.security.access.AccessDeniedException
8+
import org.springframework.security.config.test.SpringTestContext
9+
import org.springframework.security.config.test.SpringTestContextExtension
10+
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners
11+
import org.springframework.security.test.context.support.WithMockUser
12+
import org.springframework.test.context.junit.jupiter.SpringExtension
13+
14+
@ExtendWith(SpringTestContextExtension::class)
15+
@ExtendWith(SpringExtension::class)
16+
@SecurityTestExecutionListeners
17+
class MethodSecurityHasScopeConfigurationTests {
18+
@JvmField
19+
val spring: SpringTestContext = SpringTestContext(this).mockMvcAfterSpringSecurityOk()
20+
21+
@Autowired
22+
var messages: MessageService? = null
23+
24+
@Test
25+
@WithMockUser(authorities = ["SCOPE_message:read"])
26+
fun readMessageWhenMessageReadThenAllowed() {
27+
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
28+
this.messages!!.readMessage()
29+
}
30+
31+
@Test
32+
@WithMockUser
33+
fun readMessageWhenNoScopeThenDenied() {
34+
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
35+
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
36+
.isThrownBy({ this.messages!!.readMessage() })
37+
}
38+
39+
@Test
40+
@WithMockUser(authorities = ["SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509"])
41+
fun mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
42+
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
43+
this.messages!!.readMessage()
44+
}
45+
46+
@Test
47+
@WithMockUser(authorities = ["SCOPE_message:read"])
48+
fun mfaReadMessageWhenMessageReadThenDenied() {
49+
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
50+
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
51+
.isThrownBy({ this.messages!!.readMessage() })
52+
}
53+
54+
@Test
55+
@WithMockUser
56+
fun mfaReadMessageWhenNoScopeThenDenied() {
57+
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
58+
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
59+
.isThrownBy({ this.messages!!.readMessage() })
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
2+
3+
import org.springframework.context.annotation.Bean
4+
import org.springframework.context.annotation.Configuration
5+
import org.springframework.security.authorization.AuthorizationManagerFactory
6+
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
7+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
8+
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
9+
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory
10+
11+
@Configuration
12+
@EnableMethodSecurity
13+
@EnableMultiFactorAuthentication(authorities = ["FACTOR_BEARER", "FACTOR_X509"])
14+
open class MethodSecurityHasScopeMfaConfiguration {
15+
// tag::declare-factory[]
16+
@Bean
17+
open fun oauth2(authz: AuthorizationManagerFactory<Any>): OAuth2AuthorizationManagerFactory<Any> {
18+
return DefaultOAuth2AuthorizationManagerFactory(authz)
19+
} // end::declare-factory[]
20+
}

0 commit comments

Comments
 (0)