javaspring-bootauthenticationspring-securitynetflix-dgs

How to enable @AuthenticationPrincipal argument in a DGS query


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?


Solution

  • 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.