eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Course – LSS – NPI (cat=Spring Security)
announcement - icon

If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:

>> LEARN SPRING SECURITY

1. Introduction

In this tutorial, we’ll show how to extend the Spring Authorization Server to handle dynamic OAuth 2.0 scopes, which are required in advanced authentication/authorization use cases.

2. When to Use Dynamic Scopes?

OAuth2-based applications use scopes to request access tokens with a defined set of privileges, like reading fields from a user’s profile, performing an automation task, and so on. Usually, the scopes are fixed and known in advance both by the authorization server and the client application. In some scenarios, however, this is not the case.

For instance, suppose your client application needs an access token to execute a single operation, like a transfer request or updating sensitive information. One option is to implement a custom authorization scheme between the client and the resource server, but we’d be reinventing the wheel. From a security perspective, this is also not ideal, since it implies a bigger surface attack, as those protocols would need to be implemented across every resource server and client.

This is where dynamic scopes come into play. In a nutshell, a dynamic scope is one that a client can request, but it’s not known beforehand by the authorization server.

Usually, a dynamic scope requires some sort of validation service to exist, so it can be validated. In practical terms, this requires a consent repository to store the requested scope, which will be used to validate it and, if the end user grants it, to update its status.

When the client receives the access token, it will have the dynamic scope associated with it, allowing it to complete the desired operation.

3. Dynamic Scopes in Spring Authorization Server

The Spring Authorization Server, true to its roots, has a modular architecture, which allows us to replace many of its components with custom ones. We’ve already explored this characteristic to implement multitenancy support, a feature not available by default. In our case, these are the aspects we need to modify:

  • Scope validation logic
  • Consent Validation
  • Consent page: optional, but usually required

Moreover, it’s a good idea to separate the Spring-specific code from the actual scope validation logic. We’ll create a DynamicScopeService for that purpose, leaving to a @Configuration class the task of creating an adapter between the expected method’s signature and the service.

Now, let’s move to the implementation.

4. Project Setup

Dynamic Scopes don’t need any extra dependencies besides those needed by the Spring Authorization Server itself:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-oauth2-authorization-server</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-oauth2-authorization-server-test</artifactId>
    <version>4.1.0</version>
    <scope>test</scope>
</dependency>

The latest versions of these dependencies are available on Maven Central:

5. Security Configuration

The Spring Authorization Server needs two security filter chains to operate:

  • one for handling OAuth/OIDC protocol endpoints
  • a “catch-all” chain that handles everything else, including user authentication

As usual in Spring Security, the built-in ones are only created when there are no application-defined chains available. In our case, we need to tweak just the first one, but by doing so, the auto-configuration for the catch-all chain will be disabled, so we’ll need to provide both.

5.1. Authorization Server Chain

This is the modified chain where we’ll add our customization logic:

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) {

    http.oauth2AuthorizationServer(authorizationServer -> {
        http.securityMatcher(authorizationServer.getEndpointsMatcher());
        authorizationServer
          .oidc(withDefaults())
          .authorizationEndpoint(ap -> {
              ap.consentPage("/consent");
              ap.authenticationProviders(providers -> {
                  providers.stream()
                    .filter(OAuth2AuthorizationCodeRequestAuthenticationProvider.class::isInstance)
                    .map(p -> (OAuth2AuthorizationCodeRequestAuthenticationProvider)p)
                    .findFirst()
                    .ifPresent(p -> {
                        p.setAuthenticationValidator(dynamicScopesAuthenticationValidator());
                        p.setAuthorizationConsentRequired(dynamicScopesConsentValidator());
                    });
              });
          });
    });
    http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated());
    http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()));
    http.exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor(
      new LoginUrlAuthenticationEntryPoint("/login"), createRequestMatcher()));
    return http.build();
}

The scope validation takes place at the OAuth2AuthorizationCodeRequestAuthenticationProvider, which uses two collaborating objects to perform its task:

  • An AuthenticationValidator to validate the submitted parameters
  • A Predicate that decides whether to ask for consent

Since the authorization server uses multiple AuthenticationProviders in its implementation, we need to go through all of them until we find the proper one. All this happens in the authorizationEndpointCustomizer, which exposes the list of authentication providers.

The same customizer also allows us to set the URI for a custom consent page, which will be “/consent” in our case. We’ll come back to the consent page later in this tutorial.

5.2. “Catch-All” Chain

This chain, which has no securityMatcher() call in its DSL, will apply the following rules to incoming requests:

  • Any request must be authenticated
  • When receiving a non-authenticated request, use form-based login with default settings
@Bean
@Order(SecurityFilterProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) {
    http.authorizeHttpRequests(authorize -> {
        authorize.anyRequest().authenticated();
      })
      .formLogin(withDefaults());
    return http.build();
}

Here, we’re just replicating the default configuration. In a real-world scenario, we’d probably want to change some aspects of its properties. For instance, this is where we’d provide the URI for a custom login page, enable two-factor authentication, or add support for a federated identity provider.

6. Scope Validation

The dynamicScopesAuthenticationValidator() method creates an adapter that isolates the Spring Security aspects of the scope validation from the business logic.

In our tutorial, we’ll assume that the dynamic scope will be a string that starts with the “TX:” prefix, followed by a unique transaction identifier. When a client needs an access token with this scope, it must add it to the scope request parameter of an authentication request, as in this example:

https://idp/oauth2/authorize?scope=openid TX:1234&...other params omitted

The authentication provider will perform the standard validation steps and, eventually, will call our adapter to perform the validation logic:

private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> dynamicScopesAuthenticationValidator() {
    return ctx -> {
        OAuth2AuthorizationCodeRequestAuthenticationToken auth = ctx.getAuthentication();
        var requestedScopes = new HashSet<>(auth.getScopes());
        if ( requestedScopes.isEmpty() ) {
            return;
        }

        var registeredClient = ctx.getRegisteredClient();
        var allowedScopes = registeredClient.getScopes();
        requestedScopes.removeIf(allowedScopes::contains);
        if (requestedScopes.isEmpty() ) {
            return;
        }

        // Now, let's validate the remaining scopes using the provided validation service
        try {
            if (!dynamicScopeService.validate(registeredClient.getId(), requestedScopes)) {
                throw new OAuth2AuthorizationCodeRequestAuthenticationException(
                  new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE),
                  auth
                );
            }
        } catch (Exception ex) {
            throw new OAuth2AuthorizationCodeRequestAuthenticationException(
              new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR), 
              ex, 
              auth
            );
        }
    };
}

The adapter receives an OAuth2AuthorizationCodeRequestAuthenticationContext instance that contains information about the authorization request. Notice that, at this point, the end user is not yet known, so we can’t use it as part of this logic.

These are the steps performed by the adapter:

  • Firstly, recover the requested scopes and client information from the context.
  • Secondly, remove any static scopes from the list.
  • If there are no dynamic scopes, return immediately
  • Otherwise, pass the client identifier and dynamic scopes to the validation service

The validation service’s job is to return just a yes/no answer depending on whether the request should be authorized. Our implementation, available online, just checks for a valid pattern. A real-world one would perform more complex validation, such as querying a database and applying any rules required by the specific use case.

The other tweak we’ve made to support dynamic scopes is to add custom logic to determine whether the authorization needs to ask for user consent before issuing an access token. This was done using the AuthenticationProvider’s setAuthorizationConsentRequentid() method, which accepts a Predicate argument.

This Predicate should return true when an authorization request needs user consent, and false otherwise. As we’ve done in the scope validation case, we’ll use an adapter to extract the client’s identifier and requested scopes from the OAuth2AuthorizationCodeRequestAuthenticationContext instance passed to the Predicate. Once the basic checks are done, we invoke the validation service to get a final decision about the consent:

private Predicate<OAuth2AuthorizationCodeRequestAuthenticationContext> dynamicScopesConsentValidator() {
    return ctx -> {
        var previousConsent = ctx.getAuthorizationConsent();
        if ( previousConsent == null ) {
            // First consent, so consent is required
            return true;
        }

        OAuth2AuthorizationCodeRequestAuthenticationToken auth = ctx.getAuthentication();
        var requestedScopes = new HashSet<>(auth.getScopes());
        if ( requestedScopes.isEmpty() ) {
            return false;
        }

        // Remove already consented scopes
        var alreadyConsented = new HashSet<>(previousConsent .getScopes());		
        requestedScopes.removeIf(alreadyConsented::contains);
        if (requestedScopes.isEmpty() ) {
            return false;
        }

        // Any remaining scopes are dynamic scopes or static ones with no previous consent
        return dynamicScopeService.isConsentNeeded(ctx.getRegisteredClient().getId(), requestedScopes);
    };
}

A key distinction of this validation is that it happens after a successful authentication. This means that the business logic can use any of the standard Spring Security methods to get information about the current user and use it as part of the decision logic.

This page will be displayed to the user after a successful authentication, right before issuing an authorization code to a client application.

From the user agent’s perspective, this page will be accessed through a redirect from the login page. The Location header will point to the configured URI along with the following query parameters:

  • client_id: Client application’s identifier
  • scope: Requested scopes
  • state: Server-side state associated with the current authorization request.

Please note that the state parameter in this case has no relation to the one with the same name that’s included in the initial authorization URL created by the client!

Let’s implement a simple controller that takes these parameters, prepares a Model, and forwards the request to a view:

@Controller
public class ConsentController {

    private final RegisteredClientRepository registeredClientRepository;
    private final OAuth2AuthorizationConsentService authorizationConsentService;

    // ... constructor omitted

    @GetMapping("/consent")
    public String consent(Principal principal, Model model,
      @RequestParam(name = OAuth2ParameterNames.CLIENT_ID) String clientId,
      @RequestParam(name = OAuth2ParameterNames.SCOPE) String scope,
      @RequestParam(name = OAuth2ParameterNames.STATE) String state) {

        var client = registeredClientRepository.findByClientId(clientId);
        assert client != null;
        var currentConsent =  authorizationConsentService.findById(client.getId(), principal.getName());
        Set<String> authorizedScopes = currentConsent != null ? currentConsent.getScopes() : Set.of();

        // Remove already authorized scopes from the requested scopes and the special 'openid' scope.
        var neededScopes = Set.of(scope.split(" ")).stream()
          .filter(s -> !authorizedScopes.contains(s) && !OidcScopes.OPENID.equals(s))
          .toList();

        model.addAttribute("clientId", clientId);
        model.addAttribute(
          "clientName", 
          client.getClientName() != null ? client.getClientName() : client.getClientId()
        );
        model.addAttribute("scopes", neededScopes);
        model.addAttribute("state", state);
        model.addAttribute("authorizedScopes", authorizedScopes);

        return "consent";
   }
}

We’ve injected two services in this controller to look for additional information needed to build the view:

  • registeredClientRepository: Allow us to retrieve detailed information for the client, given its identifier
  • authorizationConsentService: Returns an OAuthAuthorizationConsent containing all consents given to a client by a user

We use those services to build a list of the newly required consents and store it in a Spring MVC Model.

Here, we could also query a business service to enrich the Model with details related to any requested dynamic scope. For instance, if our dynamic scope represents a transfer request, we could look up its details and include them in the model.

The view itself uses a Thymeleaf template to display the consent request and present to the user an option to accept or deny it. Note that the form’s submit target is, by default, protected with a CSRF token, so make sure to use the th:action=”@{/path}” syntax. Thanks to Thymeleaf’s Spring integration,  this will automatically include a hidden input field with the correct CSRF token value.

Last, but not least, note that the form’s action target should point to the server’s OAuth2 authorization endpoint. Later, if we decide to change this URI from the default value, we must make sure that this form points to the same place.

9. Testing

Now, let’s write a test to verify our customized authorization server’s behavior, at least for the “happy path” case. The full code is a bit long, so here we’ll just discuss the main caveats when implementing it.

The first thing is that, to validate end-to-end calls, it’s better to use an environment that’s close to the real one. This is why we use WebEnvironment.RANDOM_PORT, which starts a full servlet stack bound to a free local port. The port’s actual value is injected into the test using the @LocalPort annotation, so we can use it to build all request URLs.

Secondly, we need to be able to keep session cookies between requests sent throughout the authentication/authorization process. This means we need to configure a RestTestClient with a factory request with this support enabled.

In our case, there’s another issue. We can instruct a RestTestClient  to either follow all redirects or none. However, once configured, it’s not trivial to modify this behavior. We could opt to use manual redirect handling only, but that would require additional coding. Instead, we’ve opted to define two clients: one that follows redirects, and another that doesn’t. Both clients share the same CookieManager instance, so we can use either one in the test without getting into session-related issues:

@BeforeEach
void setupRestClient() {
    CookieManager cookieManager = new CookieManager();

    var followRedirectsHttpClient = HttpClient.newBuilder()
      .followRedirects(HttpClient.Redirect.ALWAYS)
      .cookieHandler(cookieManager)
      .build();

    var followRedirectsRequestFactory = new JdkClientHttpRequestFactory(followRedirectsHttpClient);
    restTestClient = RestTestClient
      .bindToServer(followRedirectsRequestFactory)
      .baseUrl("http://localhost:" + port)
      .build();

    var noRedirectsHttpClient = HttpClient.newBuilder()
      .followRedirects(HttpClient.Redirect.NEVER)
      .cookieHandler(cookieManager)
      .build();

    var noRedirectsRequestFactory = new JdkClientHttpRequestFactory(noRedirectsHttpClient);
    noRedirecRestTestClient = RestTestClient
      .bindToServer(noRedirectsRequestFactory)
      .baseUrl("http://localhost:" + port)
      .build();
}

Our “happy-path” test covers the following:

  • Endpoint discovery
  • Building an authorization request with a dynamic scope
  • Submitting user credentials to the login form
  • Submitting values in the consent form
  • Using the generated authentication code to retrieve an access token
  • Validate that the access token has the request dynamic scope associated with it

Those steps cover a complete authorization flow, producing an access token that we can use to send a request to a resource server.

10. Conclusion

In this tutorial, we’ve seen how we can modify the Spring Authorization Server’s default configuration to support dynamic scopes. This expands the range of use cases where we can use it, making it a suitable candidate for complex authorization-related scenarios.

As always, the full source code is available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LSS – NPI (cat=Security/Spring Security)
announcement - icon

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments