Skip to content

Commit

Permalink
Pre authenticated user (#2386)
Browse files Browse the repository at this point in the history
  • Loading branch information
nulls authored Aug 4, 2023
1 parent 0f95a03 commit 398b864
Show file tree
Hide file tree
Showing 37 changed files with 368 additions and 550 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class SecurityInfoController(
is OAuth2AuthenticationToken -> {
val source = authentication.authorizedClientRegistrationId
val nameInSource = authentication.name
backendService.findByOriginalLogin(source, nameInSource).map { it.username }
backendService.findByOriginalLogin(source, nameInSource).map { it.name }
}
else -> Mono.empty()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.springframework.security.authorization.AuthenticatedReactiveAuthoriza
import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
Expand All @@ -31,6 +32,7 @@ import org.springframework.security.web.server.util.matcher.NegatedServerWebExch
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
import reactor.kotlin.core.publisher.cast

@EnableWebFluxSecurity
@Suppress(
Expand Down Expand Up @@ -118,7 +120,7 @@ class WebSecurityConfig(
// Authenticate by comparing received basic credentials with existing one from DB
httpBasicSpec.authenticationManager(
UserDetailsRepositoryReactiveAuthenticationManager { username ->
backendService.findByName(username)
backendService.findByName(username).cast<UserDetails>()
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package com.saveourtool.save.gateway.service

import com.saveourtool.save.authservice.utils.AuthenticationUserDetails
import com.saveourtool.save.entities.User
import com.saveourtool.save.gateway.config.ConfigurationProperties
import com.saveourtool.save.utils.orNotFound

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.http.MediaType
import org.springframework.security.core.userdetails.User as SpringUser
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.jackson2.CoreJackson2Module
import org.springframework.security.jackson2.SecurityJackson2Modules
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.toEntity
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono

/**
* A service to backend to lookup users in DB
Expand All @@ -22,42 +21,35 @@ import reactor.core.publisher.Mono
class BackendService(
configurationProperties: ConfigurationProperties,
) {
private val springUserDetailsReader = ObjectMapper()
.findAndRegisterModules()
.registerModule(CoreJackson2Module())
.registerModules(SecurityJackson2Modules.getModules(javaClass.classLoader))
.readerFor(SpringUser::class.java)
private val webClient = WebClient.create(configurationProperties.backend.url)

/**
* @param username
* @return [UserDetails] found in DB by received name
*/
fun findByName(username: String): Mono<UserDetails> = webClient.get()
.uri("/internal/users/find-by-name/$username")
.retrieve()
.onStatus({ it.is4xxClientError }) {
Mono.error(ResponseStatusException(it.statusCode()))
}
.toEntity<String>()
.map {
springUserDetailsReader.readValue(it.body)
}
fun findByName(
username: String,
): Mono<AuthenticationUserDetails> = findAuthenticationUserDetails("/internal/users/find-by-name/$username")

/**
* @param source
* @param nameInSource
* @return [UserDetails] found in DB by source and name in this source
*/
fun findByOriginalLogin(source: String, nameInSource: String): Mono<UserDetails> = webClient.get()
.uri("/internal/users/find-by-original-login/$source/$nameInSource")
fun findByOriginalLogin(
source: String,
nameInSource: String,
): Mono<AuthenticationUserDetails> = findAuthenticationUserDetails("/internal/users/find-by-original-login/$source/$nameInSource")

private fun findAuthenticationUserDetails(uri: String): Mono<AuthenticationUserDetails> = webClient.get()
.uri(uri)
.retrieve()
.onStatus({ it.is4xxClientError }) {
Mono.error(ResponseStatusException(it.statusCode()))
}
.toEntity<String>()
.map {
springUserDetailsReader.readValue(it.body)
.toEntity<AuthenticationUserDetails>()
.flatMap { responseEntity ->
responseEntity.body.toMono().orNotFound { "Authentication body is empty" }
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.saveourtool.save.gateway.utils

import com.saveourtool.save.authservice.utils.AuthenticationUserDetails
import com.saveourtool.save.gateway.service.BackendService
import com.saveourtool.save.utils.switchIfEmptyToResponseException
import org.springframework.cloud.gateway.filter.GatewayFilter
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono
import java.security.Principal

/**
* Filter, that mutate existing exchange,
* inserts user's info into Authorization headers instead of existing value, not paying attention to the credentials,
* since at this moment they are already checked by gateway.
*/
@Component
class AuthorizationHeadersGatewayFilterFactory(
private val backendService: BackendService,
) : AbstractGatewayFilterFactory<Any>() {
override fun apply(config: Any?): GatewayFilter = GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain ->
exchange.getPrincipal<Principal>()
.flatMap { resolveSaveUser(it) }
.map { user ->
exchange.mutate()
.request { builder ->
builder.headers { headers: HttpHeaders ->
headers.remove(HttpHeaders.AUTHORIZATION)
user.populateHeaders(headers)
}
}
.build()
}
.defaultIfEmpty(exchange)
.flatMap { chain.filter(it) }
}

private fun resolveSaveUser(principal: Principal): Mono<AuthenticationUserDetails> = when (principal) {
is OAuth2AuthenticationToken -> backendService.findByOriginalLogin(principal.authorizedClientRegistrationId, principal.name)
is UsernamePasswordAuthenticationToken -> (principal.principal as? AuthenticationUserDetails)
.toMono()
.switchIfEmptyToResponseException(HttpStatus.INTERNAL_SERVER_ERROR) {
"Unexpected principal type ${principal.principal.javaClass} in ${UsernamePasswordAuthenticationToken::class}"
}
else -> Mono.error(BadCredentialsException("Unsupported authentication type: ${principal::class}"))
}
}

This file was deleted.

6 changes: 3 additions & 3 deletions api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ spring:
filters:
# If SESSION cookie is passed to downstream, it is then removed, because downstream discards it
- RemoveRequestHeader=Cookie
- ConvertAuthorizationHeader=
- AuthorizationHeaders=
- id: demo-api_route
uri: ${gateway.demo.url}
predicates:
- Path=/api/demo/**
filters:
# If SESSION cookie is passed to downstream, it is then removed, because downstream discards it
- RemoveRequestHeader=Cookie
- ConvertAuthorizationHeader=
- AuthorizationHeaders=
- id: demo-cpg-api_route
uri: ${gateway.demo-cpg.url}
predicates:
Expand All @@ -57,7 +57,7 @@ spring:
filters:
# If SESSION cookie is passed to downstream, it is then removed, because downstream discards it
- RemoveRequestHeader=Cookie
- ConvertAuthorizationHeader=
- AuthorizationHeaders=
- id: error_route
uri: ${gateway.backend.url}/error
predicates:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,26 @@

package com.saveourtool.save.authservice.config

import com.saveourtool.save.authservice.security.ConvertingAuthenticationManager
import com.saveourtool.save.authservice.utils.AuthenticationUserDetails.Companion.toAuthenticationUserDetails
import com.saveourtool.save.authservice.utils.roleHierarchy
import com.saveourtool.save.v1

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.AuthenticationWebFilter
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint
import org.springframework.web.server.WebFilter
import reactor.kotlin.core.publisher.toMono

import javax.annotation.PostConstruct

Expand All @@ -28,9 +32,19 @@ import javax.annotation.PostConstruct
@Profile("secure")
@Suppress("MISSING_KDOC_TOP_LEVEL", "MISSING_KDOC_CLASS_ELEMENTS", "MISSING_KDOC_ON_FUNCTION")
class WebSecurityConfig(
private val authenticationManager: ConvertingAuthenticationManager,
@Autowired private var defaultMethodSecurityExpressionHandler: DefaultMethodSecurityExpressionHandler
private val defaultMethodSecurityExpressionHandler: DefaultMethodSecurityExpressionHandler
) {
@Bean
fun saveUserPreAuthenticatedProcessingWebFilter(): WebFilter {
val authenticationManager = ReactiveAuthenticationManager { authentication -> authentication.toMono() }
return AuthenticationWebFilter(authenticationManager)
.also { authenticationWebFilter ->
authenticationWebFilter.setServerAuthenticationConverter { exchange ->
exchange.request.headers.toAuthenticationUserDetails()?.toAuthenticationToken().toMono()
}
}
}

@Bean
fun securityWebFilterChain(
http: ServerHttpSecurity
Expand All @@ -53,9 +67,7 @@ class WebSecurityConfig(
// FixMe: Properly support CSRF protection https://github.com/saveourtool/save-cloud/issues/34
csrf().disable()
}
.httpBasic {
it.authenticationManager(authenticationManager)
}
.addFilterAt(saveUserPreAuthenticatedProcessingWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.exceptionHandling {
it.authenticationEntryPoint(
HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)
Expand Down

This file was deleted.

Loading

0 comments on commit 398b864

Please sign in to comment.