Issue
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
)
}
}
}
}
Solution
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:
- Declare a Servlet filter that extracts the header value and set it as a request attribute with a well-known name
- Create a ThreadLocalAccessor component and use it to store request attributes into the context
- Fetch the relevant attribute from your
DataFetcher
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;
}
Answered By - Brian Clozel