Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,21 @@ fun getMessages(): List<Message> { }
----
======

[[method-security-has-scope]]
=== Using `hasScope` in Method Security

Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:

include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]

and then doing:

include-code::./MessageService[tag=protected-method,indent=0]

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:

include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]

[[oauth2resourceserver-jwt-authorization-extraction]]
=== Extracting Authorities Manually

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,21 @@ fun getMessages(): List<Message?> {}
----
======

[[method-security-has-scope]]
=== Using `hasScope` in Method Security

Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:

include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]

and then doing:

include-code::./MessageService[tag=protected-method,indent=0]

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:

include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]

[[oauth2resourceserver-opaque-authorization-extraction]]
=== Extracting Authorities Manually

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;


import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
class MessageService {

// tag::protected-method[]
@PreAuthorize("@oauth2.hasScope('message:read')")
String readMessage() {
return "message";
}
// end::protected-method[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;

@Configuration
@EnableMethodSecurity
class MethodSecurityHasScopeConfiguration {
// tag::declare-factory[]
@Bean
OAuth2AuthorizationManagerFactory<?> oauth2() {
return new DefaultOAuth2AuthorizationManagerFactory<>();
}
// end::declare-factory[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;

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.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;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

@ExtendWith(SpringTestContextExtension.class)
@ExtendWith(SpringExtension.class)
@SecurityTestExecutionListeners
public class MethodSecurityHasScopeConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this).mockMvcAfterSpringSecurityOk();

@Autowired
private MessageService messages;

@Test
@WithMockUser(authorities = "SCOPE_message:read")
void readMessageWhenMessageReadThenAllowed() {
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
this.messages.readMessage();
}

@Test
@WithMockUser
void readMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}

@Test
@WithMockUser(authorities = { "SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509" })
void mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
this.messages.readMessage();
}

@Test
@WithMockUser(authorities = { "SCOPE_message:read" })
void mfaReadMessageWhenMessageReadThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}

@Test
@WithMockUser
void mfaReadMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;

@Configuration
@EnableMethodSecurity
@EnableMultiFactorAuthentication(authorities = { "FACTOR_BEARER", "FACTOR_X509" })
class MethodSecurityHasScopeMfaConfiguration {
// tag::declare-factory[]
@Bean
OAuth2AuthorizationManagerFactory<?> oauth2(AuthorizationManagerFactory<?> authz) {
return new DefaultOAuth2AuthorizationManagerFactory<>(authz);
}
// end::declare-factory[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope

import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Service


@Service
open class MessageService {
// tag::protected-method[]
@PreAuthorize("@oauth2.hasScope('message:read')")
open fun readMessage(): String {
return "message"
}
// end::protected-method[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory

@Configuration
@EnableMethodSecurity
open class MethodSecurityHasScopeConfiguration {
// tag::declare-factory[]
@Bean
open fun oauth2(): OAuth2AuthorizationManagerFactory<Any> {
return DefaultOAuth2AuthorizationManagerFactory()
}
// end::declare-factory[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope

import org.assertj.core.api.Assertions
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.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(SpringTestContextExtension::class)
@ExtendWith(SpringExtension::class)
@SecurityTestExecutionListeners
class MethodSecurityHasScopeConfigurationTests {
@JvmField
val spring: SpringTestContext = SpringTestContext(this).mockMvcAfterSpringSecurityOk()

@Autowired
var messages: MessageService? = null

@Test
@WithMockUser(authorities = ["SCOPE_message:read"])
fun readMessageWhenMessageReadThenAllowed() {
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
this.messages!!.readMessage()
}

@Test
@WithMockUser
fun readMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}

@Test
@WithMockUser(authorities = ["SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509"])
fun mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
this.messages!!.readMessage()
}

@Test
@WithMockUser(authorities = ["SCOPE_message:read"])
fun mfaReadMessageWhenMessageReadThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}

@Test
@WithMockUser
fun mfaReadMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authorization.AuthorizationManagerFactory
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory

@Configuration
@EnableMethodSecurity
@EnableMultiFactorAuthentication(authorities = ["FACTOR_BEARER", "FACTOR_X509"])
open class MethodSecurityHasScopeMfaConfiguration {
// tag::declare-factory[]
@Bean
open fun oauth2(authz: AuthorizationManagerFactory<Any>): OAuth2AuthorizationManagerFactory<Any> {
return DefaultOAuth2AuthorizationManagerFactory(authz)
} // end::declare-factory[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2025-present 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.oauth2.core.authorization;

import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.util.Assert;

/**
* A factory for creating different kinds of {@link AuthorizationManager} instances.
*
* @param <T> the type of object that the authorization check is being done on
* @author Ngoc Nhan
* @since 7.1
*/
public final class DefaultOAuth2AuthorizationManagerFactory<T> implements OAuth2AuthorizationManagerFactory<T> {

private String scopePrefix = "SCOPE_";

private final AuthorizationManagerFactory<T> authorizationManagerFactory;

public DefaultOAuth2AuthorizationManagerFactory() {
this(new DefaultAuthorizationManagerFactory<>());
}

public DefaultOAuth2AuthorizationManagerFactory(AuthorizationManagerFactory<T> authorizationManagerFactory) {
Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory can not be null");
this.authorizationManagerFactory = authorizationManagerFactory;
}

/**
* Sets the prefix used to create an authority name from a scope name. Can be an empty
* string.
* @param scopePrefix the scope prefix to use
*/
public void setScopePrefix(String scopePrefix) {
Assert.notNull(scopePrefix, "scopePrefix can not be null");
this.scopePrefix = scopePrefix;
}

@Override
public AuthorizationManager<T> hasScope(String scope) {
Assert.notNull(scope, "scope can not be null");
assertScope(scope);
return this.authorizationManagerFactory.hasAuthority(this.scopePrefix + scope);
}

@Override
public AuthorizationManager<T> hasAnyScope(String... scopes) {
return this.authorizationManagerFactory.hasAnyAuthority(this.mappedScopes(scopes));
}

@Override
public AuthorizationManager<T> hasAllScopes(String... scopes) {
return this.authorizationManagerFactory.hasAllAuthorities(this.mappedScopes(scopes));
}

private String[] mappedScopes(String... scopes) {
Assert.notNull(scopes, "scopes can not be null");
String[] mappedScopes = new String[scopes.length];
for (int i = 0; i < scopes.length; i++) {
assertScope(scopes[i]);
mappedScopes[i] = this.scopePrefix + scopes[i];
}
return mappedScopes;
}

private void assertScope(String scope) {
Assert.isTrue(!scope.startsWith(this.scopePrefix), () -> scope + " should not start with '" + this.scopePrefix
+ "' since '" + this.scopePrefix
+ "' is automatically prepended when using hasScope and hasAnyScope. Consider using AuthorizationManagerFactory#hasAuthority or #hasAnyAuthority instead.");
}

}
Loading
Loading