vuejs / apollo

🚀 Apollo/GraphQL integration for VueJS

Home Page:http://apollo.vuejs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to set up Apollo Client 2.0, ApolloLink with subscriptions etc..

kjetilge opened this issue · comments

I spent some time to figure it out, so just in case anyone wants to try:

// Apollo imports
import ApolloClient from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { ApolloLink, concat, split } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory'
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';

//Vue imports
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import router from './router'

//Component imports
import App from './App'
import VModal from 'vue-js-modal'

import { GC_USER_ID, GC_AUTH_TOKEN } from './constants/settings'

import store from './store/index' // Vuex

const httpLink = new HttpLink({ uri: 'https://api.graph.cool/simple/v1/xxxxxxxxx' });

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  operation.setContext({
    headers: {
      authorization: localStorage.getItem(GC_AUTH_TOKEN) || null,
    }
  });
  return forward(operation);
})

// Set up subscription
const wsLink = new WebSocketLink({
  uri: `wss://subscriptions.us-west-2.graph.cool/v1/xxxxxxxx`,
  options: {
    reconnect: true
  }
});

// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
  // split based on operation type
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query);
    return kind === 'OperationDefinition' && operation === 'subscription';
  },
  wsLink,
  httpLink,
);

const apolloClient = new ApolloClient({
  link: concat(authMiddleware, link),
  cache: new InMemoryCache()
});

Vue.use(VueApollo)
Vue.use(VModal, { dialog: true })

Vue.config.productionTip = false

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
  defaultOptions: {
    $loadingKey: 'loading'
  },
  errorHandler (error) {
    console.log('Global error handler')
    console.error(error)
  }
})

const userId = localStorage.getItem(GC_USER_ID)
/* eslint-disable no-new */
window.vm = new Vue({
  el: '#app',
  store,
  apolloProvider,
  router,
  data: {
    userId
  },
  render: h => h(App)
})

Works great, however I needed to do some adjustments to authorization, in case anyone needs it, this worked for me.

instead of:

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  operation.setContext({
    headers: {
      authorization: localStorage.getItem(GC_AUTH_TOKEN) || null,
    }
  });
  return forward(operation);
})

used:

const token = localStorage.getItem(GC_AUTH_TOKEN) || null
const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  operation.setContext({
    headers: {
      authorization: `Bearer ${token}`
    }
  })
  return forward(operation)
})

Thanks @kjetilge !!!

Why use localstorage instead of cookies? It won't be shared across subdomains and http/https

commented

Why not use websockets for everything? Why split?

I found an issue. After set the the token, we have to refresh the web page to get authorization to work.

Solved: #183

Just my 2c for future visitors (and those who asked the questions):

  • I also prefer HttpOnly cookies. In the browser (not in Node), you can use credentials: 'include' to pass cookies to your API without having them accessible via JS. This is the safest approach I've found so far for protecting auth tokens. Granted, you'll need to set up your API to look for the auth token in both a cookie and/or auth header. I have my back-end look for the cookie auth token first, and then write that to the request's Authorization/Bearer header. Then, business as usual.

  • I've been using WS on the server, and HTTP in the browser. In my particular case, the back-end isn't accepting cookies (by choice, and due to web socket's lack of CORS; which results in a CSWSH vulnerability), so the only way to send the auth token in the upgrade handshake would be to make it accessible to JS in the browser. You can do this by setting the store in nuxtServerInit and accessing it via window.__NUXT__.state.authToken. The problem is that now your auth token is accessible to all of JS, and you may-or-may-not feel comfortable doing that.

Hi @bjunc

Can you make an example repo about this?

Yeah, I think I can do that. I was actually planning on doing a Medium article about this setup (along with the back-end; which is in Elixir).

Also worth noting, is that you can use @nuxtjs/proxy to make your browser-based queries/mutations. You set your GraphQL URI to /graphql, and then proxy this to the actual API endpoint. In doing this, the HttpOnly cookie is shared between the API and the Nuxt app. That allows you to use the cookie for page auth; as well as for API requests. It also negates the CORS pre-flight and other origin related issues. So at no point is your auth token accessible via JS in the browser.

I'm currently building a full demo app in the tests/demo folder that will also be used for the e2e tests, and it already has a user system with authentication.

The solution above to set authentication headers via middleware uses apollo-link and apollo-client. Is it possible to have middlewares using apollo-boost to set custom headers dynamically for each request?

Are there any examples of using the default out-of-the-box boilerplate with bearer tokens?

As in, when using vue add apollo in vue-cli where do I set the token I get from my auth provider? (auth0)

@hades200082 in your vue-apollo.js scaffolded out by the cli tool, there's a getAuth() method in the options, you can ues that to return the token.

i.e.

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

Works great, however I needed to do some adjustments to authorization, in case anyone needs it, this worked for me.

instead of:

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  operation.setContext({
    headers: {
      authorization: localStorage.getItem(GC_AUTH_TOKEN) || null,
    }
  });
  return forward(operation);
})

used:

const token = localStorage.getItem(GC_AUTH_TOKEN) || null
const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  operation.setContext({
    headers: {
      authorization: `Bearer ${token}`
    }
  })
  return forward(operation)
})

Thanks @kjetilge !!!

How to do this for the connection_terminated? I want to send a payload together

FF to 2019: this is the new approach

import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';

const httpLink = createHttpLink({
  uri: '/graphql',
});

const authLink = setContext((_, { headers }) => {
  // get the authentication token from local storage if it exists
  const token = localStorage.getItem('token');
  // return the headers to the context so httpLink can read them
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  }
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

basically the change is that use of setContext, which is to my opinion cleaner.

This is my entire apollo boot file when using subscriptions:

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';

// You should use an absolute URL here
const options = {
  httpUri: process.env.GRAPHQL_HTTP_ENDPOINT || 'http://localhost:7001/graphql',
  wsUri:   process.env.GRAPHQL_WS_ENDPOINT  || 'ws://localhost:7001/graphql',
};

let link = new HttpLink({
  uri: options.httpUri,
});

// Create the subscription websocket link if available
if (options.wsUri) {
  const wsLink = new WebSocketLink({
    uri:     options.wsUri,
    options: {
      reconnect: true,
    },
  });

  // using the ability to split links, you can send data to each link
  // depending on what kind of operation is being sent
  link = split(
      // split based on operation type
      ({ query }) => {
        const definition = getMainDefinition(query);
        return definition.kind === 'OperationDefinition' &&
            definition.operation === 'subscription';
      },
      wsLink,
      link,
  );
}


const authLink = setContext((_, { headers }) => {
  // get the authentication token from local storage if it exists
  const token = localStorage.getItem('authorization_token');
  // return the headers to the context so httpLink can read them
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  };
});


export const cache = new InMemoryCache();

// Create the apollo client instance
export default new ApolloClient({
  link: authLink.concat(link),
  cache,
  connectToDevTools: true,
});

@Akryum @emahuni The middleware has no affect on webSocketLink.
the headers seems to only be set via connectionParams on initialization.
Is there a way to change the headers dynamically? in case the token has been refreshed

You have to restart the websocket connection.

@Akryum how do i do that without restarting apollo instance?

  • I've been using WS on the server, and HTTP in the browser. In my particular case, the back-end isn't accepting cookies (by choice, and due to web socket's lack of CORS; which results in a CSWSH vulnerability), so the only way to send the auth token in the upgrade handshake would be to make it accessible to JS in the browser. You can do this by setting the store in nuxtServerInit and accessing it via window.__NUXT__.state.authToken. The problem is that now your auth token is accessible to all of JS, and you may-or-may-not feel comfortable doing that.

Thank you; your post nicely summarizes the issue, and one way to resolve it.

In my case, I did not want to let the frontend code ever have access to the auth-token.

So, I use the following approach instead:

  1. When frontend loads, it sends an http request to the server, calling getConnectionID; because it's an http-request, the http-only auth-token cookie gets included.
  2. The server generates a random UUID for the current connection (ie. "connection id"), associates it with the user-data it read/verified from the http-only cookie, and sends the connection-id to the frontend.
  3. The frontend sends its connection-id back to the server, calling passConnectionID, except this time over the persistent websocket connection.
  4. The server checks for a matching connection-id; if found, checks if ip-address of getConnectionID caller matches current caller.
  5. If the ip-addresses match, the user-id associated with the connection-id is now associated with the websocket connection as well; server then marks connection-id as "used up", preventing additional "redemption attempts".
  6. Now for all future GraphQL requests over the websocket connection, the server knows what user-data is associated with it.

Is the approach above safe/sound?

If not (or only partially), another idea for making it safer:

  • Only accept usage/association-with-websocket-connection/"redemption" of a connection-id within a few seconds of its generation.

Anyway, assuming it's safe/sound, I prefer it over making the auth-token accessible to the frontend js, because of this benefit:

  • The server only allows usage/"redemption" of the connection-id one time, and only from the ip-address that supplied the http-only cookie. This means that if there is malicious code in the frontend that is harvesting the connection-ids, it should not be an issue (beyond the harvesting of data from your frontend code, of course!) because the connection-id is unable to be "redeemed" for on-server-authentication outside of that browser instance.

EDIT: Looks like the approach above is already an established pattern! More info here: https://stackoverflow.com/a/4361358/2441655