Skip to content

Commit

Permalink
fix(security): make CORS configurable and don't allow credentials whe…
Browse files Browse the repository at this point in the history
…n using all (*) origins, fix redirections from another domain by allowing using redirect_uri
  • Loading branch information
filipowm committed May 2, 2022
1 parent d0bd817 commit 181e3e4
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.roche.ambassador.security.configuration

import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain

@EnableConfigurationProperties(SecurityProperties::class)
internal abstract class BaseSecurityConfiguration(private val configurers: List<SecurityConfigurer>) {

@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
// @formatter:off
http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
configurers.forEach { it.configure(http) }
configure(http)
return http.build()
// @formatter:on
}

abstract fun configure(http: ServerHttpSecurity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.roche.ambassador.security.configuration

import com.roche.ambassador.extensions.LoggerDelegate
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.stereotype.Component
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource

@Component
class CorsConfigurer(private val securityProperties: SecurityProperties) : SecurityConfigurer {

companion object {
private const val ALLOW_ALL: String = "*"
private val log by LoggerDelegate()
}

override fun configure(http: ServerHttpSecurity) {
http.cors().configure()
}

private fun ServerHttpSecurity.CorsSpec.configure(): ServerHttpSecurity {
val corsConfig = UrlBasedCorsConfigurationSource()
val _allowedOrigins = securityProperties.cors.allowedOrigins.ifEmpty {
listOf(ALLOW_ALL)
}
log.info("Setting up CORS with allowed origins: {}", _allowedOrigins)
val cors = with(CorsConfiguration()) {
allowedOrigins = _allowedOrigins
allowedMethods = listOf(ALLOW_ALL)
allowedHeaders = listOf(ALLOW_ALL)
allowCredentials = ALLOW_ALL !in securityProperties.cors.allowedOrigins && securityProperties.cors.allowedOrigins.isNotEmpty()
this
}
corsConfig.registerCorsConfiguration("/**", cors)
return configurationSource(corsConfig).and()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.roche.ambassador.security.configuration

import com.roche.ambassador.extensions.LoggerDelegate
import org.springframework.http.HttpCookie
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.http.ResponseCookie
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.security.web.server.savedrequest.ServerRequestCache
import org.springframework.security.web.server.util.matcher.*
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import java.net.URI
import java.time.Duration
import java.util.*

class RedirectUriAwareCookieServerRequestCache(private val allowedRedirectUris: List<String> = listOf()) : ServerRequestCache {

companion object {
const val REDIRECT_URI_COOKIE_NAME: String = "REDIRECT_URI"
const val REDIRECT_URI_PARAMETER: String = "redirect_uri"
val COOKIE_MAX_AGE: Duration = Duration.ofSeconds(-1)
private val log by LoggerDelegate()
}

private var saveRequestMatcher: ServerWebExchangeMatcher = createDefaultRequestMatcher()

override fun saveRequest(exchange: ServerWebExchange): Mono<Void> {
return saveRequestMatcher.matches(exchange)
.filter { it.isMatch }
.map { exchange.response }
.map { it.cookies }
.doOnNext {
val redirectUriCookie: ResponseCookie = createRedirectUriCookie(exchange.request)
if (redirectUriCookie.value.isNotEmpty()) {
it.add(REDIRECT_URI_COOKIE_NAME, redirectUriCookie)
}
log.debug("Request added to Cookie: {}")
}.then()
}

override fun getRedirectUri(exchange: ServerWebExchange): Mono<URI> {
val cookieMap = exchange.request.cookies
return Mono.justOrEmpty(cookieMap.getFirst(REDIRECT_URI_COOKIE_NAME))
.map { obj: HttpCookie -> obj.value }
.map { decodeCookie(it) }
.onErrorResume(IllegalArgumentException::class.java) { Mono.empty() }
.map { URI.create(it) }
}

override fun removeMatchingRequest(exchange: ServerWebExchange): Mono<ServerHttpRequest> {
return Mono.just(exchange.response).map { obj: ServerHttpResponse -> obj.cookies }
.doOnNext {
val invalidateCookie = invalidateRedirectUriCookie(exchange.request)
it.add(REDIRECT_URI_COOKIE_NAME, invalidateCookie)
}
.thenReturn(exchange.request)
}

private fun createRedirectUriCookie(request: ServerHttpRequest): ResponseCookie {
val path = request.path.pathWithinApplication().value()
val query = request.uri.rawQuery
val redirectUriParam = request.queryParams.getOrDefault(REDIRECT_URI_PARAMETER, listOf())
val redirectUriParamWithoutQuery = redirectUriParam
.map { URI.create(it) }
.map { URI(it.scheme, it.authority, it.path, null, it.fragment).toString() }
.firstOrNull()

val redirectUri = if (redirectUriParamWithoutQuery != null && redirectUriParamWithoutQuery in allowedRedirectUris) {
redirectUriParam.first()
} else {
path + if (query != null) "?$query" else ""
}
return createResponseCookie(request, encodeCookie(redirectUri), COOKIE_MAX_AGE)
}

private fun invalidateRedirectUriCookie(request: ServerHttpRequest): ResponseCookie {
return createResponseCookie(request, null, Duration.ZERO)
}

private fun createResponseCookie(request: ServerHttpRequest, cookieValue: String?, age: Duration): ResponseCookie {
return ResponseCookie.from(REDIRECT_URI_COOKIE_NAME, cookieValue.orEmpty())
.path(request.path.contextPath().value() + "/")
.maxAge(age)
.httpOnly(true)
.secure("https".equals(request.uri.scheme, ignoreCase = true)).sameSite("Lax").build()
}

private fun encodeCookie(cookieValue: String): String {
return String(Base64.getEncoder().encode(cookieValue.toByteArray()))
}

private fun decodeCookie(encodedCookieValue: String): String {
return String(Base64.getDecoder().decode(encodedCookieValue.toByteArray()))
}

private fun createDefaultRequestMatcher(): ServerWebExchangeMatcher {
val get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**")
val notFavicon: ServerWebExchangeMatcher = NegatedServerWebExchangeMatcher(
ServerWebExchangeMatchers.pathMatchers("/favicon.*")
)
val html = MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML)
html.setIgnoredMediaTypes(setOf(MediaType.ALL))
return AndServerWebExchangeMatcher(get, notFavicon, html)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.roche.ambassador.security.configuration

import org.springframework.security.config.web.server.ServerHttpSecurity

internal interface SecurityConfigurer {

fun configure(http: ServerHttpSecurity)

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,17 @@ package com.roche.ambassador.security.configuration
import com.roche.ambassador.extensions.LoggerDelegate
import com.roche.ambassador.security.AmbassadorUser
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain

@Configuration
@EnableReactiveMethodSecurity
@ConditionalOnProperty(prefix = "ambassador.security", name = ["enabled"], havingValue = "false", matchIfMissing = false)
internal class NoSecurityConfiguration {

private val log by LoggerDelegate()

@Bean
fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
log.warn("Disabling web security!")
// @formatter:off
return http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.cors().disable()
.authorizeExchange()
.anyExchange().permitAll().and()
.anonymous()
.principal(anonymousUser)
.authorities(anonymousUser.authorities)
.and()
.build()
// @formatter:on
}
internal class SecurityDisabledConfiguration(configurers: List<SecurityConfigurer>) : BaseSecurityConfiguration(configurers) {

companion object {
private val log by LoggerDelegate()
private val anonymousUser = AmbassadorUser(
"__local__",
"__local__",
Expand All @@ -45,4 +22,15 @@ internal class NoSecurityConfiguration {
listOf(AmbassadorUser.ADMIN, AmbassadorUser.USER)
)
}

override fun configure(http: ServerHttpSecurity) {
log.warn("Disabling web security!")
//@formatter:off
http.authorizeExchange()
.anyExchange().permitAll().and()
.anonymous()
.principal(anonymousUser)
.authorities(anonymousUser.authorities)
//@formatter:on
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,100 +13,77 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.core.AuthenticationException
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.WebFilterExchange
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
import org.springframework.security.web.server.savedrequest.ServerRequestCache
import reactor.core.publisher.Mono

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@EnableWebFluxSecurity
@EnableConfigurationProperties(SessionProperties::class)
@ConditionalOnProperty(prefix = "ambassador.security", name = ["enabled"], havingValue = "true", matchIfMissing = true)
internal class SecurityConfiguration {
internal class SecurityEnabledConfiguration(
configurers: List<SecurityConfigurer>,
private val securityProperties: SecurityProperties,
private val sessionProperties: SessionProperties,
private val projectSourcesProperties: ProjectSourcesProperties,
private val projectSources: ProjectSources
) : BaseSecurityConfiguration(configurers) {

private val log by LoggerDelegate()
companion object {
private val log by LoggerDelegate()
}

@Bean
fun reactiveClientRegistrationRepository(
projectSourcesProperties: ProjectSourcesProperties,
projectSources: ProjectSources
): InMemoryReactiveClientRegistrationRepository {
fun reactiveClientRegistrationRepository(): InMemoryReactiveClientRegistrationRepository {
val registrar = ClientRegistrationRegistrar(projectSourcesProperties)
return registrar.createRegistrations(listOf(*projectSources.getAll().toTypedArray()))
}

@Bean
fun ambassadorUserDetailsService(
repository: InMemoryReactiveClientRegistrationRepository,
projectSources: ProjectSources
): AmbassadorUserService {
fun ambassadorUserDetailsService(repository: InMemoryReactiveClientRegistrationRepository): AmbassadorUserService {
// TODO make it more friendly to create holder and create mapping of registration to source
val holder = OAuth2ProvidersHolder()
for (registration in repository) {
projectSources.getByName(registration.clientName)
.ifPresent { holder.add(registration, it) }
projectSources.getByName(registration.clientName).ifPresent { holder.add(registration, it) }
}
if (holder.isEmpty()) {
throw IllegalStateException("Unable to find any matching project source for registrations")
}
return AmbassadorUserService(holder)
}

@Bean
fun springWebFilterChain(
http: ServerHttpSecurity,
sessionProperties: SessionProperties,
reactiveClientRegistrationRepository: ReactiveClientRegistrationRepository
): SecurityWebFilterChain {
log.info("Enabling web security...")
// @formatter:off
return http
.csrf().disable()
.cors().configure()
.httpBasic().disable()
.formLogin().disable()
override fun configure(http: ServerHttpSecurity) {
log.info("Enabling web security")
val requestCache = RedirectUriAwareCookieServerRequestCache(securityProperties.allowedRedirectUris)
//@formatter:off
http
.requestCache().requestCache(requestCache).and()
.authorizeExchange()
.pathMatchers("/actuator/health/**").permitAll()
.pathMatchers("/actuator/health/**").permitAll()
.anyExchange().authenticated().and()
.oauth2Login()
.authenticationSuccessHandler(buildSuccessHandler(sessionProperties))
.authenticationFailureHandler(AmbassadorAuthenticationFailureHandler())
.clientRegistrationRepository(reactiveClientRegistrationRepository)
.and()
.build()
// @formatter:on
.authenticationSuccessHandler(buildSuccessHandler(sessionProperties, requestCache))
.authenticationFailureHandler(AmbassadorAuthenticationFailureHandler)
//@formatter:on
}

private fun buildSuccessHandler(sessionProperties: SessionProperties): ServerAuthenticationSuccessHandler {
private fun buildSuccessHandler(sessionProperties: SessionProperties, requestCache: ServerRequestCache): ServerAuthenticationSuccessHandler {
val redirectSuccessHandler = RedirectServerAuthenticationSuccessHandler()
redirectSuccessHandler.setRequestCache(requestCache)
return DelegatingServerAuthenticationSuccessHandler(
SessionConfiguringAuthenticationSuccessHandler(sessionProperties),
LoggingAuthenticationSuccessHandler,
RedirectServerAuthenticationSuccessHandler() // keep this one always, otherwise redirect from OAuth login would not work
redirectSuccessHandler // keep this one always, otherwise redirect from OAuth login would not work
)
}

private fun ServerHttpSecurity.CorsSpec.configure(): ServerHttpSecurity {
val corsConfig = UrlBasedCorsConfigurationSource()
val cors = with(CorsConfiguration()) {
allowedOrigins = listOf("*")
allowedMethods = listOf("*")
allowedHeaders = listOf("*")
allowCredentials = true
this
}
corsConfig.registerCorsConfiguration("/**", cors)
return configurationSource(corsConfig).and()
}

private class AmbassadorAuthenticationFailureHandler : RedirectServerAuthenticationFailureHandler("/login?error") {
private object AmbassadorAuthenticationFailureHandler : RedirectServerAuthenticationFailureHandler("/login?error") {

private val log by LoggerDelegate()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.roche.ambassador.security.configuration

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.boot.context.properties.NestedConfigurationProperty
import org.springframework.validation.annotation.Validated
import javax.validation.Valid

@ConfigurationProperties(prefix = "ambassador.security")
@ConstructorBinding
@Validated
data class SecurityProperties(
val allowedRedirectUris: List<String> = listOf(),

@NestedConfigurationProperty
@Valid
val cors: CorsProperties = CorsProperties()
) {

data class CorsProperties(val allowedOrigins: List<String> = listOf("*"))
}
Loading

0 comments on commit 181e3e4

Please sign in to comment.