I'm working on a Spring Boot service that has both a REST controller and a Netflix DGS GraphQL component. REST methods are protected with Spring Security, and whenever the current username is required, I add a method argument using the @AuthenticationPrincipal
annotation, which gives me access to the authenticated user info:
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
@RestController
public class ActionController {
@GetMapping("/getActions")
public List<ActionResponse> getActions(@AuthenticationPrincipal UserDetails userDetails) {
return actionService.getActions(userDetails.getUsername());
}
}
Now I want the same functionality for GraphQL methods implemented through Netflix DGS. But when I try to use the @AuthenticationPrincipal
argument (like in the first example) it always equals null
. The workaround I found is to manually assign the userDetails from the SecurityContextHolder:
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsQuery;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
@DgsComponent
public class ActionDatafetcher {
@DgsQuery
public List<Map<String, Object>> actions(@AuthenticationPrincipal UserDetails userDetails) {
// The following line works well:
// userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = userDetails.getUsername(); // ===> NullPointerException here
return actionService.getActionsMap(username);
}
}
How can I get @AuthenticationPrincipal to work in a DgsComponent?
Even though Spring Security's AuthenticationPrincipalArgumentResolver
is in the application context, it's not picked up by DGS by default. You can achieve this by implementing DGS' own ArgumentResolver
and delegating its work to Spring's AuthenticationPrincipalArgumentResolver
.
So all you need to create is this:
[Kotlin]
@Component
class DgsAuthenticationPrincipalArgumentResolver : ArgumentResolver {
private val delegate = AuthenticationPrincipalArgumentResolver()
override fun supportsParameter(parameter: MethodParameter): Boolean {
return delegate.supportsParameter(parameter)
}
override fun resolveArgument(parameter: MethodParameter, dfe: DataFetchingEnvironment): Any? {
val request = (DgsDataFetchingEnvironment(dfe).getDgsContext().requestData as DgsWebMvcRequestData).webRequest as NativeWebRequest
return delegate.resolveArgument(parameter, null, request, null)
}
}
[Java]
@Component
public class DgsAuthenticationPrincipalArgumentResolver implements ArgumentResolver {
private final AuthenticationPrincipalArgumentResolver delegate = new AuthenticationPrincipalArgumentResolver();
@Nullable
@Override
public Object resolveArgument(@NotNull MethodParameter parameter, @NotNull DataFetchingEnvironment dfe) {
DgsContext context = ((DataFetchingEnvironmentImpl) dfe).getContext();
DgsWebMvcRequestData requestData = (DgsWebMvcRequestData) context.getRequestData();
NativeWebRequest request = requestData == null ? null : (NativeWebRequest) requestData.getWebRequest();
return delegate.resolveArgument(parameter, null, request, null);
}
@Override
public boolean supportsParameter(@NotNull MethodParameter parameter) {
return delegate.supportsParameter(parameter);
}
}
Passing null
s on 2n and 4th parameters is OK because they have no usage within delegated resolveArgument
as you can check here.