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

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

Closed
kjetilge opened this issue Nov 6, 2017 · 19 comments
Closed

Comments

@kjetilge
Copy link

kjetilge commented Nov 6, 2017

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)
})
@kjetilge kjetilge changed the title How to use Apollo 2.0, ApolloLink etc.. How to set up Apollo 2.0, ApolloLink with subscriptions etc.. Nov 6, 2017
@kjetilge kjetilge changed the title How to set up Apollo 2.0, ApolloLink with subscriptions etc.. How to set up Apollo Client 2.0, ApolloLink with subscriptions etc.. Nov 6, 2017
@soyximo
Copy link

soyximo commented Nov 8, 2017

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 !!!

@gijo-varghese
Copy link

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

@rnenjoy
Copy link

rnenjoy commented Dec 26, 2017

Why not use websockets for everything? Why split?

@kieusonlam
Copy link

kieusonlam commented Jan 8, 2018

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

Solved: #183

@bjunc
Copy link

bjunc commented Feb 12, 2018

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.

@kieusonlam
Copy link

Hi @bjunc

Can you make an example repo about this?

@bjunc
Copy link

bjunc commented Feb 12, 2018

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.

@Akryum
Copy link
Member

Akryum commented May 26, 2018

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.

@Akryum Akryum closed this as completed May 26, 2018
@ptrk8
Copy link

ptrk8 commented Oct 2, 2018

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?

@hades200082
Copy link

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)

@oller
Copy link

oller commented Apr 11, 2019

@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 ''
    }
  }

@Valdenirmezadri
Copy link

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

@emahuni
Copy link
Contributor

emahuni commented Aug 20, 2019

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.

@emahuni
Copy link
Contributor

emahuni commented Aug 20, 2019

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,
});

@LermanR
Copy link

LermanR commented Jan 9, 2020

@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

@Akryum
Copy link
Member

Akryum commented Jan 9, 2020

You have to restart the websocket connection.

@LermanR
Copy link

LermanR commented Jan 9, 2020

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

@Akryum
Copy link
Member

Akryum commented Jan 9, 2020

Here is an example: https://github.com/Akryum/vue-cli-plugin-apollo/blob/a52696165732381787dafe6b8e694d4b30af4826/graphql-client/src/index.js#L187-L205

@Venryx
Copy link

Venryx commented Jul 18, 2021

  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests