0

We are using spqr graphQl with Spring. It works great and simplifies a lot, thanks for that! Right now we are using a ResolverInterceptor for authorization: A JWT-Token is read from DefaultGlobalContext and validated. We are also able to determine the current username from this token. So now my question is: Can I store this username in a spring bean? Is there any connection between spring beans and the context? What we do at the moment is to inject the DefaultGlobalContext as a @GraphQLRootContext into every method of our @GraphQLApi and get the user this way. It would be nicer if we could just read it once, store it somewhere and have access in a spring managed service. Does anyone know how to do that? Thanks in advance, Matthias

kaqqao
  • 12,984
  • 10
  • 64
  • 118
Matthias
  • 551
  • 4
  • 11
  • I'd use a central data store like redis to store the user and its JWT. But I'm not sure if there's a Java specific way. – Vatsal Jain Sep 25 '19 at 15:13

1 Answers1

3

There's a few things you can do.

1. Custom ArgumentInjector

The simplest way (and the one I'd recommend) is wiring a custom ArgumentInjector. You can make it react to some annotation, or just by type. E.g.

@Query
public Book book(String id, @LoggedIn User user) {...}

And an injector like:

public UserInjector implement ArgumentInjector {
    @Override
    public Object getArgumentValue(ArgumentInjectorParams params) {
        ResolutionEnvironment env = params.getResolutionEnvironment();
        return extractUserFromRootContext(env.rootContext);
    }

    @Override
    public boolean supports(AnnotatedType type, Parameter parameter) {
        //You can also check type.getType().equals(User.class), but IMHO explicit is always better
        return parameter != null && parameter.isAnnotationPresent(LoggedIn.class);
    }
}

2. Custom GraphQLExecutor and a request-scoped bean

Another way is to have a @RequestScoped bean that you inject both into your GraphQLExecutor and into your @GraphQLApi class.

@Component
//implements GraphQLServletExecutor
public class CustomGraphQLExecutor extends DefaultGraphQLExecutor {

    //This is @RequestScoped
    private UserHolder userHolder;

    @Autowired
    public CustomGraphQLExecutor(
            ServletContextFactory contextFactory,
            Optional<DataLoaderRegistryFactory> registryFactory,
            UserHolder userHolder) {

        super(contextFactory, registryFactory.orElse(null));
        this.userHolder = userHolder;
    }

    @Override
    public Map<String, Object> execute(GraphQL graphQL, GraphQLRequest graphQLRequest, NativeWebRequest nativeRequest) {
        userHolder.setUser(getUserFromTheRequest(nativeRequest));
        return super.execute(graphQL, graphQLRequest, nativeRequest);
    }
}

@Service
@GraphQLApi
public class BookService {

    @Autowired
    private UserHolder userHolder; //@RequestScoped

    @Query
    public Book book(String id) {
        //Access userHolder as needed
        ... 
    }
}

I do not like this approach as it creates an invisible dependency to the ThreadLocal. If at any point you start making your resolvers asynchronous (which is highly probable with graphql-java), you'll need specially wrapped thread pools or the things won't work at all. (Also, if you ever turn to WebFlux, it likely won't work at all, as graphql-java isn't Reactor-aware and can't propagate the context).

3. Use Spring Security

Since all you're doing is basic Spring Security stuff, just use the features it gives you. It has the same issues as the request-scoped bean approach, but you at least don't have to do anything yourself. Spring already gives you SecurityContext access from anywhere. And if you customize the controller class, you can also inject Principal / Authentication directly into the controller methods.

kaqqao
  • 12,984
  • 10
  • 64
  • 118