1

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?

Denis Abakumov
  • 355
  • 3
  • 11

1 Answers1

1

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 nulls on 2n and 4th parameters is OK because they have no usage within delegated resolveArgument as you can check here.

Denis Abakumov
  • 355
  • 3
  • 11
sedooe
  • 3,100
  • 2
  • 22
  • 27
  • Brilliant! I tested this approach in my service - and it works! Thank you. – Denis Abakumov Oct 03 '22 at 01:49
  • Hi, I tried adding that code block, but getting a cast exception at `(DgsWebMvcRequestData) context.getRequestData();`, I think the issue may be because I am using the webflux version of the DGS library so tried going down the `ReactiveDgsContext.from(dataFetchingEnvironment);` route but have no idea how to get the NativeWebRequest object - do you have any idea how one could adapt your code block to work in a reactive context? – Henry Hargreaves May 23 '23 at 19:45