2

I have a Spring GraphQL project. Each data fetcher (@SchemaMapping) will get data from a remote API protected by authentication.

I need to propagate the authorization header from the original request (that I can see inside the @QueryMapping method) to the data fetcher.

In the data fetcher I can use RequestContextHolder to get the request and the headers like this:

    val request = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes?)?.getRequest()
    val token = request?.getHeader("authorization")

This works but I am worried it could break. Spring GraphQL documentation states that:

A DataFetcher and other components invoked by GraphQL Java may not always execute on the same thread as the Spring MVC handler, for example if an asynchronous WebInterceptor or DataFetcher switches to a different thread.

I tried adding a ThreadLocalAccessor component but it seems to me from debugging and reading source code that the restoreValue method gets called only in a WebFlux project.

How can I be sure to get the right RequestContextHolder in a WebMvc project?

UPDATE

I will add some code to better explain my use case.

CurrentActivity is the parent entity while Booking is the child entity.

I need to fetch the entities from a backend with APIs protected by authentication. I receive the auth token in the original request (the one with the graphql query).

CurrentActivityController.kt

@Controller
class CurrentActivityController @Autowired constructor(
    val retrofitApiService: RetrofitApiService,
    val request: HttpServletRequest
) {

    @QueryMapping
    fun currentActivity(graphQLContext: GraphQLContext): CurrentActivity {
        // Get auth token from request.
        // Can I use the injected request here?
        // Or do I need to use Filter + ThreadLocalAccessor to get the token?
        val token = request.getHeader("authorization")
        // Can I save the token to GraphQL Context?
        graphQLContext.put("AUTH_TOKEN", token) 
        return runBlocking {
        // Authenticated API call to backend to get the CurrentActivity
            return@runBlocking entityretrofitApiService.apiHandler.activitiesCurrent(mapOf("authorization" to token))
        }
    }
}

BookingController.kt

@Controller
class BookingController @Autowired constructor(val retrofitApiService: RetrofitApiService) {

    @SchemaMapping
    fun booking(
        currentActivity: CurrentActivity,
        graphQLContext: GraphQLContext,
    ): Booking? {
        // Can I retrieve the token from GraphQL context?
        val token: String = graphQLContext.get("AUTH_TOKEN")
        return runBlocking {
            // Authenticated API call to backend to get Booking entity
            return@runBlocking currentActivity.currentCarBookingId?.let { currentCarBookingId ->
                retrofitApiService.apiHandler.booking(
                    headerMap = mapOf("authorization" to token),
                    bookingId = currentCarBookingId
                )
            }
        }
    }
}
Ena
  • 3,481
  • 36
  • 34

1 Answers1

3

The ThreadLocalAccessor concept is really meant as a way to store/restore context values in an environment where execution can happen asynchronously, on a different thread if no other infrastructure already supports that.

In the case of Spring WebFlux, the Reactor context is already present and fills this role. A WebFlux application should use reactive DataFetchers and the Reactor Context natively.

ThreadLocalAccessor implementations are mostly useful for Spring MVC apps. Any ThreadLocalAccessor bean will be auto-configured by the starter.

In your case, you could follow one of the samples and have a similar arrangement:

I tried adding a ThreadLocalAccessor component but it seems to me from debugging and reading source code that the restoreValue method gets called only in a WebFlux project.

Note that the restoreValue is only called if the current Thread is not the one values where extracted from originally (nothing needs to be done, values are already in the ThreadLocal).

I've successfully tested this approach, getting the "authorization" HTTP header value from the RequestContextHolder. It seems you tried this approach unsuccessfully - could you try with 1.0.0-M3 and let us know if it doesn't work? You can create an issue on the project with a link to a sample project that reproduces the issue.

Alternate solution

If you don't want to deal with ThreadLocal-bound values, you can always use a WebInterceptor to augment the GraphQLContext with custom values.

Here's an example:

@Component
public class AuthorizationWebInterceptor implements WebInterceptor {
    @Override
    public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
        String authorization = webInput.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        webInput.configureExecutionInput((input, inputBuilder) ->
                inputBuilder
                        .graphQLContext(contextBuilder -> contextBuilder.put("Authorization", authorization))
                        .build()
        );
        return chain.next(webInput);
    }
}

With that, you can fetch that value from the GraphQL context:

    @QueryMapping
    public String greeting(GraphQLContext context) {
        String authorization = context.getOrDefault("Authorization", "default");
        return "Hello, " + authorization;
    }
Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
  • For DataFetcher I mean [@SchemaMapping](https://github.com/spring-projects/spring-graphql/blob/main/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java) annotated methods. In the sample the request attribute is retrieved from a @QueryMapping method. I will try with the version you suggest and I will be back. Thanks! – Ena Nov 02 '21 at 08:00
  • I tried with your version but it still doesn't call restore from ThreadLocalAccessor. If I put the auth token in GraphQLContext from the @ QueryMapping method can I safely get it from @ SchemaMapping method? Can I be sure that the value will be the one of the original request? I will update my question with some code. – Ena Nov 02 '21 at 09:11
  • I've edited my answer to address your comments. – Brian Clozel Nov 08 '21 at 10:38
  • Thank you, web interceptor example is really useful. I didn't know restoreValues is called only if different thread (makes sense..). Just for the sake of curiosity, do you think it's right or wrong to inject the original request in the `@Controller` annotated class? And what about having the request as a parameter of the `@QueryMapping` annotated method? This works in a plain mvc rest controller. – Ena Nov 08 '21 at 14:45
  • Because GrpahQL is transport-agnostic, I don't think the HTTP request should be resolved as a Controller method argument. Queries could come from HTTP, websocket or even a future RSocket transport. – Brian Clozel Nov 09 '21 at 17:46