spring Security Architecture Principles by Daniel Garnier-Moiroux @ Spring I/O 2024

Abstract

  • Filter for security decisions on HTTP requests.
  • Authentication is the domain language of spring security.
  • AuthenticationProvider to validate credentials.
  • Filter + AuthenticationProvider for custom login.

A spring security configuration that can work for most web applications:

@Configuration
@EnableWebSecurity
class SecurityConfig {
 
  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(
            authorizeHttp -> {
              authorizeHttp.requestMatchers("/").permitAll();
              authorizeHttp.requestMatchers("/favicon.svg").permitAll();
              authorizeHttp.requestMatchers("/css/*").permitAll();
              authorizeHttp.requestMatchers("/error").permitAll();
              authorizeHttp.anyRequest().authenticated();
            }
        )
        .formLogin(l -> l.defaultSuccessUrl("/private"))
        .logout(l -> l.logoutSuccessUrl("/"))
        .oauth2Login(Customizer.withDefaults())
        // You can add multiple login types.
        .httpBasic(Customizer.withDefaults())
        // You can add your custom filters.
        // /!\ Order matters!!!
        .addFilterBefore(new CustomFilter(), Authorization.class)
        // You can add your custom AuthenticationProvider.
        .authenticationProvider(new CustomAuthenticationProvider())
        .build();
  }
}

Filter

  • When creating custom Filter, implements OncePerRequestFilter.
    • Takes HttpServletRequest, HttpServletResponse.
    • Reads from request to:
      • sometimes writes to Response,
      • sometimes does nothing!
  • You can look at the spring logs on startup to check the order of the Filter
  • To debug any HTTP 401 issue, e.g. to know which Filter rejected your HTTP requests, you can downlevel the log level to TRACE:
    • logging.level.org.springframework.security=TRACE
    • execute your HTTP request, and look at the logs from FilterChainProxy

Authentication objects

spring Security produces Authentication objects, used for:

  • Authentication: who is the user?
  • Authorization: is the user allowed to perform XYZ?

Vocabulary:

  • Principal: user “identity” (name, email, …)
  • GrantedAuthorities: “permissions” (roles, …)
  • .isAuthenticated(): almost always true
  • details: details about the request
  • (Credentials): “password”, often null

Use SecurityContextHolder.getContext().getAuthentication() to get your Authentication object. The SecurityContext is:

  • Thread-local,
  • not propagated to child threads,
  • cleared after requests is processed.

Example of setting your custom Authentication in a custom Filter:

class RoboAuthenticationFilter extends OncePerRequestFilter {
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
    // 1. Decide whether to apply the filter or not.
    // ...
    // 2. Check credentials and [authenticate | reject ], e.g. by checking the request headers.
    // ...
    // 3. Create custom Authentication.
    var auth = new RoboAuthenticationToken();
    var newContext = SecurityContextHolder.createEmptyContext();
    newContext.setAuthentication(auth);
    SecurityContextHolder.setContext(newContext);
    
    filterChain.doFilter(request, response);
  }
}

Some Filter produce an Authentication:

  • read the request and convert to “domain” Authentication object,
  • authenticate (are the credentials valid?),
  • save the Authentication in the SecurityContext
  • or reject the request when credentials are invalid.

AuthenticationProvider

Implement your custom authentication provider by implementing the interface AuthenticationProvider:

class CustomAuthenticationProvider implements AuthenticationProvider {
  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    if (Objects.equals(authentication.getName(), "foobar")) {
      var user = User.withUsername("foobar").password("ignored").roles("user", "admin").build();
      return UsernamePasswordAuthenticationToken.authenticated(
        user, null, user.getAuthorities()
      );
    }
    return null;
  }
  
  @Overrde
  public boolean supports(Class authentication) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentcation);
  }
}

Then add it to your SecurityConfig.

You can add an ApplicationListener to audit who logged in your application:

@Bean
ApplicationListener<AuthenticationSuccessEvent&gt; successListener() {
  return event -> {
    System.out.println("🎉 [%s] %s".formatted(
        event.getAuthentication().getClass().getSimpleName(),
        event.getAuthentication().getName()
    ));
  };
}
  • Authentication is both an auth request and a successful auth result.
  • AuthenticationProvider validates credentials.
    • Operates only within the “auth” domain (no HTTP, HTML, …).
  • AuthenticationProvider leverages spring security infrastructure.