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 use setContext or other methods for setting the params #10

Open
G3z opened this issue Nov 30, 2017 · 26 comments
Open

how to use setContext or other methods for setting the params #10

G3z opened this issue Nov 30, 2017 · 26 comments
Labels

Comments

@G3z
Copy link

G3z commented Nov 30, 2017

Is it possible to use import { setContext } from 'apollo-link-context'; to set the auth token ?
with apollo-link-http it's done as so:

const authHttpLink = setContext(() => {
	const token = getToken();
	if (token) {
		return {
			headers: {
				authorization: 'Bearer ' + token,
			},
		};
	}
});

authHttpLink.concat(httpLink)

i tried

const authWsLink = setContext(req => {
	const token = getToken();
	if (token) {
		req.params = {
			guardian_token: token,
		};
	}
});

const wsLink = createAbsintheSocketLink(
	AbsintheSocket.create(new PhoenixSocket(WS_SERVER))
);

authWsLink.concat(wsLink)

but i couldn't set the connection params
since i'm on React Native i have to access AsyncStorage to get my token so i can't simply

const wsLink = createAbsintheSocketLink(
	AbsintheSocket.create(new PhoenixSocket(WS_SERVER,{params: { guardian_token: getToken()}}))
);
@tlvenn
Copy link
Member

tlvenn commented Nov 30, 2017

Hi @G3z, You have to be careful when you compose links with a WS termination as most of them have been designed with an HTTP termination in mind (stateless) which is the case with apollo-link-context.

Given the socket is stateful and params are passed only when you connect, you will have to drop and create a new socket on logout / login actions.

@mgtitimoli
Copy link
Member

mgtitimoli commented Nov 30, 2017

Hi @G3z,

You will need to create a wrapper link on top of socket-link to do this. It will have to be somekind of a memoized factory, where it will have to keep record of the latest credentials it received, and if the ones passed are the same as the stored, it could be able to reuse previously created socket-link, but if not, it will have to close the active one, and create a new one with the credentials given and use this one from there on.

@Yamilquery
Copy link

Yamilquery commented Dec 12, 2017

I`ve been stuck for several days looking for a library that works correctly, I was recommended to use this one, but I can not manage to pass my token to do the authentication.

Could somebody give me an code example of that implementation that mention @mgtitimoli !

Thank you!

@G3z
Copy link
Author

G3z commented Dec 13, 2017

this is my dirty hack if anyone is interested

let token = getToken(); //either undefined or proper_token
const params = {
	get guardian_token() {
		if (!token) {
			token = getToken();
		}
		return token;
	},
};

const wsLink = createAbsintheSocketLink(
	AbsintheSocket.create(
		new PhoenixSocket(WS_SERVER, {
			reconnect: true,
			timeout: 30000,
			params,
		})
	)
);

this way the token is checked upon each connection

@Yamilquery
Copy link

Yamilquery commented Dec 14, 2017

It works great!

Thank you!

@G3z
Copy link
Author

G3z commented Dec 14, 2017

@Yamilquery
Copy link

Yamilquery commented Dec 14, 2017

Ok, awesome.

Do you know why doesn't works if this is code gives the same result?

const params = {
    guardian_token: (!token) ? getToken() : token,
};

However when I use your getter function, it works fine!

@mgtitimoli
Copy link
Member

mgtitimoli commented Dec 14, 2017

Nice hack @G3z, but it won't reconnect to socket if auth changed.

The proper way to handle this would be to introduce an extra level of indirection, so instead of passing an instance of phoenix.Socket to createAbsintheSocketLink, it would have to receive a fn, and this fn would have to be executed with the context, so it would provide a way to check if it has or not changed from prev execution, and if so, then this fn should have to return a new instance or the current one in case it has not.

@mgtitimoli
Copy link
Member

The way I've just commented will introduce a breaking change, that's why I'm still doubting if it is the best way or we should create another factory fn for this cases.

@Yamilquery
Copy link

Yamilquery commented Dec 19, 2017

Something new?

How can I implement this new layer that you comment?

In my case, I use Redux, which is why I think of creating a reducer called client, which is created when you log in or is deleted when you log out.

@ssomnoremac
Copy link

I'm using RelayModern and want to be able to set the params on each login and remove on logout. Can environment be a service (singleton) with a static factory function init(params) and a getter get()?
gist here
Not sure if this approach is necessary though it helps me to think about the current socket instance.

@richeterre
Copy link

Hi @mgtitimoli, first off a big thank you for taking these packages forward!

I ran into the same issue as @G3z and @Yamilquery while upgrading my React Native app to Apollo 2. It turned out to be a major blocker because all my subscriptions require an auth token. Have you already formed an opinion on how this could be solved best? I'm quite a novice with Apollo (and JS for that matter), but if there is any way I can help please let me know!

@smithaitufe
Copy link

Hi @richeterre

This was how I managed to resolve my own
install apollo-client, apollo-cache-inmemory, apollo-link-context and apollo-link-http
then do this

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

const getUserJWT = async () => { 
    const token = await AsyncStorage.getItem(APP_USER_TOKEN)
    return token ? `BEARER ${token}` : null
}

const httpLink = createHttpLink({ uri: httpUri });
const authLink = setContext(async (req, { headers }) => {
  const token = await getUserJWT()
  return {
    headers: {
      ...headers,
      authorization: token
    }
  }
})

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

It is working perfectly for me.

@raarts
Copy link

raarts commented Feb 1, 2018

@smithaitufe Nice solution but will this work for absinthe sockets as well? At the moment it does not work for apollo-link-ws.

BTW: InMemoryCache is default for ApolloClient, so no need to specify that

Edit: disregard that last remark. Doc says it is default, but you still need to explicitly specify it.

@mgtitimoli
Copy link
Member

mgtitimoli commented Feb 1, 2018

Hi everyone,

The easiest way to deal with this, is to create a new link on top of the one provided here, that can keep in its internal state the last auth params received (extracted from the context), so on each request it would be able to check if they remain the same or if they changed, reusing the previous instance of the absinthe-apollo-link (created by itself), or creating a new one correspondingly.

This intermediate link will serve as a "memoized absinthe-apollo-link factory" only, it will not handle the request by itself, but it will pass it to the instance of absinthe-apollo-link it holds, which it will remain the same if the auth (or any other) params needed to create the absinthe-socket (dependency of absinthe-socket-link) didn't change or it will have to create a new one (with the new params) if they did.

I will expose this in the near future, I'm currently working on the next release, it will be ready by the end of February, meanwhile you can do what I wrote above.

Thanks!

@raarts
Copy link

raarts commented Mar 1, 2018

@mgtitimoli :

The easiest way to deal with this, is to create a new link on top of the one provided here,

I understand the gist of what you are saying here, but I looked at the source code for createAbsintheSocketLink, and your definition of easy is definitely not the same as mine ;-)

@raarts
Copy link

raarts commented Apr 14, 2018

@mgtitimoli Any news on when the new release is expected?

@SjaufStefan
Copy link

Any update on this? I imagine there are more people saving their auth tokens in the asyncstorage so some solution for that is pretty pressing.

@richeterre
Copy link

richeterre commented May 14, 2018

I tried following @mgtitimoli's advice of creating a "memoized factory" for AbsintheSocketLink instances, and ended up with this:

// socketLink.ts

import * as AbsintheSocket from '@absinthe/socket'
import { AbsintheSocketLink, createAbsintheSocketLink } from '@absinthe/socket-apollo-link'
import { ApolloLink, NextLink, Operation } from 'apollo-link'
import { Socket as PhoenixSocket } from 'phoenix'

import config from '../constants/config'
import store from '../redux/store'

function getAuthToken() {
  return store.getState().auth.token
}

function createPhoenixSocket(authToken: string | null): PhoenixSocket {
  return new PhoenixSocket(config.subscriptionsUrl, {
    params: {
      auth_token: authToken,
    },
  })
}

function createInnerSocketLink(phoenixSocket: PhoenixSocket): any {
  const absintheSocket = AbsintheSocket.create(phoenixSocket)
  return createAbsintheSocketLink(absintheSocket)
}

class SocketLink extends ApolloLink {
  socket: PhoenixSocket
  link: AbsintheSocketLink

  constructor() {
    super()
    this.socket = createPhoenixSocket(getAuthToken())
    this.link = createInnerSocketLink(this.socket)
    this._watchAuthToken()
  }

  request(operation: Operation, forward?: NextLink | undefined) {
    return this.link.request(operation, forward)
  }

  _watchAuthToken() {
    let token = getAuthToken()

    store.subscribe(() => {
      const newToken = getAuthToken()
      if (newToken !== token) {
        token = newToken
        this.socket.disconnect()
        this.socket = createPhoenixSocket(token)
        this.link = createInnerSocketLink(this.socket)
      }
    })
  }
}

export default new SocketLink()

and in my Apollo client setup I use the above link for all subscriptions:

export default new ApolloClient({
  link: split(operation => hasSubscription(operation.query), socketLink, httpLink),
  cache: new InMemoryCache(),
})

Note that I'm getting my auth token from the Redux store, not AsyncStorage, but the general pattern should be similar. Hope this helps others bridge the gap until there's a more "official" solution ☺️

@krystofbe
Copy link

Thanks for your solution @richeterre!

This is how I did it with async-storage in react native

import * as AbsintheSocket from "@absinthe/socket";
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
import { ApolloLink } from "apollo-link";
import { Socket as PhoenixSocket } from "phoenix";
// import { getAccessToken } from "../../storage/authentication";
import { CHAT_URL } from "../endpoints";
import { AsyncStorage } from "react-native";

const AUTH_TOKEN_KEY = "accessToken";

async function getAccessToken() {
  return await AsyncStorage.getItem(AUTH_TOKEN_KEY);
}

class SocketLink extends ApolloLink {
  constructor() {
    super();
    getAccessToken().then(accessToken => {
      this.link = createAbsintheSocketLink(
        AbsintheSocket.create(
          new PhoenixSocket(CHAT_URL, {
            params: {
              access_token: accessToken,
            },
          }),
        ),
      );
    });
  }

  request(operation, forward) {
    return this.link.request(operation, forward);
  }
}

export default new SocketLink();

@MortadaAK
Copy link

Based on the https://github.com/phoenixframework/phoenix/blob/473a3cb40aae1ba36c70dfc4841ace2d3aab7471/assets/js/phoenix.js#L733
The params accepts a function. Every time it will establish a connection, it will call this function.
My implementation is

import * as AbsintheSocket from '@absinthe/socket'
import { createAbsintheSocketLink } from '@absinthe/socket-apollo-link'
import { Socket as PhoenixSocket } from 'phoenix'

const socket = new PhoenixSocket(WS_URL, {
  params: () => {
    token: window.localStorage.getItem('token')
  },
  heartbeatIntervalMs: 5000
})
export default createAbsintheSocketLink(AbsintheSocket.create(socket))

@richeterre
Copy link

@mgtitimoli Has this been addressed in the recent release (v0.2.0)? I couldn't find release notes or a changelog anywhere…

@sulliwane
Copy link

sulliwane commented Jan 17, 2019

could someone confirm that once the websocket channel has been opened (with Authorization header = token AAA), each subsequent request using the websocket link will always be identified as AAA token.

Or is there a way to send a different Authorization header on each request (other than re-opening another ws channel)?

I'd like to understand what's happening on a low level protocol for ws.

Thank you for you reply!

const wsClient = new SubscriptionClient(
  graphqlEndpoint,
  {
    reconnect: true,
    connectionParams: () => ({
      headers: {
        'Authorization': 'mytokenAAA',
      },
    }),
  },
  ws,
);
const link = new WebSocketLink(wsClient);

makePromise(execute(link, options)); // that's using token AAA
// how to make another query (execute) using token BBB without creating another link ?

@oakromulo
Copy link

@richeterre thank you very much for sharing your solution. I ended up with something similar:

import { create as createAbsintheSocket } from '@absinthe/socket'
import { createAbsintheSocketLink } from '@absinthe/socket-apollo-link'
import { ApolloLink, Operation } from 'apollo-link'
import { Socket as PhoenixSocket } from 'phoenix'

import { $cookies, $env } from '@/plugins'

class TerminatingLink extends ApolloLink {
  link?: ApolloLink
  socket?: PhoenixSocket

  tokenStatus = ''

  constructor() {
    super()
  }

  request(operation: Operation) {
    const prev = this.tokenStatus
    const curr = $cookies.socketToken.get()

    this.tokenStatus = curr

    if (curr && (!this.link || !prev || prev !== curr)) {
      this.link = this._create(curr)
    }

    return this.link ? this.link.request(operation) : null
  }

  _create(token: string): ApolloLink {
    if (this.socket) {
      this.socket.disconnect()
    }

    this.socket = new PhoenixSocket($env.api.wssUri, {
      params: { token }
    })

    return createAbsintheSocketLink(createAbsintheSocket(this.socket), (_) => {
      $cookies.socketToken.remove()
    })
  }
}

export default new TerminatingLink()

@slorber
Copy link

slorber commented Nov 13, 2019

@richeterre solution is nice. My solution is a bit more advanced and try to support workflows such as disconnecting the socket when user logs out, and reconnect if the user logs in again etc... I actually want no socket to stay alive when user is not logged in.

Here's some generic infrastructure code:

export class ApolloUpdatableLink extends ApolloLink {
  link: ApolloLink;

  constructor(link: ApolloLink) {
    super();
    this.link = link;
  }

  updateLink = (link: ApolloLink) => {
    this.link = link;
  };

  request(operation: Operation, forward?: NextLink | undefined) {
    return this.link.request(operation, forward);
  }
}

export class ApolloThrowOnRequestLink extends ApolloLink {
  error: Error;

  constructor(error: Error) {
    super();
    this.error = error;
  }

  request(operation: Operation, forward?: NextLink | undefined): Observable<FetchResult> | null {
    throw this.error;
  }
}

And some app-specific code to achieve the behavior I want:

type DisconnectableLink = { link: ApolloLink; disconnectLink: () => void };

const createUnauthenticatedSubscriptionLink = (): DisconnectableLink => {
  return {
    link: new ApolloThrowOnRequestLink(
      new Error(
        'Apollo subscription link is disabled, because it requires an authenticated user',
      ),
    ),
    disconnectLink: () => {
      // Nothing to do
    },
  };
};

const createAuthenticatedSubscriptionLink = (
  token: string,
): DisconnectableLink => {
  const phoenixSocket = new PhoenixSocket(SubscriptionUrl, {
    params: { token },
  });
  return {
    link: createAbsintheSocketLink(AbsintheSocket.create(phoenixSocket)),
    disconnectLink: () => {
      phoenixSocket.disconnect();
    },
  };
};

// This link will switch between the authenticated/unauthenticated underlying links and handle the disconnexion or previous link before switching
// You have to provide your own "AppSessionUpdatedEvents" (in my case it's a simple event-emitter/bus that I trigger on login/logout)
const subscriptionLink = (() => {
  let currentDisconnectableLink: DisconnectableLink = createUnauthenticatedSubscriptionLink();
  const updatableLink = new ApolloUpdatableLink(currentDisconnectableLink.link);

  AppSessionUpdatedEvents.subscribe(appSession => {
    currentDisconnectableLink.disconnectLink();
    const nextDisconnectableLink = appSession
      ? createAuthenticatedSubscriptionLink(appSession.token)
      : createUnauthenticatedSubscriptionLink();
    currentDisconnectableLink = nextDisconnectableLink;
    updatableLink.updateLink(nextDisconnectableLink.link);
  });
})();

@ademola-raimi
Copy link

ademola-raimi commented Jul 21, 2020

Hi guys, all the examples I have seen so far use params to set the token on the client. Is it possible to pass Authorization header to the absinthe/Phoenix framework? I have tried so many things including this:

const wsClient = new PhoenixSocket(
  AUTH_ABSINTHE_SOCKET_ENDPOINT,
  {
    reconnect: true,
    connectionParams: () => ({
      headers: {
        'authorization': `Bearer ${token}`,
      },
    }),
  },
);
const link = createAbsintheSocketLink(AbsintheSocket.create(wsClient));

I see that it is possible to do this using Websocket link and subscription client here

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

No branches or pull requests