1

For starters i want to secure a part of my rest-api with basic auth. When I try to access endpoints from a react client, I keep getting 401's in the preflight requests.

I tried to follow this guide without success: https://www.baeldung.com/spring-security-cors-preflight

i'm not sure if this is part of the problem, but another part can only be accessed with certain custom http headers.

I'm using Method Security:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
class MethodSecurityConfig : GlobalMethodSecurityConfiguration() {
    override fun customMethodSecurityMetadataSource(): MethodSecurityMetadataSource = SecurityMetadataSource()

    override fun accessDecisionManager(): AccessDecisionManager = super.accessDecisionManager().apply {
        this as AbstractAccessDecisionManager
        decisionVoters.add(PrivilegeVoter())
    }
}

And this is my Security config:

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class SecurityConfig : WebSecurityConfigurerAdapter() {
    private val deviceRequestHeaderName: String = "X-DEVICE-ID"    
    private val platformRequestHeaderName: String = "X-PLATFORM-ID"

    @Autowired
    lateinit var users: AppUserRepository

    @Autowired
    lateinit var backendUsers: BackendUserRepository

    @Autowired
    lateinit var roles: RoleRepository

    val authManager by lazy { authenticationManager() }

    private val authProvider by lazy {
        PreAuthenticatedAuthenticationProvider().apply {
            setPreAuthenticatedUserDetailsService {
                val authId = it.principal as UserAuthId
                if (authId.deviceId == null) throw UsernameNotFoundException("No device-id to search for.")
                if (authId.platform == null) throw UsernameNotFoundException("Platform not specified.")
                val platform = try {
                    ApplicationPlatform.valueOf(authId.platform)
                } catch (e: IllegalArgumentException) {
                    throw UsernameNotFoundException("Unknown platform ${authId.platform}.")
                }
                val existingUser = users.findByUserDeviceIdAndPlatform(authId.deviceId, platform)
                if (existingUser != null) return@setPreAuthenticatedUserDetailsService existingUser

                users.save(AppUser(authId.deviceId, platform, roles))
            }
        }
    }

    val passwordEncoder by lazy { BCryptPasswordEncoder() }

    private val deviceIdFilter by lazy {
        HeaderFieldAuthFilter(deviceRequestHeaderName, platformRequestHeaderName).apply {
            setAuthenticationManager(authManager)
        }
    }

    override fun configure(auth: AuthenticationManagerBuilder) = auth {
        authenticationProvider(authProvider)

        val userDetailsService = BackendUserDetailsService(backendUsers)
        userDetailsService(userDetailsService).passwordEncoder(passwordEncoder)
    }

    override fun configure(http: HttpSecurity) = http {
        session {
            sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        }
        exceptionHandling()

        addFilter(deviceIdFilter)
        authorizeRequests().anyRequest().authenticated()
        csrf().disable()
        httpBasic()

        cors().configurationSource { request ->
            CorsConfiguration().apply {
                allowedOrigins = listOf(ALL)
                allowedMethods = listOf(GET, POST, DELETE, PUT, OPTIONS).map { it.name }
                allowedHeaders = listOf(ALL)
                allowCredentials = true
                maxAge = 3600
            }
        }
    }

    @Bean
    fun auditorProvider(): AuditorAware<User> = AuditorAware<User> {
        val authentication = SecurityContextHolder.getContext().authentication
        val user = authentication.run { if (isAuthenticated) principal as? User else null }
        return@AuditorAware Optional.ofNullable(user)
    }
}
IARI
  • 1,217
  • 1
  • 18
  • 35

2 Answers2

0

I could solve by manually exclude the preflight requests from authentication. adding this

antMatchers(OPTIONS, "/**").permitAll()

to the authorizeRequests() configuration accomplishes that. Note that Options is a direct reference to the HttpMethod enum value, imported like this

import org.springframework.http.HttpMethod.*

Stackoverflow posts that helped me get there:

Originally I had assumed, that this should have been handled by the cors configuration - which it was apparently not.

IARI
  • 1,217
  • 1
  • 18
  • 35
0

To enable CORS for a single rest endpoint you can annotate it with:

@CrossOrigin

To allow for CORS for all endpoints you can have a bean like so:

  @Bean
  public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurerAdapter() {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
                .allowedHeaders("*");
      }
    };
Tbuddy
  • 73
  • 5
  • Putting @CrossOrigin on a single endpoint didn't solve the problem for that endpoint in my project. The options request gets a 401 response. – IARI May 13 '20 at 19:50
  • I'll test the WebMvcConfigurerAdapter suggestion tomorrow. – IARI May 13 '20 at 19:56