Spring boot security with keycloak not working for graphql - spring-boot

Apologies for the lengthy post. I have a spring boot application which contains both rest and graphql APIs. Also I'm using keycloak auth server. I want to add keycloak authentication for both apis. Setup is as follows
Dependencies
I have added following dependencies as mentioned in docs.
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>13.0.1</version>
</dependency>
//-- other dependencies
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>13.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Additionally I'm using following dependencies for spring security and expediagroup graphql kotlin support.
<dependency>
<groupId>com.expediagroup</groupId>
<artifactId>graphql-kotlin-spring-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Security Configuration
import org.keycloak.adapters.KeycloakConfigResolver
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver
import org.keycloak.adapters.springsecurity.KeycloakConfiguration
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents
import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory
import org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.context.annotation.*
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper
import org.springframework.security.core.session.SessionRegistryImpl
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
#KeycloakConfiguration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true)
#ComponentScan(basePackageClasses = [KeycloakSecurityComponents::class])
class KeycloakSecurityConfig: KeycloakWebSecurityConfigurerAdapter() {
#Autowired
var keycloakClientRequestFactory: KeycloakClientRequestFactory? = null
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
super.configure(http)
http.csrf().disable().cors().disable()
.authorizeRequests()
.anyRequest()
.permitAll()
}
#Autowired
#Throws(Exception::class)
fun configureGlobal(auth: AuthenticationManagerBuilder) {
val keycloakAuthenticationProvider = keycloakAuthenticationProvider()
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(SimpleAuthorityMapper())
auth.authenticationProvider(keycloakAuthenticationProvider)
}
#Bean
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy? {
return RegisterSessionAuthenticationStrategy(SessionRegistryImpl())
}
#Bean
fun KeycloakConfigResolver(): KeycloakConfigResolver? {
return KeycloakSpringBootConfigResolver()
}
#Bean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
fun keycloakRestTemplate(): KeycloakRestTemplate? {
return KeycloakRestTemplate(keycloakClientRequestFactory)
}
}
Endpoints
These are my sample rest and graphql endpoints.
GraphQL
import com.expediagroup.graphql.spring.operations.Query
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
import java.util.concurrent.CompletableFuture
#Component
class HelloQuery: Query {
fun helloWorld():CompletableFuture<String>{
return Mono.just("hello from graphql").toFuture()
}
}
Rest Controller
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ResponseBody
#Controller
class HelloController {
#GetMapping("/hello")
#ResponseBody
fun hello(): String {
return "hello from rest"
}
}
Results
When I run the application and test the APIs get following results.
Rest API - work successfully
GraphQL - 404 error
But when I remove all security configs graphql api also work successfully.
My problem is can I use graphql with keycloak spring security. If yes what am I doing wrong here?
Any help is appreciated.

Needed to do some changes to security config file.
adding corsFilter
adding securityWebFilterChain
Also I'm using dependency
<dependency>
<groupId>com.expediagroup</groupId>
<artifactId>graphql-kotlin-spring-server</artifactId>
<version>${graphql-kotlin-spring-server.version}</version>
</dependency>
It worked for me. Posting the answer hopefully will help someone.
Security config file
#EnableWebFluxSecurity
#Configuration
internal class SecurityConfig(
private val monitoringProperties: MonitoringProperties,
private val securityProperties: SecurityProperties,
private val graphQLConfigurationProperties: GraphQLConfigurationProperties
) {
companion object {
const val AUTHORITY_ACTUATOR = "AUTHORITY_ACTUATOR"
const val AUTHORITY_ACCESS_MONITORING = "AUTHORITY_ACCESS_MONITORING"
const val AUTHORITY_ACCESS_INTERNAL_API = "AUTHORITY_ACCESS_INTERNAL_API"
}
#Bean
#ConditionalOnProperty(value = ["spring.security.user.passwordGenerated"], matchIfMissing = true, havingValue = "false")
fun userDetailsService(): MapReactiveUserDetailsService {
val actuatorUser = User
.withUsername(securityProperties.user.name)
.password("{noop}${securityProperties.user.password}")
.authorities(AUTHORITY_ACTUATOR, AUTHORITY_ACCESS_MONITORING, AUTHORITY_ACCESS_INTERNAL_API).build()
val monitoringUser = User
.withUsername(monitoringProperties.username)
.password("{noop}${monitoringProperties.password}")
.authorities(AUTHORITY_ACCESS_MONITORING)
.build()
return MapReactiveUserDetailsService(actuatorUser, monitoringUser)
}
#Bean
fun corsWebFilter(): CorsWebFilter {
return CorsWebFilter(UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration(
"/${graphQLConfigurationProperties.endpoint}",
CorsConfiguration().apply {
allowCredentials = true
allowedOrigins = listOf("*")
allowedHeaders = listOf("*")
allowedMethods = listOf("*")
}
)
registerCorsConfiguration(
"/${graphQLConfigurationProperties.subscriptions.endpoint}",
CorsConfiguration().apply {
allowCredentials = true
allowedOrigins = listOf("*")
allowedHeaders = listOf("*")
allowedMethods = listOf("*")
}
)
registerCorsConfiguration(
"/web/*",
CorsConfiguration().apply {
allowCredentials = true
allowedOrigins = listOf("*")
allowedHeaders = listOf("*")
allowedMethods = listOf("*")
}
)
})
}
#Bean
fun securityWebFilterChain(
http: ServerHttpSecurity
): SecurityWebFilterChain {
return http {
httpBasic { }
configureKeycloakAuth()
csrf { disable() }
authorizeExchange {
// actuator and monitoring
authorize(EndpointRequest.to("health", "info"), permitAll)
authorize(EndpointRequest.to(PrometheusScrapeEndpoint::class.java), hasAnyAuthority(AUTHORITY_ACCESS_MONITORING))
authorize(EndpointRequest.toAnyEndpoint(), hasAuthority(AUTHORITY_ACTUATOR))
authorizeGraphQL()
// rest
authorize(anyExchange, denyAll)
}
requestCache { disable() }
}
}
private fun ServerHttpSecurityDsl.configureKeycloakAuth() {
oauth2ResourceServer {
bearerTokenConverter = ServerBearerTokenAuthenticationConverter().apply { setAllowUriQueryParameter(true) }
jwt { jwtAuthenticationConverter = KeycloakJwtAuthenticationConverter() }
}
}
private fun AuthorizeExchangeDsl.authorizeGraphQL() {
authorize(ServerWebExchangeMatchers.pathMatchers(HttpMethod.OPTIONS, "/${graphQLConfigurationProperties.endpoint}"), permitAll)
authorize(antPattern = "/${graphQLConfigurationProperties.endpoint}", access = hasAnyAuthority("ADMIN", "USER"))
authorize(antPattern = "/${graphQLConfigurationProperties.subscriptions.endpoint}", access = hasAnyAuthority("ADMIN"))
authorize(antPattern = "/web/*", access = permitAll)
if (graphQLConfigurationProperties.playground.enabled) {
authorize(antPattern = "/${graphQLConfigurationProperties.playground.endpoint}", access = permitAll)
}
if (graphQLConfigurationProperties.sdl.enabled) {
authorize(antPattern = "/${graphQLConfigurationProperties.sdl.endpoint}", access = permitAll)
}
}
}
class KeycloakJwtAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
private val delegate = JwtAuthenticationConverter()
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
return Mono.just(jwt)
.map { source: Jwt -> delegate.convert(source) }
.map { auth ->
when (auth) {
is JwtAuthenticationToken -> try {
KeycloakToken.fromOauth2Token(auth)
} catch (e: Exception) {
auth
}
else -> auth
}
}
}
}
class KeycloakToken(authorities: Collection<out GrantedAuthority>, private val principal: AuthenticatedUser, private val credentials: Any) :
AbstractAuthenticationToken(authorities) {
companion object {
fun fromOauth2Token(oauth2Token: JwtAuthenticationToken): KeycloakToken {
val id = UUID.fromString(oauth2Token.name)
val jwt = oauth2Token.principal as Jwt
val email = jwt.claims["email"] as? String
val firstName = jwt.claims["given_name"] as? String
val lastName = jwt.claims["family_name"] as? String
val fullName = jwt.claims["name"] as? String
if (email == null || firstName == null || lastName == null || fullName == null) {
throw RuntimeException("User Details incomplete - name not set of user with id $id: email: $email, first name: $firstName, last name: $lastName, full name: $fullName")
}
val realmAccess = jwt.claims["realm_access"] as Map<*, *>
val roles = (realmAccess["roles"] as List<*>).map { it.toString() }
val authorities = oauth2Token.authorities.toMutableList()
authorities.addAll(roles.map { SimpleGrantedAuthority(it) })
return KeycloakToken(
authorities = authorities,
principal = AuthenticatedUser(
id = id,
email = email,
firstName = firstName,
lastName = lastName,
fullName = fullName,
roles = roles.toList()
),
credentials = oauth2Token.credentials
)
.apply {
details = oauth2Token
isAuthenticated = true
}
}
}
override fun getPrincipal() = principal
override fun getCredentials() = credentials
}

Related

PATCH endpoint 404 on Upgrade to Springboot Version 3

I'm in the process of migrating my Kotlin API to Springboot version 3.
I'm having a problem where certain endpoints aren't getting hit, for example::
a GET to localhost:8081/search/hello
works just fine, here is the GET endpoint::
#PreAuthorize("hasRole('ROLE_MYROLE')")
#GetMapping("/search/{somephrase}")
suspend fun getSomething(#PathVariable("somephrase") phrase: String): someResponse {
return myService.getPhrase(phrase)
}
a PATCH to localhost:8081/update/resourceId
does not, and here is my patch endpoint::
#PreAuthorize("hasRole('ROLE_MYROLE')")
#PatchMapping("/update/{resourceId}")
suspend fun updateSomething(
#PathVariable("resourceId") resourceId: Long,
#RequestBody updateJson: JsonNode
): UpdateResponse {
return recommendationService.update(updateJson, resourceId)
}
Here is the reponse I get::
{
"timestamp": "2023-02-12 09:59:50",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/search"
}
and this in the console::
: [preHandle] PATCH /error - operationId: 00000000000000000000000000000000 |
Here is my Spring Security Config::
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.DefaultSecurityFilterChain
#Configuration
#EnableWebSecurity
class ResourceServerConfiguration {
#Bean
fun configure(http: HttpSecurity): DefaultSecurityFilterChain? {
http.authorizeHttpRequests()
.requestMatchers("/csrf").permitAll()
.requestMatchers("/**").authenticated()
.and()
.csrf()
.disable()
.oauth2ResourceServer().jwt().jwtAuthenticationConverter { AuthAwareTokenConverter().convert(it) }
return http.build()
}
}
and here is my token converter (it just adds a couple of extra claims to the token)
import org.springframework.core.convert.converter.Converter
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
class AuthAwareTokenConverter : Converter<Jwt, AbstractAuthenticationToken> {
private val jwtGrantedAuthoritiesConverter: Converter<Jwt, Collection<GrantedAuthority>> =
JwtGrantedAuthoritiesConverter()
override fun convert(jwt: Jwt): AbstractAuthenticationToken {
val claims = jwt.claims
val principal = findPrincipal(claims)
val authorities = extractAuthorities(jwt)
return AuthAwareAuthenticationToken(jwt, principal, authorities)
}
private fun findPrincipal(claims: Map<String, Any?>): String {
return if (claims.containsKey(CLAIM_USERNAME)) {
claims[CLAIM_USERNAME] as String
} else if (claims.containsKey(CLAIM_USER_ID)) {
claims[CLAIM_USER_ID] as String
} else {
claims[CLAIM_CLIENT_ID] as String
}
}
private fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> {
val authorities = mutableListOf<GrantedAuthority>().apply { addAll(jwtGrantedAuthoritiesConverter.convert(jwt)!!) }
if (jwt.claims.containsKey(CLAIM_AUTHORITY)) {
#Suppress("UNCHECKED_CAST")
val claimAuthorities = (jwt.claims[CLAIM_AUTHORITY] as Collection<String>).toList()
authorities.addAll(claimAuthorities.map(::SimpleGrantedAuthority))
}
return authorities.toSet()
}
companion object {
const val CLAIM_USERNAME = "user_name"
const val CLAIM_USER_ID = "user_id"
const val CLAIM_CLIENT_ID = "client_id"
const val CLAIM_AUTHORITY = "authorities"
}
}
class AuthAwareAuthenticationToken(
jwt: Jwt,
private val aPrincipal: String,
authorities: Collection<GrantedAuthority>
) : JwtAuthenticationToken(jwt, authorities) {
override fun getPrincipal(): String {
return aPrincipal
}
}
What I've tried::
Simplifying the PATCH endpoint to simply return a "hello" String to rule out an issue with the service.
The result is the same, so I think the point of failure must be my Spring Security configuration.
Any help/pointers would be much appreciated!
Interesting! I appear to have found the solution.
I added an extra forward slash to the end of the path in my mapping.
So I changed this::
#PatchMapping("/update/{resourceId}")
to this::
#PatchMapping("/update/{resourceId}/")
and it works!
Oddly SpringBoot version 2.X worked just fine with or without the forward slash. But SpringBoot version 3.X requires it.

Deprecated methods when using Feign with Oauth2 in SpringBoot 2.4.3

I am trying to use feign client with Oauth2 in my project based in Springboot 2.4.3. I implemented this example but the DefaultOAuth2ClientContext, OAuth2ProtectedResourceDetails and ClientCredentialsResourceDetails methods are deprecated.
pom.xml
<dependencies>
....
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
....
</dependencies>
application.yml
config:
serverUrl : exampleUrl
clientId: exampleClientId
clientSecret: exampleClientSecret
scopes: exampleScopes
feign:
post:
name: postService
url: https://localhost:8102/post
get:
name: getService
url: https://localhost:8102/get
FeignClient.java
#FeignClient(name = "example", configuration = OAuth2ClientConfig.class)
public interface FeignClient {
#PostMapping(value = "${feign.post.url}")
HttpEntity search(#PathVariable String id);
#GetMapping(value = "${feign.get.url}")
HttpEntity upload(#PathVariable String id);
}
OAuth2ClientProperties.java
#ConfigurationProperties(prefix = "config")
public class OAuth2ClientProperties {
private String serverUrl;
private String clientId;
private String clientSecret;
private List<String> scopes = new ArrayList<>();
public String getAccessTokenUri() {
return serverUrl;
}
public void setAccessTokenUri(String accessTokenUri) {
this.serverUrl = accessTokenUri;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public List<String> getScopes() {
return scopes;
}
public void setScopes(List<String> scopes) {
this.scopes = scopes;
}
}
OAuth2ClientConfig.java
import feign.RequestInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
#Configuration
#EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2ClientConfig {
#Autowired
private OAuth2ClientProperties oAuth2ClientProperties;
#Bean
public RequestInterceptor requestInterceptor() {
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(),
oAuth2ProtectedResourceDetails());
}
private OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails() {
ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
details.setClientId(oAuth2ClientProperties.getClientId());
details.setClientSecret(oAuth2ClientProperties.getClientSecret());
details.setAccessTokenUri(oAuth2ClientProperties.getAccessTokenUri());
details.setScope(oAuth2ClientProperties.getScopes());
return details;
}
}
I have not yet tested whether it works or not, but what bothers me is having deprecated methods and I have not found a solution to replace them with equivalents taken into account by the new version.
Any ideas ?

No primary or single public constructor found for interface javax.servlet.http

When I fire refreshtoken api on postman I got this type of error:
java.lang.IllegalStateException: No primary or single public
constructor found for interface javax.servlet.http.HttpServletRequest
and no default constructor found either
Here all the details of my filterclass and controller
jwtrefresh.kt:
package com.main.jwtrefresh.filter
import com.main.jwtrefresh.service.UuserService
import com.main.jwtrefresh.util.JwtRefreshTokenUtil
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import io.jsonwebtoken.ExpiredJwtException
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.userdetails.User
import org.springframework.util.StringUtils
#Component
class JwtRefreshFilterUser : OncePerRequestFilter() {
#Autowired
private lateinit var jwtRefreshTokenUtil: JwtRefreshTokenUtil
#Autowired
private lateinit var uuserService: UuserService
#Throws(ServletException::class, IOException::class)
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
try {
val jwtToken: String? = extractJwtFromRequest(request)
if (StringUtils.hasText(jwtToken) && jwtRefreshTokenUtil.validateToken(jwtToken)) {
val userDetails: UserDetails = User(jwtRefreshTokenUtil.getUsernameFromToken(jwtToken), "",
ArrayList())
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.authorities
)
SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken
} else {
println("Cannot set the Security Context")
}
} catch (ex: ExpiredJwtException) {
val isRefreshToken = request.getHeader("isRefreshToken")
val requestURL = request.requestURL.toString()
if (isRefreshToken != null && isRefreshToken == "true" && requestURL.contains("refreshToken")) {
allowRefreshToken(ex, request)
} else
request.setAttribute("Exception", ex)
} catch (ex: BadCredentialsException) {
request.setAttribute("Exception", ex)
} catch (ex: Exception) {
println(ex)
}
filterChain.doFilter(request, response)
}
private fun allowRefreshToken(ex: ExpiredJwtException, request: HttpServletRequest) {
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(null, null, null)
SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken
request.setAttribute("claims", ex.claims)
}
private fun extractJwtFromRequest(request: HttpServletRequest): String? {
val bearerToken: String = request.getHeader("Authorization")
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
bearerToken.substring(7, bearerToken.length)
}
return null
}
}
jwtcontroller.kt:
#PostMapping("/refreshToken")
#Throws(Exception::class)
fun refreshToken(#ModelAttribute request: HttpServletRequest): ResponseEntity<*>? {
val claims = request.getAttribute("claims") as DefaultClaims
val expectedMap = getMapFromIoJsonwebtokenClaims(claims)
val token: String = jwtRefreshTokenUtil.doGenerateRefreshToken(expectedMap, expectedMap["sub"].toString())
return ResponseEntity(ResToken(token),HttpStatus.OK)
}
private fun getMapFromIoJsonwebtokenClaims(claims: DefaultClaims): Map<String, Any> {
val expectedMap: MutableMap<String, Any> = HashMap()
for ((key, value) in claims) {
expectedMap[key] = value
}
return expectedMap
}

Spring-Security: Getting 401s with CORS Preflights (despite http.cors())

For starters i want to secure a part of my rest-api with basic auth.
When I try to access endpoints from a react client, I keep getting 401's in the preflight requests.
I tried to follow this guide without success:
https://www.baeldung.com/spring-security-cors-preflight
i'm not sure if this is part of the problem, but another part can only be accessed with certain custom http headers.
I'm using Method Security:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
class MethodSecurityConfig : GlobalMethodSecurityConfiguration() {
override fun customMethodSecurityMetadataSource(): MethodSecurityMetadataSource = SecurityMetadataSource()
override fun accessDecisionManager(): AccessDecisionManager = super.accessDecisionManager().apply {
this as AbstractAccessDecisionManager
decisionVoters.add(PrivilegeVoter())
}
}
And this is my Security config:
#Configuration
#EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class SecurityConfig : WebSecurityConfigurerAdapter() {
private val deviceRequestHeaderName: String = "X-DEVICE-ID"
private val platformRequestHeaderName: String = "X-PLATFORM-ID"
#Autowired
lateinit var users: AppUserRepository
#Autowired
lateinit var backendUsers: BackendUserRepository
#Autowired
lateinit var roles: RoleRepository
val authManager by lazy { authenticationManager() }
private val authProvider by lazy {
PreAuthenticatedAuthenticationProvider().apply {
setPreAuthenticatedUserDetailsService {
val authId = it.principal as UserAuthId
if (authId.deviceId == null) throw UsernameNotFoundException("No device-id to search for.")
if (authId.platform == null) throw UsernameNotFoundException("Platform not specified.")
val platform = try {
ApplicationPlatform.valueOf(authId.platform)
} catch (e: IllegalArgumentException) {
throw UsernameNotFoundException("Unknown platform ${authId.platform}.")
}
val existingUser = users.findByUserDeviceIdAndPlatform(authId.deviceId, platform)
if (existingUser != null) return#setPreAuthenticatedUserDetailsService existingUser
users.save(AppUser(authId.deviceId, platform, roles))
}
}
}
val passwordEncoder by lazy { BCryptPasswordEncoder() }
private val deviceIdFilter by lazy {
HeaderFieldAuthFilter(deviceRequestHeaderName, platformRequestHeaderName).apply {
setAuthenticationManager(authManager)
}
}
override fun configure(auth: AuthenticationManagerBuilder) = auth {
authenticationProvider(authProvider)
val userDetailsService = BackendUserDetailsService(backendUsers)
userDetailsService(userDetailsService).passwordEncoder(passwordEncoder)
}
override fun configure(http: HttpSecurity) = http {
session {
sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
exceptionHandling()
addFilter(deviceIdFilter)
authorizeRequests().anyRequest().authenticated()
csrf().disable()
httpBasic()
cors().configurationSource { request ->
CorsConfiguration().apply {
allowedOrigins = listOf(ALL)
allowedMethods = listOf(GET, POST, DELETE, PUT, OPTIONS).map { it.name }
allowedHeaders = listOf(ALL)
allowCredentials = true
maxAge = 3600
}
}
}
#Bean
fun auditorProvider(): AuditorAware<User> = AuditorAware<User> {
val authentication = SecurityContextHolder.getContext().authentication
val user = authentication.run { if (isAuthenticated) principal as? User else null }
return#AuditorAware Optional.ofNullable(user)
}
}
I could solve by manually exclude the preflight requests from authentication.
adding this
antMatchers(OPTIONS, "/**").permitAll()
to the authorizeRequests() configuration accomplishes that.
Note that Options is a direct reference to the HttpMethod enum value, imported like this
import org.springframework.http.HttpMethod.*
Stackoverflow posts that helped me get there:
Response for preflight has invalid HTTP status code 401 - Spring
How to configure CORS in a Spring Boot + Spring Security application?
Originally I had assumed, that this should have been handled by the cors configuration - which it was apparently not.
To enable CORS for a single rest endpoint you can annotate it with:
#CrossOrigin
To allow for CORS for all endpoints you can have a bean like so:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
.allowedHeaders("*");
}
};

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:
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?
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()
}

Resources