Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

It is not possible to pass Authorization header to WebSocket #3967

Closed
hudymi opened this issue Oct 2, 2018 · 17 comments
Closed

It is not possible to pass Authorization header to WebSocket #3967

hudymi opened this issue Oct 2, 2018 · 17 comments

Comments

@hudymi
Copy link

hudymi commented Oct 2, 2018

Intended outcome:

Since Apollo Client 2 it is not possible to pass custom HTTP Header to WebSocket connection. In Apollo Client 1 it was possible by Middleware, but since version 2 it is not. I tried with additional link concat, or by applying Middleware to subscriptionClient.

Sample with authorized link:

  const httpLink = createHttpLink({ uri: `https://ws.server.local/graphql` });
  const wsLink = new WebSocketLink({
    uri: `wss://ws.server.local/graphql`,
    options: {
      reconnect: true,
    },
  });

  const middlewareLink = new ApolloLink((operation, forward) => {
    operation.setContext({
      headers: {
        authorization: getBearerToken() || null,
      },
    });
    return forward(operation);
  });

  const authorizedLink = middlewareLink.concat(wsLink);

  const link = split(
    ({ query }) => {
      const { kind, operation } = getMainDefinition(query);
      return kind === 'OperationDefinition' && operation === 'subscription';
    },
    authorizedLink,
    httpLink,
  );

  const client = new ApolloClient({
    link: link,
    ...
  });

Sample with subscriptionClient Middleware

  const httpLink = createHttpLink({ uri: `https://ws.server.local/graphql` });
  const wsLink = new WebSocketLink({
    uri: `wss://ws.server.local/graphql`,
    options: {
      reconnect: true,
    },
  });

  const subscriptionMiddleware = {
    applyMiddleware(options, next) {
      console.log(options);
      options.setContext({
        headers: {
          authorization: getBearerToken() || null,
        },
      });
      next();
    },
  };

  wsLink.subscriptionClient.use([subscriptionMiddleware]);

  const link = split(
    ({ query }) => {
      const { kind, operation } = getMainDefinition(query);
      return kind === 'OperationDefinition' && operation === 'subscription';
    },
    wsLink,
    httpLink,
  );

  const client = new ApolloClient({
    link: link,
    ...
  });

Versions

  System:
    OS: macOS High Sierra 10.13.6
  Binaries:
    Node: 9.5.0 - /usr/local/bin/node
    npm: 5.6.0 - /usr/local/bin/npm
  Browsers:
    Chrome: 69.0.3497.100
    Firefox: 60.0.2
    Safari: 12.0
  npmPackages:
    apollo-boost: ^0.1.3 => 0.1.15 
    apollo-client: ^2.4.2 => 2.4.2 
    apollo-link-ws: ^1.0.9 => 1.0.9 
    react-apollo: ^2.0.4 => 2.1.11 
@coco98
Copy link

coco98 commented Nov 1, 2018

@michal-hudy Does this not work for you?

const wsLink = new WebSocketLink(
  new SubscriptionClient(WS_URL, {
    reconnect: true,
    timeout: 30000,
    connectionParams: {
      headers: {
        Authorization: "Bearer xxxxx"
      }
    }
  })
);

@pkosiec
Copy link

pkosiec commented Nov 5, 2018

It won't work as WebSocket API in browsers doesn't support setting custom headers, apart from the value of Sec-Websocket-Protocol header.
https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api/41521871

@hudymi
Copy link
Author

hudymi commented Nov 5, 2018

Please see @pkosiec comment.

@hudymi hudymi closed this as completed Nov 5, 2018
@pyankoff
Copy link

Struggled with adding async function for connection params, was getting start received before the connection is initialised error. Fixed it by adding lazy: true to connection options:

const wsLink = new WebSocketLink({
  uri: WS_URL,
  options: {
    lazy: true,
    reconnect: true,
    connectionParams: async () => {
      const token = await getToken();
      return {
        headers: {
          Authorization: token ? `Bearer ${token}` : "",
        },
      }
    },
  },
})

Just in case someone having the same issue.

@georgyfarniev
Copy link

@pyankoff how can I read this header on server side?

@pyankoff
Copy link

pyankoff commented Aug 8, 2019

@georgyfarniev I think it depends on your server side implementation. I was using Hasura and didn't have to handle it.

@LermanR
Copy link

LermanR commented Jan 8, 2020

@pyankoff I'm using Hasura as well. how did you handle token change (refresh after expiration) with WebSocketLink?

@dreamer01
Copy link

Hey, @pyankoff can you please share how you were able to re-authenticate the user once the token has expired.
I am able to get new accessToken using refreshToken but I am failing to pass the new accessToken to client.
I have read through few examples and all direct towards using operation.setContext , which I was unable to implement while using WebSocketLink.

Thanks.

@LermanR
Copy link

LermanR commented Feb 13, 2020

@dreamer01 If i understand currently your question is like mine.
If so then on WebSocketLink you can only pass tokens on connect.
if your token expires and you get a new one, you need to reconnect with the new tokens.
here is an example.
And if that wasn't your question, then maybe it will help someone else.. :)

@dreamer01
Copy link

@dreamer01 If i understand currently your question is like mine.
If so then on WebSocketLink you can only pass tokens on connect.
if your token expires and you get a new one, you need to reconnect with the new tokens.
here is an example.
And if that wasn't your question, then maybe it will help someone else.. :)

Hey, @lroy83 this what exactly I meant, thank you for sharing the code.

@garrettmk
Copy link

@lroy83 Oh man, I spent a long time searching for an example like what you posted. Thank you for sharing!

@ARMATAV
Copy link

ARMATAV commented Apr 9, 2020

@dreamer01 If i understand currently your question is like mine.
If so then on WebSocketLink you can only pass tokens on connect.
if your token expires and you get a new one, you need to reconnect with the new tokens.
here is an example.
And if that wasn't your question, then maybe it will help someone else.. :)

Finally an actual code example for how to refresh tokens with websocket properly! Pasting it here just because the past 2 examples I've been linked 404'd.

import { ApolloClient } from 'apollo-client'
import { split, from } from 'apollo-link'
import { createUploadLink } from 'apollo-upload-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import MessageTypes from 'subscriptions-transport-ws/dist/message-types'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities'
import { createPersistedQueryLink } from 'apollo-link-persisted-queries'
import { setContext } from 'apollo-link-context'
import { withClientState } from 'apollo-link-state'

// Create the apollo client
export function createApolloClient ({
  // Client ID if using multiple Clients
  clientId = 'defaultClient',
  // URL to the HTTP API
  httpEndpoint,
  // Url to the Websocket API
  wsEndpoint = null,
  // Token used in localstorage
  tokenName = 'apollo-token',
  // Enable this if you use Query persisting with Apollo Engine
  persisting = false,
  // Is currently Server-Side Rendering or not
  ssr = false,
  // Only use Websocket for all requests (including queries and mutations)
  websocketsOnly = false,
  // Custom starting link.
  // If you want to replace the default HttpLink, set `defaultHttpLink` to false
  link = null,
  // If true, add the default HttpLink.
  // Disable it if you want to replace it with a terminating link using `link` option.
  defaultHttpLink = true,
  // Options for the default HttpLink
  httpLinkOptions = {},
  // Custom Apollo cache implementation (default is apollo-cache-inmemory)
  cache = null,
  // Options for the default cache
  inMemoryCacheOptions = {},
  // Additional Apollo client options
  apollo = {},
  // apollo-link-state options
  clientState = null,
  // Function returning Authorization header token
  getAuth = defaultGetAuth,
  // Local Schema
  typeDefs = undefined,
  // Local Resolvers
  resolvers = undefined,
  // Hook called when you should write local state in the cache
  onCacheInit = undefined,
}) {
  let wsClient, authLink, stateLink
  const disableHttp = websocketsOnly && !ssr && wsEndpoint

  // Apollo cache
  if (!cache) {
    cache = new InMemoryCache(inMemoryCacheOptions)
  }

  if (!disableHttp) {
    const httpLink = createUploadLink({
      uri: httpEndpoint,
      ...httpLinkOptions,
    })

    if (!link) {
      link = httpLink
    } else if (defaultHttpLink) {
      link = from([link, httpLink])
    }

    // HTTP Auth header injection
    authLink = setContext((_, { headers }) => {
      const authorization = getAuth(tokenName)
      const authorizationHeader = authorization ? { authorization } : {}
      return {
        headers: {
          ...headers,
          ...authorizationHeader,
        },
      }
    })

    // Concat all the http link parts
    link = authLink.concat(link)
  }

  // On the server, we don't want WebSockets and Upload links
  if (!ssr) {
    // If on the client, recover the injected state
    if (typeof window !== 'undefined') {
      // eslint-disable-next-line no-underscore-dangle
      const state = window.__APOLLO_STATE__
      if (state && state[clientId]) {
        // Restore state
        cache.restore(state[clientId])
      }
    }

    if (!disableHttp) {
      let persistingOpts = {}
      if (typeof persisting === 'object' && persisting != null) {
        persistingOpts = persisting
        persisting = true
      }
      if (persisting === true) {
        link = createPersistedQueryLink(persistingOpts).concat(link)
      }
    }

    // Web socket
    if (wsEndpoint) {
      wsClient = new SubscriptionClient(wsEndpoint, {
        reconnect: true,
        connectionParams: () => {
          const authorization = getAuth(tokenName)
          return authorization ? { authorization, headers: { authorization } } : {}
        },
      })

      // Create the subscription websocket link
      const wsLink = new WebSocketLink(wsClient)

      if (disableHttp) {
        link = wsLink
      } else {
        link = split(
          // split based on operation type
          ({ query }) => {
            const { kind, operation } = getMainDefinition(query)
            return kind === 'OperationDefinition' &&
              operation === 'subscription'
          },
          wsLink,
          link
        )
      }
    }
  }

  if (clientState) {
    console.warn(`clientState is deprecated, see https://vue-cli-plugin-apollo.netlify.com/guide/client-state.html`)
    stateLink = withClientState({
      cache,
      ...clientState,
    })
    link = from([stateLink, link])
  }

  const apolloClient = new ApolloClient({
    link,
    cache,
    // Additional options
    ...(ssr ? {
      // Set this on the server to optimize queries when SSR
      ssrMode: true,
    } : {
      // This will temporary disable query force-fetching
      ssrForceFetchDelay: 100,
      // Apollo devtools
      connectToDevTools: process.env.NODE_ENV !== 'production',
    }),
    typeDefs,
    resolvers,
    ...apollo,
  })

  // Re-write the client state defaults on cache reset
  if (stateLink) {
    apolloClient.onResetStore(stateLink.writeDefaults)
  }

  if (onCacheInit) {
    onCacheInit(cache)
    apolloClient.onResetStore(() => onCacheInit(cache))
  }

  return {
    apolloClient,
    wsClient,
    stateLink,
  }
}

export function restartWebsockets (wsClient) {
  // Copy current operations
  const operations = Object.assign({}, wsClient.operations)

  // Close connection
  wsClient.close(true)

  // Open a new one
  wsClient.connect()

  // Push all current operations to the new connection
  Object.keys(operations).forEach(id => {
    wsClient.sendMessage(
      id,
      MessageTypes.GQL_START,
      operations[id].options
    )
  })
}

function defaultGetAuth (tokenName) {
  if (typeof window !== 'undefined') {
    // get the authentication token from local storage if it exists
    const token = window.localStorage.getItem(tokenName)
    // return the headers to the context so httpLink can read them
    return token ? `Bearer ${token}` : ''
  }
}

It took literally forever to find your comment with this example.

@Venryx
Copy link

Venryx commented Jul 19, 2021

The code example by @pyankoff did not work for me.

Instead, I had to supply the auth token in connectionParams.authorization rather than connectionParams.headers.Authorization:

const wsLink = new WebSocketLink({
  uri: WS_URL,
  options: {
    lazy: true,
    reconnect: true,
    connectionParams: async () => {
      const token = await getToken();
      return {
        // this works for me
        authorization: token ? `Bearer ${token}` : "",

        // this did not work for me
        //headers: {Authorization: token ? `Bearer ${token}` : ""},
      };
    },
  },
});

@Hemistone
Copy link

Hemistone commented Aug 13, 2021

@ARMATAV I've tried to implement code as mentioned, but it gives me type error for some of the wsClient functions. For example, when I try to call wsClient.sendMessage({}), it gives me Property 'sendMessage' is private and only accessible within class 'SubscriptionClient'. Is there any issue with typescript support?

@ARMATAV
Copy link

ARMATAV commented Aug 13, 2021

@ARMATAV I've tried to implement code as mentioned, but it gives me type error for some of the wsClient functions. For example, when I try to call wsClient.sendMessage({}), it gives me Property 'sendMessage' is private and only accessible within class 'SubscriptionClient'. Is there any issue with typescript support?

If I recall, yes - // @ts-ignore that thing and it will work I believe

@victor-enogwe
Copy link

For me I needed to pass the authentication information on every request in a stateless manner.
WebSockets won't let you pass HttpHeaders as stated, and I did not want a stateful authentication via cookies. To accomplish this, we'll make use of the WebSocket protocols argument shown below to send the authentication information.

var aWebSocket = new WebSocket(url [, protocols]);
https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket

According to the MDN docs, "The WebSocket.protocol read-only property returns the name of the sub-protocol the server selected;".

Your server implementation will likely have a check for this to ensure protocol compatibility. Good news is we can pass many protocols.

From the apollographql/subscription-transport-ws source code, the WebSocket client is instantiated within the connect private method

private connect() {
    this.client = new this.wsImpl(this.url, this.wsProtocols, ...this.wsOptionArguments);
    ...
}

https://github.com/apollographql/subscriptions-transport-ws/blob/a8bbabfe375090b44d4a8bd028356ec53d48a23a/src/client.ts#L557

We can pass the protools synchronously by instantiating the SubscriptionClient class like this:

const wsClient = new SubscriptionClient(<WS_LINK>, { reconnect: true, lazy: true }, <WebSocket implementation | undefined>, [, <protocols>]);

However I wanted to be able to asynchronously pass in the authentication information once it changes. The class property wsProtocol in the apollographql/subscription-transport-ws stores the protocol information, however it is private.

private wsProtocols: string | string[];

https://github.com/apollographql/subscriptions-transport-ws/blob/a8bbabfe375090b44d4a8bd028356ec53d48a23a/src/client.ts#L93

To asynchronously send the WebSocket Authentication information, we need to extend the SubsciptionClient as below:

import { ExecutionResult } from 'graphql';
import { BehaviorSubject, skip } from 'rxjs';
import { ClientOptions, Observable, OperationOptions, SubscriptionClient } from 'subscriptions-transport-ws';

export class CustomSubscriptionClient extends SubscriptionClient {
  protocols = new BehaviorSubject<string[]>(['graphql-ws']); // where `graphql-ws` is the default protocol

  constructor(url: string, options?: ClientOptions, webSocketImpl?: WebSocket, webSocketProtocols?: string | string[]) {
    super(url, options, webSocketImpl, webSocketProtocols);

    this.protocols.pipe(skip(1)).subscribe((wsProtocols) => {
      Object.assign(this, { wsProtocols }); // this hack overrides the private variable `wsProtocols` to set our authentication information
      this.close(false);
    });
  }

  public request(request: OperationOptions): Observable<ExecutionResult> {
    return super.request(request);
  }
}

We can then create a WebSocket client and link as follows:

...
const wsClient = new CustomSubscriptionClient(<WS_LINK>, { reconnect: true, lazy: true });
const wsLink = new WebSocketLink(wsClient.use([{ applyMiddleware: wsAuth }]));

Somewhere else in your code, e.g after authentication you can update the authentication information as follows:

wsClient.protocols.next(['graphql-ws',  <AUTH_INFO_STRING>])

On logout user you can do

wsClient.protocols.next(['graphql-ws'])

Note: This fix works by restarting the WebSocket client each time you set a new authentication information, it relies on the close method on the subscription client which restarts the WebSocket client if its isForced argument is false.

The server will then have access to the authentication information in its subprotocols headers on each request.
I personally use the python channels subscription implementation on Django so my server authentication middleware looks like this:

class WSAuthMiddleware(BaseMiddleware):
    async def populate_scope(self, scope):
        token = scope.get("subprotocols")[1]
        user = None

        if token is None:
            raise ValueError("WSAuthMiddleware cannot find authorization in scope. ")

        # # Add it to the scope if it's not there already
        if "user" not in scope:
            scope["user"] = AnonymousUser()

        if token is not None:
            user = await sync_to_async(get_user_by_token, thread_sensitive=True)(
                token, scope
            )
            scope["user"] = user
    ...

Hope this helps someone.

@Rhomennik
Copy link

Rhomennik commented Nov 10, 2022

this work for me =>

connectionParams: async () => {
      const token = await getToken();
      return {
        // this works for me
        authorization: token ? `Bearer ${token}` : "",

        // this did not work for me
        //headers: {Authorization: token ? `Bearer ${token}` : ""},
      };
    },

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests