· Jose Antonio López  · 6 min read

Spring Security with WebSecurityConfig – Authentication and Authorization with Roles

Robust security for Spring Boot applications using WebSecurityConfig and SecurityFilterChain. A complete guide with authentication, authorization, JWT integration, and code examples to secure your APIs.

Robust security for Spring Boot applications using WebSecurityConfig and SecurityFilterChain. A complete guide with authentication, authorization, JWT integration, and code examples to secure your APIs.

Objective

This content is part of the article authentication in Spring Security with JWT.

I believe this part is so important that it needs to be explained in a separate post for the implementation in Spring Security 6.4.2.

Dependencies

The dependencies used in this post:

DependencyDescription
spring-boot-starter-securitySecurity support for the application. More in the official documentation.
spring-boot-starter-oauth2-authorization-serverProvides an OAuth2 authorization server to manage access tokens. More in the official documentation.

ApiConfig

First, it’s good to define the ApiConfig class. This class contains all the information related to the root paths of the API.

package com.hey.fincas.common.infrastructure.config;

public class ApiConfig {
    private static final String COMMON_PATH = "/hey-fincas-api";
    private static final String API_VERSION = "/v1";
    public  static final String API_BASE_PATH = COMMON_PATH + API_VERSION;

    private ApiConfig() {
        throw new UnsupportedOperationException("This class should never be instantiated");
    }
}

API Versioning

In this case, the API is versioned with /v1. If you want to change the version, you can do it in this class. This point often goes unnoticed by developers who are not used to working with APIs.

APIs are versioned so that clients can continue using the previous version while updating to the new version. It can be as simple as having two versions of the API in production. Eventually, the previous version can be deactivated.

If this point is not addressed early in development, it can be a headache in the future.

I like to keep the COMMON_PATH and API_VERSION variables private so they cannot be modified from outside the class. If for any reason they need to be exposed as public, you can change the access modifier to public.

Roles

In the application, two roles are defined: END_USER and BACK_OFFICE_ADMIN.

package com.hey.fincas.auth.domain;

import org.springframework.security.core.GrantedAuthority;

public enum Role implements GrantedAuthority {
    BACK_OFFICE_ADMIN,
    SALES_MANAGER,
    END_USER;

    @Override
    public String getAuthority() {
        return "ROLE_" + name();
    }
}

GrantedAuthority

GrantedAuthority is an interface that represents an authority. The authority is granted to a user and has specific permissions like READ, WRITE, DELETE, etc.

To simplify the implementation, I advise using GrantedAuthority with roles.

In the implementation, ROLE_ is used as a prefix for roles. It is a Spring Security convention that can be customized, but I think doing so complicates things.

As it stands now, the enum value is serialized as BACK_OFFICE_ADMIN and END_USER. Within the application, it will be used as Spring Security needs. In the database and in request and response bodies, it will appear as BACK_OFFICE_ADMIN and END_USER.

SecurityConfig

SecurityConfig.java
package com.hey.fincas.auth.infrastructure.config;

@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
public class WebSecurityConfig {
    private final AuthService authService;
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public WebSecurityConfig(AuthService authService, UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.authService = authService;
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    public final static String LOGIN_URL_MATCHER = ApiConfig.API_BASE_PATH + "/auth/login";
    public final static String LOG_OUT_URL_MATCHER = ApiConfig.API_BASE_PATH + "/auth/logout";
    final String BASE_URL_MATCHER = ApiConfig.API_BASE_PATH + "/**";

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        final Filter jwtFilter = jwtAuthenticationFilter();
        http
            .formLogin(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests((requests) -> requests
                .requestMatchers(HttpMethod.POST, LOGIN_URL_MATCHER).permitAll()
                .requestMatchers(BASE_URL_MATCHER).authenticated()
                .anyRequest().denyAll()
            )
            .logout(logout -> {
                logout
                    .logoutRequestMatcher(new AntPathRequestMatcher(LOG_OUT_URL_MATCHER, HttpMethod.POST.name()))
                    .logoutSuccessHandler((request, response, authentication) -> {
                        response.setStatus(HttpStatus.NO_CONTENT.value());
                        final Cookie cookie = new Cookie(AuthCookieConstants.TOKEN_COOKIE_NAME, null);
                        cookie.setMaxAge(0);
                        response.addCookie(cookie);
                    })
                ;
            })
            .addFilterBefore(jwtFilter, LogoutFilter.class)
            .csrf((csrf) -> {
                    try {
                        csrf.disable()
                            .sessionManagement((sessionManagement) -> sessionManagement
                                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                            ).oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
                    } catch (Exception e) {
                        throw new AuthenticationException("Spring Security Config Issue",e) {
                        };
                    }
                }
            )
            .authenticationManager(authenticationManager())
            .exceptionHandling(handler -> handler
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                })
            )
        ;

        return http.build();
    }


    @Bean
    public AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);

        ProviderManager providerManager = new ProviderManager(authenticationProvider);
        providerManager.setEraseCredentialsAfterAuthentication(true);

        return providerManager;
    }

    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setRoleHierarchy(roleHierarchy());
        return expressionHandler;
    }

    @Bean
    public RoleHierarchy roleHierarchy() {
        return RoleHierarchyImpl.withDefaultRolePrefix()
            .role(Role.BACK_OFFICE_ADMIN.name())
            .implies(Role.SALES_MANAGER.name())
            .implies(Role.END_USER.name())
            .build();
    }


    private JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter(authService, userDetailsService);
    }

}

Annotations

AnnotationDescription
@ConfigurationIndicates that the class is a configuration class.
@EnableWebSecurity(debug = true)Enables web security in the application. The easiest way to implement security is by exposing a @Bean that returns a SecurityFilterChain object.
@EnableMethodSecurityEnables method-level security, allowing the use of security annotations on controller methods.

SecurityFilterChain

The securityFilterChain method is responsible for configuring the application’s security. The method receives an instance of HttpSecurity which configures the application’s security.

FormLogin

Spring Security’s default login form is a web page that prompts the user to enter their credentials. The default login form is disabled. Since this is an API, a login form is not needed.

authorizeHttpRequests

  • The path /hey-fincas-api/v1/auth/login is accessible without authentication and only via POST method.
  • All paths starting with /hey-fincas-api/v1 are authenticated.
  • All other paths are denied.

The configuration can be customized according to the application but is a good starting point. It ensures that requests outside the root are denied and no one can access them to see what’s behind.

logout

  • The path /hey-fincas-api/v1/auth/logout is accessible only via POST method.
  • On logout, the authentication cookie is deleted.

Some applications implement logout in a controller. Implementing it in the controller is not a bad practice, but it is more secure to do it in the Spring Security configuration.

This way, it ensures that the request does not pass through all the application’s filters and avoids reaching the servlet.

addFilterBefore

The jwtFilter is added before the LogoutFilter. The jwtFilter validates the JWT token and authenticates the user. To determine where to add the custom filter, you can use the addFilterBefore or addFilterAfter method.

For more details on where to add the custom filter, visit the official Spring Security documentation.

csrf

  • CSRF protection is disabled.
  • The session creation policy is set to STATELESS.
  • The OAuth2 resource server is configured with default settings.

CSRF is an attack that occurs when an attacker tricks a user into performing an unwanted action on an application in which the user is authenticated. Disabling CSRF prevents this type of attack. More information about CSRF.

The STATELESS session creation policy indicates that no session will be created for the user. Each request is considered independent and no user information is stored in the session. This is one of the parts that ensures the API is RESTful.

exceptionHandling

  • The response status code is set to 401 Unauthorized when the user is not authenticated.
  • If more customization is needed, a custom AuthenticationEntryPoint should be created.

authenticationManager

AuthenticationManager is the main component of Spring Security that manages authentication. It verifies the user’s credentials and authenticates them. The most common implementation is DaoAuthenticationProvider and you can find more information in the official documentation.

methodSecurityExpressionHandler

MethodSecurityExpressionHandler is an interface for handling security expressions in methods. It allows defining security rules at the method level using annotations like @PreAuthorize and @PostAuthorize.

I recommend using DefaultMethodSecurityExpressionHandler which is the default implementation and nothing more.

roleHierarchy

RoleHierarchy allows defining a hierarchy of roles in your application. It is useful when you have roles that inherit permissions from other roles.

For example, you have an ADMIN role that has its permissions plus those of a SALES_MANAGER. Spring Security will use that hierarchy.

Back to the main article

You can return to authentication in Spring Security with JWT.

  • Spring Security
  • Spring Boot
Share: