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:
Is ServerSecurityContextRepository
good place to obtain username and token from exchange - or there is a better place to do it?
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?
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.
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 .
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.
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.
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()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With