Skip to content

Support for Reactive Method Security #1332

@tabneib

Description

@tabneib

Hi,

I am setting up oauth2 login for my project. Everything works fine until it comes to method-level security. The Reactor's Context seems not to be integrated, therefore PreAuthorize cannot determine the current user role. I also tried to use GraphQLContext to gain access to the Reactor's Context, but it also does not work. I have done some research but have not yet found any reliable information/examples regarding authorization for graphql-kotlin. It would be great if we could do method-level security as usual.

I'm using graphql-kotlin-spring-server 5.3.1 and spring boot 2.6.2

My simplified SecurityConfig looks like this:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class SecurityConfiguration {

  @Bean
  fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http
      .oauth2Login()
      .and().oauth2Client()
      .and().authorizeExchange().anyExchange().authenticated()
      .and()
      .build()
  }
}

My sample queries:
currentRole always throws IllegalStateException due to empty context
foo and bar always throw DeniedException

@Component
class FooQuery: Query {

  fun currentRole(): Mono<String> {
      return ReactiveSecurityContextHolder.getContext()
        .switchIfEmpty(Mono.error(IllegalStateException("Context is empty!")))
        .map(SecurityContext::getAuthentication)
        .map(Authentication::getPrincipal)
        .cast(DefaultOAuth2User::class.java)
        .map { user -> user.authorities.first().toString() }
   }

  // Reactive Method Security requires the return type of the method to be a 
  // org.reactivestreams.Publisher (i.e. Mono/Flux) 
  // or the function must be a Kotlin coroutine function. 
  // This is necessary to integrate with Reactor’s Context.
  @PreAuthorize("hasRole('USER')")
  fun foo(): Mono<String> = Mono.just("foo")
    
  @PreAuthorize("hasRole('USER')")
  suspend fun bar() = "bar"
}

My custom FunctionDataFetcher:

class CustomFunctionDataFetcher(
  target: Any?,
  fn: KFunction<*>,
  objectMapper: ObjectMapper,
  appContext: ApplicationContext
) : SpringDataFetcher(target, fn, objectMapper, appContext) {

  override fun get(environment: DataFetchingEnvironment): Any? = when (val result = super.get(environment)) {
    is Mono<*> -> result.toFuture()
    else -> result
  }
}

In contrast, Method-level security works in the context of Rest Controller, for example:

@RestController
class SomeClass {
  @PreAuthorize("hasRole('USER')")
  @GetMapping("/currentRole")
  fun getCurrentRole(): Mono<String> {
    return ReactiveSecurityContextHolder.getContext()
      .switchIfEmpty(Mono.error(IllegalStateException("Context is empty")))
      .map(SecurityContext::getAuthentication)
      .map(Authentication::getPrincipal)
      .cast(DefaultOAuth2User::class.java)
      .map { user -> user.authorities.first().toString() }
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions