Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot 3: JWT and Basic Auth Security

I need two authentications. One secured with JWT to access Keycloak token and another ressource to provide access over Basic-Auth. Created two SecurityConfigs with the @Order Annotation.

WebSecurityConfig for Keycloak:

@Configuration
@Order(2)
class WebSecurityConfig {

    @SuppressWarnings("unchecked")
    @Bean
    public Jwt2AuthoritiesConverter authoritiesConverter() {
        // This is a converter for roles as embedded in the JWT by a Keycloak server
        // Roles are taken from both realm_access.roles
        return jwt -> {
            final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
            final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());

            return realmRoles.stream().filter(UserRole::isValidRole)
                    .map(SimpleGrantedAuthority::new).toList();
        };
    }

    @Bean
    public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
        return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
    }

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
        http
                .authorizeHttpRequests()
                .requestMatchers(HttpMethod.GET, "/api/**").hasAnyAuthority("EDIT")
                .requestMatchers("/api/**").hasAuthority("EDIT")
                .requestMatchers("/setup/**", "/*", "/assets/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt().jwtAuthenticationConverter(authenticationConverter);
        return http.build();
    }

    public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
    }

    public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
    }

ExtWebSecurityConfig config for the Basic Auth:

@Configuration
@Order(1)
class ExtWebSecurityConfig {

    @Bean
    public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception {
        http
                .httpBasic().and()
                .securityMatcher("/ext/**")
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated());
        return http.build();
    }
}

If i start the application, the JWT Keycloak ressource is secured correctly. If i access the enpoint /ext/test and provide the Basic-Auth user and password from my configuration it results to "Unauthorized 401". URL: http://localhost:8080/ext/v1

This is the related application.yml configuration:

spring:
  security:
    user:
      name: admin
      password: admin
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ${keycloak.auth-server-url}/realms/${keycloak.realm}
like image 828
stylepatrick Avatar asked Oct 17 '25 01:10

stylepatrick


1 Answers

Put the @Order on SecurityFilterChain beans (not the @Configuration beans): what is important is the order in which it is evaluated at runtime, not the one it is built at startup.

Also, you might define a securityMatcher for the SecurityFilterChain with the lowest @Order (the one evaluated first) based on Authorization header pattern, rather than request path.

For instance:

@Configuration
class WebSecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher((HttpServletRequest request) -> {
                return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).map(h -> {
                    return h.toLowerCase().startsWith("basic ");
                }).orElse(false);
            }).httpBasic(withDefaults())
            .userDetailsService((String username) -> {
                return new User(username, "", List.of());
            }).authenticationManager(...)
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(configurer -> configurer.disable());

        http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated());

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
        http
                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(configurer -> configurer.disable())
                .authorizeHttpRequests()
                .requestMatchers(HttpMethod.GET, "/api/**").hasAnyAuthority("EDIT")
                .requestMatchers("/api/**").hasAuthority("EDIT")
                .requestMatchers("/setup/**", "/*", "/assets/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
        return http.build();
    }
}

Sample taken from there.

like image 174
ch4mp Avatar answered Oct 18 '25 18:10

ch4mp



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!