Vue-apollo: Add Authorization Header to Websocket for Vue Cli Plugin setup?

Created on 25 Feb 2019  路  24Comments  路  Source: vuejs/vue-apollo

Hi,
I am able to add Authorization to the Http call but not able to add it to Websocket. Attaching my setup below:

import Vue from 'vue'
import VueApollo from 'vue-apollo'
import {
  createApolloClient,
  restartWebsockets
} from 'vue-cli-plugin-apollo/graphql-client'

// Install the vue plugin
Vue.use(VueApollo)

// Name of the localStorage item
const AUTH_TOKEN = 'apollo-token'
// return the headers to the context so httpLink can read them

// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP,

// Files URL root
export const filesRoot =
  process.env.VUE_APP_FILES_ROOT ||
  httpEndpoint.substr(0, httpEndpoint.indexOf("/graphql"));

// Config
const defaultOptions = {
  // You can use `https` for secure connection (recommended in production)
  httpEndpoint,
  // You can use `wss` for secure connection (recommended in production)
  // Use `null` to disable subscriptions
  wsEndpoint: process.env.VUE_APP_GRAPHQL_WS,
  // LocalStorage token
  tokenName: AUTH_TOKEN,
  // Enable Automatic Query persisting with Apollo Engine
  persisting: false,
  // Use websockets for everything (no HTTP)
  // You need to pass a `wsEndpoint` for this to work
  websocketsOnly: false,
  // Is being rendered on the server?
  ssr: false,

  // Override default apollo link
  // note: don't override httpLink here, specify httpLink options in the
  // httpLinkOptions property of defaultOptions.
  // link: myLink

  // Override default cache
  // cache: myCache

  // Override the way the Authorization header is set
  getAuth: tokenName => {
    // get the authentication token from local storage if it exists
    const token = 'Bearer ' + localStorage.getItem("id_token");
    // return the headers to the context so httpLink can read them
    return token || ''
  },

  // Additional ApolloClient options
  // apollo: { ... }

  // Client local data (see apollo-link-state)
  // clientState: { resolvers: { ... }, defaults: { ... } }
}

// Call this in the Vue app file
export function createProvider(options = {}) {
  // Create apollo client
  const {
    apolloClient,
    wsClient
  } = createApolloClient({
    ...defaultOptions,
    ...options,
  })
  apolloClient.wsClient = wsClient

  // Create vue apollo provider
  const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
    defaultOptions: {
      $query: {
        // fetchPolicy: 'cache-and-network',
      },
    },
    errorHandler(error) {
      // eslint-disable-next-line no-console
      console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
    },
  })

  return apolloProvider
}

// Manually call this when user log in
export async function onLogin(apolloClient, token) {
  if (typeof localStorage !== 'undefined' && token) {
    localStorage.setItem(AUTH_TOKEN, token)
  }
  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
  try {
    await apolloClient.resetStore()
  } catch (e) {
    // eslint-disable-next-line no-console
    console.log('%cError on cache reset (login)', 'color: orange;', e.message)
  }
}

// Manually call this when user log out
export async function onLogout(apolloClient) {
  if (typeof localStorage !== 'undefined') {
    localStorage.removeItem(AUTH_TOKEN)
  }
  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
  try {
    await apolloClient.resetStore()
  } catch (e) {
    // eslint-disable-next-line no-console
    console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
  }
}

Can you help me with this?

Thanks.

Cheers,
Ashish

Most helpful comment

A workaround is to manually override the connection params.

// Call this in the Vue app file
export function createProvider (options = {}) {
  // Create apollo client
  const { apolloClient, wsClient } = createApolloClient({
    ...defaultOptions,
    ...options
  })

  // Override connection params
  wsClient.connectionParams = () => {
    return {
      headers: {
        Authorization: localStorage.getItem(AUTH_TOKEN) ? `Bearer ${localStorage.getItem(AUTH_TOKEN)}` : ''
      }
    }
  }

  // Set apollo client's websocket client
  apolloClient.wsClient = wsClient

  // Create vue apollo provider
  const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
    defaultOptions: {
      $query: {
        // fetchPolicy: 'cache-and-network',
      }
    },
    errorHandler (error) {
      // eslint-disable-next-line no-console
      console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
    }
  })

  return apolloProvider
}

All 24 comments

I see the same behavior. Stepping through the source code this looks like the probable cause:

https://github.com/Akryum/vue-cli-plugin-apollo/blob/b99182283eeb8ea49336bb1617a309269d32dc8b/graphql-client/src/index.js#L44

vue-apollo never attempts to use the getAuth set by the developer, it always falls back to the default, and does not set authorization headers as requested.

@Akryum does that seem like an accurate analysis?

@agosto-levitomes I only use defaultGetAuth if you don't pass any value for getAuth, that's how default values work in JavaScript. So your analysis is incorrect.

Not Chris, but this is trivial to reproduce.
I will leave you to it

Example that sets getAuth to something with observable side effects:

const defaultOptions = {
    wsEndpoint: < validAddress > ,
    ...
    getAuth: () => {
        console.log(`Making auth token: Bearer ${token}`);
        return `Bearer ${token}`;
    }
    ...
}

Observe getAuth is never called/side effects don't happen:

Observe lack of custom auth headers:
image

Not sure how I got pulled into this conversation. Probably an autocomplete phenomenon of some sort.

@Akryum For now, I am overiding the WSClient creation by extracting it out of your library & setting it up as a duplicate in my project, as I need to pass Authorization headers to wsClient & httpLink in different way.

I may be missing something here, but there must be a cleaner way to do it?

wsClient = new _subscriptionsTransportWs.SubscriptionClient(wsEndpoint, {
                reconnect: true,
                connectionParams: {
                    "headers": {
                        authorization: getAuth(tokenName)
                    }
                }
            }); // Create the subscription websocket link

            var wsLink = new _apolloLinkWs.WebSocketLink(wsClient);
authLink = (0, _apolloLinkContext.setContext)(function (_, _ref2) {
            var headers = _ref2.headers;
            var authorization = getAuth(tokenName);
            var authorizationHeader = authorization ? {
                authorization: authorization
            } : {};
            return {
                headers: _objectSpread({}, headers, authorizationHeader)
            };
        }); // Concat all the http link parts

@agosto-chrisbartling Sorry! The autocomplete did something strange :confused:

@agosto-levitomes See a working example: https://github.com/Akryum/vue-apollo/blob/master/tests/demo/src/vue-apollo.js

@agosto-levitomes See a working example: https://github.com/Akryum/vue-apollo/blob/master/tests/demo/src/vue-apollo.js

Thanks for sharing, that is the example I started working from. I then started vue-apollo via vue add apollo, and accepted example code in case I had made a mistake. That working example does not use websockets.
I'm sorry to say it is not a working example in regards to getAuth for websockets/getting the correct headers on a websocket request.

getAuth from the users defaultOptions Is _not_ called. Simply add a comment to the method in your demo (and a ws endpoint), and run it it to observe this. The default getAuth method from https://github.com/Akryum/vue-cli-plugin-apollo/blob/b99182283eeb8ea49336bb1617a309269d32dc8b/graphql-client/src/index.js#L186 is always called. Hence why @ashishwadekar went through all that effort to abstract add auth headers.

It _does_ work for http/s requests.

@ltomes @Akryum

What I have observed is that token works but only to set headers on httpLink & not on the wsClient. wsClient expects connectionParams & block pertaining to it which is different from httpLink. Ideally once the token is found there could be the following scenarios:

  1. Add authorization headers to httpLink the default way. This is happening now.
  2. Add authorization headers to wsClient with connectionParams. Not happening now.
  3. Options to consider whether authorization headers be set in same way or different way for httpLink & wsClient. Cannot see this in current implementation.

I was unable to set different headers on httpLink & wsLink using getAuth.

@Akryum Share your thoughts on this. Cheers.

@Akryum is this considered to be implemented? Can I help in some way?

@Akryum Any updates on this one?

So should https://github.com/Akryum/vue-cli-plugin-apollo/blob/b99182283eeb8ea49336bb1617a309269d32dc8b/graphql-client/src/index.js#L106 be:

return authorization ? { headers: { authorization  } } : {};

As per this example?

So is there currently any way to add authorization headers to websocket calls?

I think I just hit this issue. With the default setup, setting my token works fine for HTTP calls, but websocket calls complain that no token is set. Using Hasura for the backend. If I'm doing things correctly for one, I'd expect them to work for the other.

@ndarilek I've not tackled this one yet. I'm currently handing up what would otherwise be in this header as a first-class argument in any WebSocket subscriptions that would require authentication.

@ashishwadekar seemed to have posted a monkey-patch above that appears to do the job.

I'd be interested in seeing a full example of how to set up a client using the above monkeypatch, or at the very least, how to configure my own client and make it available to vue-apollo. I'll keep hacking on things here, and if I get something more complete working then I'll post it here.

But this is definitely non-functional. Unfortunately I don't have control over the GraphQL server (Hasura) so, to the best of my knowledge, it's authorization headers or nothing.

A workaround is to manually override the connection params.

// Call this in the Vue app file
export function createProvider (options = {}) {
  // Create apollo client
  const { apolloClient, wsClient } = createApolloClient({
    ...defaultOptions,
    ...options
  })

  // Override connection params
  wsClient.connectionParams = () => {
    return {
      headers: {
        Authorization: localStorage.getItem(AUTH_TOKEN) ? `Bearer ${localStorage.getItem(AUTH_TOKEN)}` : ''
      }
    }
  }

  // Set apollo client's websocket client
  apolloClient.wsClient = wsClient

  // Create vue apollo provider
  const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
    defaultOptions: {
      $query: {
        // fetchPolicy: 'cache-and-network',
      }
    },
    errorHandler (error) {
      // eslint-disable-next-line no-console
      console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
    }
  })

  return apolloProvider
}

@leemarlow That worked perfectly for me. Solid find. Thanks!

@leemarlow thanks, that looks solid, any idea how to implement this in Nuxt? I tried to create a plugin and overwrite the wsClient without luck:

export default (context) => {
  const client = context.app.apolloProvider.defaultClient
  const token = localStorage.getItem('token')

  client.wsClient = {
    ...client.wsClient,
    connectionParams: () => {
      return {
        headers: {
          Authorization: token ? `Bearer ${token}` : ''
        }
      }
    }
  }
}

Edit: got it working by mutating the wsClient

export default (context) => {
  const client = context.app.apolloProvider.defaultClient
  const token = localStorage.getItem('token')

  client.wsClient.connectionParams = () => {
      return {
        headers: {
          Authorization: token ? `Bearer ${token}` : ''
        }
      }
    }
}

@leemarlow Thanks, this looks lots cleaner than my initial workaround. I'm trying to implement your fix now, because my original didn't account for onLogin restarting the websocket connection when authentication parameters change. I'm hitting an issue, though:

  const { apolloClient, wsClient } = createApolloClient({
    ...defaultOptions,
    ...options
  })

  console.log("Apollo client", apolloClient)
  console.log("WS client", wsClient)

In the above code, apolloClient is defined but wsClient is undefined, so this fails:

  // Override connection params
  wsClient.connectionParams = () => {
...

Any suggestions for a quick fix? I guess I can chase this through the labyrinth that is Apollo and the Vue plugin, but I'm on a bit of a time crunch and am hoping there may have been a cut-and-paste error somewhere.

Thanks!

Oh, never mind, I see I wasn't passing in the correct configuration parameters because my previous solution hacked around those. Apologies for the noise.

I'm coming to this as a neophyte and I wonder if someone could review and encapsulate the results of this discussion. What edits need to be made to vue-apollo.js, and if necessary to vue-cli-plugin-apollo/graphql-client/src/index.js, to make vue-apollo work with hasura? I am trying to get hasura's sample application vuejs-auth0-graphql to work and I found my way here in the process of struggling with this error:

GraphQL error: Missing Authorization header in JWT authentication mode

Tracing the code I found that no token was retrieved from localStorage.

I had had to add import { InMemoryCache } from 'apollo-cache-inmemory'; and cache: new InMemoryCache(), to vue-apollo.js in that sample. It wasn't there initially. Also, for good measure, I was able to verify that hasura is properly configured and returning a JWT following directions at https://docs.hasura.io/1.0/graphql/manual/guides/integrations/auth0-jwt.html.

No time for any extensive help, but here is my src/vue-apollo.ts which
connects to Hasura via websockets.

You can drop the prevApolloToken check--we have to reauthenticate for
Auth0 account linking. You can probably also ignore the unauthClient.
I have a situation where the client should never authenticate so Hasura
won't assign it a non-anonymous role. But this is what I came up with
after reading this thread and hacking through this mess until I got
something that worked. :) Improvements welcome..

import Vue from "vue"
import VueApollo from "vue-apollo"
import { createApolloClient, restartWebsockets } from 
"vue-cli-plugin-apollo/graphql-client"

// Install the vue plugin
Vue.use(VueApollo)

// Name of the localStorage item
const AUTH_TOKEN = "apollo-token"

// Http endpoint
// const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 
"http://localhost:8080/graphql"

const wsProtocol = (window.location.protocol === "https:") ? "wss" : "ws"

// Config
const defaultOptions = {
 聽 // You can use `https` for secure connection (recommended in production)
 聽 // httpEndpoint,
 聽 // You can use `wss` for secure connection (recommended in production)
 聽 // Use `null` to disable subscriptions
 聽 wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 
`${wsProtocol}://${window.location.host}/graphql`,
 聽 // LocalStorage token
 聽 tokenName: AUTH_TOKEN,
 聽 // Enable Automatic Query persisting with Apollo Engine
 聽 persisting: false,
 聽 // Use websockets for everything (no HTTP)
 聽 // You need to pass a `wsEndpoint` for this to work
 聽 websocketsOnly: true,
 聽 // Is being rendered on the server?
 聽 ssr: false,

 聽 // Override default apollo link
 聽 // note: don't override httpLink here, specify httpLink options in the
 聽 // httpLinkOptions property of defaultOptions.
 聽 // link: myLink

 聽 // Override default cache
 聽 // cache: myCache

 聽 // Override the way the Authorization header is set
 聽 // getAuth: (tokenName) => ...

 聽 // Additional ApolloClient options
 聽 // apollo: { ... }

 聽 // Client local data (see apollo-link-state)
 聽 // clientState: { resolvers: { ... }, defaults: { ... } }
}

// Call this in the Vue app file
export function createProvider (options = {}) {
 聽 // Create apollo client
 聽 const { apolloClient, wsClient } = createApolloClient({
 聽聽聽 ...defaultOptions,
 聽聽聽 ...options
 聽 })

 聽 // Override connection params
 聽 wsClient.connectionParams = () => {
 聽聽聽 const token = localStorage.getItem("prevApolloToken") || 
localStorage.getItem(AUTH_TOKEN)
 聽聽聽 if(token)
 聽聽聽聽聽 return { headers: {
 聽聽聽聽聽聽聽 Authorization: `Bearer ${token}`,
 聽聽聽聽聽 } }
 聽聽聽 else
 聽聽聽聽聽 return {headers: {}}
 聽 }

 聽 // Set apollo client's websocket client
 聽 apolloClient.wsClient = wsClient

 聽 // Create an Apollo client that will never authenticate.
 聽 const unauth = createApolloClient({
 聽聽聽 ...defaultOptions,
 聽聽聽 ...options
 聽 })

 聽 // Create vue apollo provider
 聽 const apolloProvider = new VueApollo({
 聽聽聽 clients: {
 聽聽聽聽聽 default: apolloClient,
 聽聽聽聽聽 unauth: unauth.apolloClient,
 聽聽聽 },
 聽聽聽 defaultClient: apolloClient,
 聽聽聽 defaultOptions: {
 聽聽聽聽聽 $query: {
 聽聽聽聽聽聽聽 // fetchPolicy: 'cache-and-network',
 聽聽聽聽聽 }
 聽聽聽 },
 聽聽聽 errorHandler (error) {
 聽聽聽聽聽 let message = ""
 聽聽聽聽聽 if(error.originalError)
 聽聽聽聽聽聽聽 message = error.originalError
 聽聽聽聽聽 else
 聽聽聽聽聽聽聽 message = error.message
 聽聽聽聽聽 // eslint-disable-next-line no-console
 聽聽聽聽聽 console.log("%cError", "background: red; color: white; padding: 
2px 4px; border-radius: 3px; font-weight: bold;", message)
 聽聽聽 }
 聽 })

 聽 return apolloProvider
}

// Manually call this when user logs in
export async function onLogin (apolloClient: any, token: string) {
 聽 if (typeof localStorage !== "undefined" && token) {
 聽聽聽 localStorage.setItem(AUTH_TOKEN, token)
 聽 }
 聽 if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
 聽 try {
 聽聽聽 await apolloClient.resetStore()
 聽 } catch (e) {
 聽聽聽 // eslint-disable-next-line no-console
 聽聽聽 console.log("%cError on cache reset (login)", "color: orange;", 
e.message)
 聽 }
}

// Manually call this when user log out
export async function onLogout (apolloClient: any) {
 聽 if (typeof localStorage !== "undefined") {
 聽聽聽 localStorage.removeItem(AUTH_TOKEN)
 聽 }
 聽 if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
 聽 try {
 聽聽聽 await apolloClient.resetStore()
 聽 } catch (e) {
 聽聽聽 // eslint-disable-next-line no-console
 聽聽聽 console.log("%cError on cache reset (logout)", "color: orange;", 
e.message)
 聽 }
}

Thank you @ndarilek. The problem was only my cobweb-ridden brain. I had not yet logged into Auth0 and I got freaked because I didn't expect the console to show errors at that point in the UX. But this is a sample app whose purpose is to demonstrate the core functionality of the stack and in hindsight I'm wiser. Anyway, I logged in, the token returned, got passed to hasura, and the data got rendered. All good now.

Doing some spring cleaning since it's not related to the repository.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

anymost picture anymost  路  3Comments

dsbert picture dsbert  路  4Comments

Akryum picture Akryum  路  3Comments

jakub300 picture jakub300  路  4Comments

sadhakbj picture sadhakbj  路  3Comments