Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom authentication with spring-security and reactive spring

I'm having a custom authentication scheme. I'm having a REST endpoint that has userId in http uri path and token in http header. I would like to check that such request is perform by valid user with valid token. Users and tokens are stored in mongo collection.

I don't know in which class I should authorize user.

My SecurityConfig:

@EnableWebFluxSecurity
class SecurityConfig {

  @Bean
  fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {

    val build = http
        .httpBasic().disable()
        .formLogin().disable()
        .csrf().disable()
        .logout().disable()
        .authenticationManager(CustomReactiveAuthenticationManager())
        .securityContextRepository(CustomServerSecurityContextRepository())
        .authorizeExchange().pathMatchers("/api/measurement/**").hasAuthority("ROLE_USER")
        .anyExchange().permitAll().and()

    return build.build()
  }

  @Bean
  fun userDetailsService(): MapReactiveUserDetailsService {
    val user = User.withDefaultPasswordEncoder()
        .username("sampleDeviceIdV1")
        .password("foo")
        .roles("USER")
        .build()

    return MapReactiveUserDetailsService(user)
  }
}

My ServerSecurityContextRepository:

class CustomServerSecurityContextRepository : ServerSecurityContextRepository {

  override fun load(exchange: ServerWebExchange): Mono<SecurityContext> {
    val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
    val path = exchange.request.uri.path


    return if (path.startsWith("/api/measurement/") && authHeader != null && authHeader.startsWith(prefix = "Bearer ")) {
      val deviceId = path.drop(17)

      val authToken = authHeader.drop(7)
      val auth = UsernamePasswordAuthenticationToken(deviceId, authToken)
      Mono.just(SecurityContextImpl(auth))
    } else {
      Mono.empty()
    }
  }

  override fun save(exchange: ServerWebExchange?, context: SecurityContext?): Mono<Void> {
    return Mono.empty()
  }
}

Two questions arise:

  1. Is ServerSecurityContextRepository good place to obtain username and token from exchange - or there is a better place to do it?

  2. Where should I perform authentication (check token and username against mongo collection)? My custom AuthenticationManager does not get called anywhere. Should I do everything inside ServerSecurityContextRepository or perform user and token validation inside ReactiveAuthenticationManager? Or maybe other class would be even more suitable?

like image 689
pixel Avatar asked Jul 30 '18 16:07

pixel


People also ask

How do I authenticate in Spring Security?

Simply put, Spring Security hold the principal information of each authenticated user in a ThreadLocal – represented as an Authentication object. In order to construct and set this Authentication object – we need to use the same approach Spring Security typically uses to build the object on a standard authentication.

What is difference between AuthenticationManager and AuthenticationProvider?

Authentication Provider calls User Details service loads the User Details and returns the Authenticated Principal. Authentication Manager returns the Authenticated Object to Authentication Filter and Authentication Filter sets the Authentication object in Security Context .

What is the use of AuthenticationManagerBuilder?

Class AuthenticationManagerBuilder. SecurityBuilder used to create an AuthenticationManager . Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService , and adding AuthenticationProvider 's.

How does Spring Security authentication work internally?

The Spring Security Architecture There are multiple filters in spring security out of which one is the Authentication Filter, which initiates the process of authentication. Once the request passes through the authentication filter, the credentials of the user are stored in the Authentication object.


1 Answers

It turns out that some tutorials on the web are plain wrong.

I've managed to configure everything using following code:

class DeviceAuthenticationConverter : Function<ServerWebExchange, Mono<Authentication>> {
  override fun apply(exchange: ServerWebExchange): Mono<Authentication> {
    val authHeader: String? = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
    val path: String? = exchange.request.uri.path

    return when {
      isValidPath(path) && isValidHeader(authHeader) -> Mono.just(UsernamePasswordAuthenticationToken(path?.drop(17), authHeader?.drop(7)))
      else -> Mono.empty()
    }
  }

  private fun isValidPath(path: String?) = path != null && path.startsWith(API_MEASUREMENT)

  private fun isValidHeader(authHeader: String?) = authHeader != null && authHeader.startsWith(prefix = "Bearer ")

}

And config:

@EnableWebFluxSecurity
class SecurityConfig {

  companion object {
    const val API_MEASUREMENT = "/api/measurement/"
    const val API_MEASUREMENT_PATH = "$API_MEASUREMENT**"
    const val DEVICE = "DEVICE"
    const val DEVICE_ID = "deviceId"
  }

  @Bean
  fun securityWebFilterChain(http: ServerHttpSecurity, authenticationManager: ReactiveAuthenticationManager) =
      http
          .httpBasic().disable()
          .formLogin().disable()
          .csrf().disable()
          .logout().disable()
          .authorizeExchange().pathMatchers(API_MEASUREMENT_PATH).hasRole(DEVICE)
          .anyExchange().permitAll().and().addFilterAt(authenticationWebFilter(authenticationManager), AUTHENTICATION).build()

  @Bean
  fun userDetailsService(tokenRepository: TokenRepository) = MongoDeviceTokenReactiveUserDetailsService(tokenRepository)

  @Bean
  fun tokenRepository(template: ReactiveMongoTemplate, passwordEncoder: PasswordEncoder) = MongoTokenRepository(template, passwordEncoder)

  @Bean
  fun tokenFacade(tokenRepository: TokenRepository) = TokenFacade(tokenRepository)

  @Bean
  fun authManager(userDetailsService: ReactiveUserDetailsService) = UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService)

  private fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager) =
      AuthenticationWebFilter(reactiveAuthenticationManager).apply {
        setAuthenticationConverter(DeviceAuthenticationConverter())
        setRequiresAuthenticationMatcher(
            ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, API_MEASUREMENT_PATH)
        )
      }

  @Bean
  fun passwordEncoder() = PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
like image 103
pixel Avatar answered Oct 30 '22 16:10

pixel