-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(security): make CORS configurable and don't allow credentials whe…
…n using all (*) origins, fix redirections from another domain by allowing using redirect_uri
- Loading branch information
Showing
8 changed files
with
251 additions
and
79 deletions.
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
.../src/main/kotlin/com/roche/ambassador/security/configuration/BaseSecurityConfiguration.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
37 changes: 37 additions & 0 deletions
37
...application/src/main/kotlin/com/roche/ambassador/security/configuration/CorsConfigurer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
...n/com/roche/ambassador/security/configuration/RedirectUriAwareCookieServerRequestCache.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
9 changes: 9 additions & 0 deletions
9
...ication/src/main/kotlin/com/roche/ambassador/security/configuration/SecurityConfigurer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
...ication/src/main/kotlin/com/roche/ambassador/security/configuration/SecurityProperties.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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("*")) | ||
} |
Oops, something went wrong.