graphql-java-kickstart / graphql-spring-boot

GraphQL and GraphiQL Spring Framework Boot Starters - Forked from oembedler/graphql-spring-boot due to inactivity.
https://www.graphql-java-kickstart.com/spring-boot/
MIT License
1.5k stars 325 forks source link

Possibility to add authorization headers to the GraphQLTestSubscription #919

Closed maxkramer closed 1 year ago

maxkramer commented 1 year ago

Describe the bug Hi all, I'm getting the following error when attempting to create a subscription to one of our endpoints, which is protected by OAuth2. The exception makes sense, however I'm unable to find a way to attach the token as a header to the connection that's being made.

I can see that headers can be added through the TestWebSocketClientConfigurator, but no way to add custom headers to it, neither switch it out for a different implementation. Is there something I'm missing on the session that would allow this?

Caused by: javax.websocket.DeploymentException: Failed to handle HTTP response code [401]. Unsupported Authentication scheme [Bearer] returned in response
    at app//org.apache.tomcat.websocket.WsWebSocketContainer.processAuthenticationChallenge(WsWebSocketContainer.java:524)
    at app//org.apache.tomcat.websocket.WsWebSocketContainer.connectToServerRecursive(WsWebSocketContainer.java:396)
    at app//org.apache.tomcat.websocket.WsWebSocketContainer.connectToServer(WsWebSocketContainer.java:185)
    at app//com.graphql.spring.boot.test.GraphQLTestSubscription.initClient(GraphQLTestSubscription.java:532)
    at app//com.graphql.spring.boot.test.GraphQLTestSubscription.init(GraphQLTestSubscription.java:105)

To Reproduce Steps to reproduce the behavior:

  1. Create an subscription resolver protected by isAuthorized()
  2. Attempt to fetch data from this endpoint in a test:
    graphQLTestSubscription
            .init("some-subscription.graphql")
            .awaitAndGetAllResponses(Duration.ofSeconds(1))

Expected behavior It should take into account @MockUser or ideally allow setting custom headers especially for authentication. GraphQLTestTemplate specifically has graphQLTestTemplate.withBearerAuth().

maxkramer commented 1 year ago

@BlasiusSecundus might you have a recommendation here?

vikforfda commented 1 year ago

Hi @maxkramer were you able to figure out how to send oAuth access tokens to Spring Boot based GraphQL dynamically / programmatically? We are trying to do the same thing where we are launching GraphQL from a web page. We want the web page to pass the oAuth JWT access tokens so that GraphQL can enforce authorization when user executes queries using the information in the token.

BlasiusSecundus commented 1 year ago

Hi @maxkramer, sorry for not responding earlier. AFAIK there is no standard way to handle authentication via GraphQL subscriptions.

One possibility is to put the auth token in the connection_init (via providing a payload for the GraphQLTestSubscription.init method) or the start message payload. Then handling it on the other side by providing an implementation of ApolloSubscriptionConnectionListener.

Then verifying the token and putting necessary things into Spring Security context. See also https://github.com/graphql-java-kickstart/graphql-java-servlet/discussions/134

BlasiusSecundus commented 1 year ago

See also a similar approach of the Apollo Server JS implementation: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#onconnect-and-ondisconnect

maxkramer commented 1 year ago

Hi @vikforfda, @BlasiusSecundus, I did indeed manage to get a working solution. I can't remember the exact resource I used for it, but the links you shared are very close. I'm not very happy with the solution in the sense that I would've hoped the library would've supported this case specifically as it's something supported by Apollo out of the box with the connection params. I know there's no "best practice" or even recommended practice for handling auth tokens with subscriptions, but it needs to start from somewhere! Hope this code is helpful!

@Component
internal class GraphQLSubscriptionAuthenticationListener(
    private val authenticationProvider: AuthenticationProvider,
) : ApolloSubscriptionConnectionListener {
    override fun onConnect(session: SubscriptionSession, message: OperationMessage) {
        val authToken = (message.payload as Map<*, *>)[AUTH_TOKEN_PAYLOAD_KEY] as? String
        if (authToken != null) {
            val authentication = authenticationProvider.authenticate(BearerTokenAuthenticationToken(authToken))
            SecurityContextHolder.getContext().authentication = authentication
            session.userProperties[AUTHENTICATION_USER_PROPERTY_KEY] = authentication
        }
    }

    override fun onStart(session: SubscriptionSession, message: OperationMessage) {
        val authentication = session.userProperties[AUTHENTICATION_USER_PROPERTY_KEY] as? Authentication
        SecurityContextHolder.getContext().authentication = authentication
    }

    companion object {
        private const val AUTH_TOKEN_PAYLOAD_KEY = "authToken"
        private const val AUTHENTICATION_USER_PROPERTY_KEY = "AUTHENTICATION"
    }
}

Frontend client creation:

const underlyingSubscriptionClient = new SubscriptionClient(getUrl(Path.GraphqlSubscriptions), {
  // https://www.npmjs.com/package/subscriptions-transport-ws
  connectionParams: async () => ({
    authToken: jwtToken,
  }),
  lazy: true,
})

Where possible, it would still be great to get some more customisability in the WebSocketClientConfigurator where can have the ability for a more flexibility implementation.

vikforfda commented 1 year ago

@maxkramer @BlasiusSecundus Thanks for sharing your solutions. I have one clarifying question for @maxkramer (I am new to Playground). I see in your code you are referring to ApolloSubscriptionConnectionListener. In your case did you use the Apollo Server version of GraphQL since we want to use the Open source version of GraphQL

skesani commented 1 year ago

@maxkramer @BlasiusSecundus Will this work with the embedded /playground? please see my question here.

https://github.com/graphql-java-kickstart/graphql-spring-boot/discussions/928

maxkramer commented 1 year ago

@skesani we're just discussing the webhooks here, but re the headers, I haven't personally played around with setting them automatically via yaml as our access tokens aren't long living. Somebody I worked with in the past created a bookmark that ran some js that fetched a new token and injected it automatically into the http headers section. But according to the docs, I believe it should be this in the application.yaml:

graphql:
    playground:
        headers:
            Authorization: Bearer ey.someToken

There's also a graphql.playground.settings.editor.reuse-headers that perhaps is related?

@vikforfda we're using the graphql kickstart spring boot starter, I'm not sure which one that's using as we haven't had a need to look into anything further.

BlasiusSecundus commented 1 year ago

In your case did you use the Apollo Server version of GraphQL since we want to use the Open source version of GraphQL

@vikforfda ApolloSubscriptionConnectionListener is actually part of this library: https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/master/graphql-java-kickstart/src/main/java/graphql/kickstart/execution/subscriptions/apollo/ApolloSubscriptionConnectionListener.java

BlasiusSecundus commented 1 year ago

@skesani Yes, it will work. The variables set in the "HTTP headers" tab will be sent to the server as the payload of the connection_init message.

skesani commented 1 year ago

@skesani Yes, it will work. The variables set in the "HTTP headers" tab will be sent to the server as the payload of the connection_init message.

@BlasiusSecundus could you provide any sample to set the token programmatically?

I am thinking to do like this.

` import graphql.kickstart.playground.GraphQLPlaygroundConfigurer; import org.springframework.stereotype.Component;

@Component public class CustomGraphQLPlaygroundConfigurer implements GraphQLPlaygroundConfigurer {

@Override
public void configure(GraphQLPlaygroundConfigBuilder builder) {
    // Add JavaScript file to the GraphQL Playground page
    builder.addPlugin(getCustomPlugin());
}

private String getCustomPlugin() {
    // Get token from some source (e.g. database, config file, etc.)
    String token = "my-token";

    // Create JavaScript code that sets the token value in the GraphQL request headers
    return "window.addEventListener('load', function (event) {\n" +
            "  GraphQLPlayground.init(document.getElementById('root'), {\n" +
            "    settings: {\n" +
            "      'request.credentials': 'same-origin',\n" +
            "      'X-AUTH-TOKEN': '" + token + "'\n" +
            "    }\n" +
            "  });\n" +
            "});";
}

}

`

BlasiusSecundus commented 1 year ago

@skesani Variables from configuration are already passed to Playground in PlaygroundController. That would be the natural place to extend its functionality so that it can take headers from sources other than configuration properties. (E.g. some configurator bean as you proposed).

BlasiusSecundus commented 1 year ago

Returning to the original question / bug report, I think this issue should be closed:

These two should be handled as separate issues (flagged as enhancement).

skesani commented 1 year ago

@skesani Variables from configuration are already passed to Playground in PlaygroundController. That would be the natural place to extend its functionality so that it can take headers from sources other than configuration properties. (E.g. some configurator bean as you proposed).

@BlasiusSecundus I created this repo and added MyConfig and MyPlaygroundController extends PlaygroundController however, I am unable to override the headers, could you please take a look? https://github.com/skesani/graphql-playground