-
Notifications
You must be signed in to change notification settings - Fork 344
apollo-link-ws: how to change connectionParams (and token) after initial setup? #197
Comments
You can use a function that resolves to an object in the connectionParams. const wsLink = new WebSocketLink({
uri: config.subscriptionURL,
options: {
reconnect: true,
connectionParams: () => ({
authToken: reduxStore.getState().authentication.token,
}),
},
}); EDIT: Oh wait, this isn't called after the first time... Maybe concat with another middleware that puts an authentication object on the response that you can parse with |
Here's what I've got for you, modeled after the way I was doing it with ApolloClient V1 Add a new link middleware to attach the auth token onto the payload const authMiddleware = new ApolloLink((operation, forward) => {
// Add the authorization to the headers for HTTP authentication
operation.setContext({
headers: {
authorization: `Bearer ${authToken()}`,
},
});
// Add onto payload for WebSocket authentication
(operation as Operation & { authToken: string | undefined }).authToken = authToken();
return (forward as any)(operation);
});
const myLink = concat(myLink, wsLink); Then server side you can check it and apply to context using the onOperation hook function configureDecodeTokenSocketMiddleware(authURL: string) {
return async function decodeTokenSocketMiddleware<ConnectionParams extends { authToken: string }>(connectionParams: ConnectionParams, operationParams: object) {
let authPayload;
try {
if (typeof connectionParams.authToken === 'string') {
authPayload = await verifyJWT(authURL, connectionParams.authToken);
} else {
throw new Error('Auth Token not available');
}
} catch(e) {
authPayload = {};
}
return {
...operationParams,
context: {
authentication: authPayload,
},
};
};
}
new SubscriptionServer({
execute,
subscribe,
schema,
onOperation: configureDecodeTokenSocketMiddleware(appConfig.authURL),
}, {
server: appConfig.server,
path: `/${appConfig.subscriptionsEndpoint}`,
}); |
maybe, you can try to resend this message: https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L507
|
Is there a recommanded way to do this? |
subscription this issues |
Since the new I'm using the same process as I did in v1, though it feels kind of hacky to not have it as part of the WebSocketLink API. // create the web socket link
const wsLink = new WebSocketLink({
uri: 'ws://example.com',
options: {
reconnect: true
}
})
// create my middleware using the applyMiddleware method from subscriptions-transport-ws
const subscriptionMiddleware = {
applyMiddleware (options, next) {
options.auth = { ... }
next()
}
}
// add the middleware to the web socket link via the Subscription Transport client
wsLink.subscriptionClient.use([subscriptionMiddleware]) Any values that you add to the options object are available in the |
also having this issue |
I've created a solution(? it's woking in my case) a few weeks ago: A module
In my Authentication service, I'm injecting this module in constructor via
This opens the subscription client on app start and re-inits it completely on login/logout. |
Ideally this would support a promise returning function as well so if we have to async fetch the token we can. |
@clayne11 Am not sure it does. I'm trying to use AsyncStorage from React-Native as the connectionParams. But because of the async i added, it keep sending undefined to the server. UPDATE: |
@jonathanheilmann, your solution is similar to @helios1138 original workaround. However, the I tried doing a
|
Any clean new ideas on this? Love to have a solution here. I create my apollo link on load but once my user logs in id really like to update the token being sent in the context. |
This is the best solution we've been able to come up with: apollographql/apollo-server#1505. The |
This helped to me |
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! here is my code so far (working correctly with one token): 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 ? |
@sulliwane @RyannGalea @jonathanheilmann @helios1138 To dynamically update tokens/connection params you should set it using middleware of the SubscriptionClient. See my gist here: https://gist.github.com/cowlicks/71e766164647f224bf15f086ea34fa52 const subscriptionMiddleware = {
applyMiddleware: function(options, next) {
// Get the current context
const context = options.getContext();
// set it on the `options` which will be passed to the websocket with Apollo
// Server it becomes: `ApolloServer({contetx: ({payload}) => (returns options)
options.authorization = context.authorization;
next()
},
};
const server = new ApolloServer({
context: ({connection, payload, req}) => {
// whatever you return here can be accessed from the middleware of the SubscriptionMiddleware with
// applyMiddleware: (options, next) => options.getContext()
return {authorization: payload.authorization};
},
});
const link = new WebSocketLink({
uri: WS_URL,
webSocketImpl: WebSocket,
options: {
reconnect: true,
}
});
link.subscriptionClient.use([subscriptionMiddleware]); |
The only solution I found is to manually close the client... |
I posted a solution 1.5 years ago: apollographql/apollo-server#1505. AFAIK this still works. |
@clayne11 your solution implies to use a middleware. I think it's great but I work with Angular and Yoga-Graphql and for unknown reason I can't send token through context. |
Is there a way to access the token in a middleware in the server? const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
middlewares: [permissions],
resolvers,
context: ({ connection, ...request }) => {
if(connection) {
wsToken: connection.context.authToken
}
}
return ({...request, prisma });
}
});
const options = {
port: process.env.PORT || 4000,
tracing: true,
subscriptions: {
path: "/",
onConnect: async (connectionParams, webSocket) => {
if (connectionParams.authToken) {
// I can access this through context
return {
authToken: connectionParams.authToken
};
}
}
}
};
server.express.use(async (req, res, next) => {
/*
I want to access authToken here
*/
next();
});
server.start(options, ({port}) => {
console.log(`Server is runnning on http://localhost:${port}`);
}); |
Without closing the socket I get a bunch of apollo network errors (even though it corrects itself we show those errors in a bunch of toast notifications so less than ideal) so I have to close the websocket after updating the token, then Edit: Fixed by calling |
Is there one complete client and server example how to reconnect with new token instead 100 scattered semi-solutions with 5 and 23 likes? |
I install subscriptions like this Is there some other way how to add onOperation? Or I have to manually create SubscriptionServer and do not use |
I ended up dynamically generating the client. If the user is not logged in, the client does not create the /**
* Http link
*/
const httpLink = new HttpLink({
uri: '/graphql'
});
/**
* Perform actions before each http request
*/
const middlewareLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
authorization: localStorage.getItem('token')
}
});
return forward(operation);
});
const cache = new InMemoryCache();
type ApolloSubscriptionClient<TLoggedIn extends boolean> = TLoggedIn extends true ? SubscriptionClient : undefined;
/**
* Function to create our apollo client. After login we want to add subscriptions
* with websocket, so we'll need to recreate the entire client depending on the
* user being logged in or logged out.
*/
export const generateApolloClient = <TLoggedIn extends boolean>(
loggedIn: TLoggedIn
): [ApolloClient<NormalizedCacheObject>, ApolloSubscriptionClient<TLoggedIn>] => {
let link = middlewareLink.concat(httpLink);
// Only apply our subscription client when the user is logged in
let subscriptionClient: SubscriptionClient | undefined;
if (loggedIn) {
subscriptionClient = new SubscriptionClient('ws://localhost:4001/graphql/ws', {
reconnect: true,
connectionParams: () => ({ authorization: localStorage.getItem('token') })
});
const wsLink = new WebSocketLink(subscriptionClient);
link = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
link
);
}
const apolloClient = new ApolloClient({ link, cache });
return [apolloClient, subscriptionClient as ApolloSubscriptionClient<TLoggedIn>];
}; Here is the code in the const GraphqlProvider: React.FC = ({ children }) => {
const dispatch = useAppDispatch();
const loggedIn = useUserLoggedIn();
const [client, setClient] = useState(generateApolloClient(false)[0]);
useEffect(() => {
if (loggedIn) {
const [apolloClient, subscriptionClient] = generateApolloClient(loggedIn);
subscriptionClient.onConnected(() => {
dispatch(setSubscriptionConnected(true));
})
setClient(apolloClient);
}
}, [dispatch, loggedIn]);
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}; This is working well and I see a new token on the server every time a user logs out and back in again, but seems like a bit of extra work. Thoughts? |
Based on @dorsett85 answer It may be a good idea to use react context api to refresh the client for React/NextJS apps. ----------- Context API
--------- Apollo Provider
-------- util function generating new Client with auth
----------login component
----------- on login Important note: Provided Apollo Client code uses SSR |
@kamilkisiela Can we just make the |
const wsLink = new WebSocketLink({
uri: CLIENT_CONFIG.websocketUrl,
options: {
lazy: true,
reconnect: true,
connectionParams: {
get authorization() {
return user.getAuthorization();
},
},
connectionCallback: (error) => {
//@ts-ignore
if (error?.message === "Authentication Failure!") {
//@ts-ignore
//wsLink.subscriptionClient.close(false, false);
}
},
},
}); |
Would any one believe me if I say this works:
ie
TO:
|
The docs describe authentication on websockets as providing the token in
connectionParams
when creating the WsLink initially.But it is unclear how to change the token later and reinitialize connection if the user logs out and logs in again (possibly with a different token)
I did manage to achieve the desired result like this:
but it feels kinda hacky, since
wsLink.subscriptionClient
is marked as private and is not supposed to be accessible from outsideThe text was updated successfully, but these errors were encountered: