Closed hudymi closed 6 years ago
@michal-hudy Does this not work for you?
const wsLink = new WebSocketLink(
new SubscriptionClient(WS_URL, {
reconnect: true,
timeout: 30000,
connectionParams: {
headers: {
Authorization: "Bearer xxxxx"
}
}
})
);
It won't work as WebSocket API in browsers doesn't support setting custom headers, apart from the value of Sec-Websocket-Protocol
header.
https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api/41521871
Please see @pkosiec comment.
Struggled with adding async function for connection params, was getting start received before the connection is initialised
error. Fixed it by adding lazy: true
to connection options:
const wsLink = new WebSocketLink({
uri: WS_URL,
options: {
lazy: true,
reconnect: true,
connectionParams: async () => {
const token = await getToken();
return {
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
}
},
},
})
Just in case someone having the same issue.
@pyankoff how can I read this header on server side?
@georgyfarniev I think it depends on your server side implementation. I was using Hasura and didn't have to handle it.
@pyankoff I'm using Hasura as well. how did you handle token change (refresh after expiration) with WebSocketLink?
Hey, @pyankoff can you please share how you were able to re-authenticate the user once the token has expired.
I am able to get new accessToken
using refreshToken
but I am failing to pass the new accessToken to client.
I have read through few examples and all direct towards using operation.setContext
, which I was unable to implement while using WebSocketLink
.
Thanks.
@dreamer01 If i understand currently your question is like mine. If so then on WebSocketLink you can only pass tokens on connect. if your token expires and you get a new one, you need to reconnect with the new tokens. here is an example. And if that wasn't your question, then maybe it will help someone else.. :)
@dreamer01 If i understand currently your question is like mine. If so then on WebSocketLink you can only pass tokens on connect. if your token expires and you get a new one, you need to reconnect with the new tokens. here is an example. And if that wasn't your question, then maybe it will help someone else.. :)
Hey, @lroy83 this what exactly I meant, thank you for sharing the code.
@lroy83 Oh man, I spent a long time searching for an example like what you posted. Thank you for sharing!
@dreamer01 If i understand currently your question is like mine. If so then on WebSocketLink you can only pass tokens on connect. if your token expires and you get a new one, you need to reconnect with the new tokens. here is an example. And if that wasn't your question, then maybe it will help someone else.. :)
Finally an actual code example for how to refresh tokens with websocket properly! Pasting it here just because the past 2 examples I've been linked 404'd.
import { ApolloClient } from 'apollo-client'
import { split, from } from 'apollo-link'
import { createUploadLink } from 'apollo-upload-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import MessageTypes from 'subscriptions-transport-ws/dist/message-types'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities'
import { createPersistedQueryLink } from 'apollo-link-persisted-queries'
import { setContext } from 'apollo-link-context'
import { withClientState } from 'apollo-link-state'
// Create the apollo client
export function createApolloClient ({
// Client ID if using multiple Clients
clientId = 'defaultClient',
// URL to the HTTP API
httpEndpoint,
// Url to the Websocket API
wsEndpoint = null,
// Token used in localstorage
tokenName = 'apollo-token',
// Enable this if you use Query persisting with Apollo Engine
persisting = false,
// Is currently Server-Side Rendering or not
ssr = false,
// Only use Websocket for all requests (including queries and mutations)
websocketsOnly = false,
// Custom starting link.
// If you want to replace the default HttpLink, set `defaultHttpLink` to false
link = null,
// If true, add the default HttpLink.
// Disable it if you want to replace it with a terminating link using `link` option.
defaultHttpLink = true,
// Options for the default HttpLink
httpLinkOptions = {},
// Custom Apollo cache implementation (default is apollo-cache-inmemory)
cache = null,
// Options for the default cache
inMemoryCacheOptions = {},
// Additional Apollo client options
apollo = {},
// apollo-link-state options
clientState = null,
// Function returning Authorization header token
getAuth = defaultGetAuth,
// Local Schema
typeDefs = undefined,
// Local Resolvers
resolvers = undefined,
// Hook called when you should write local state in the cache
onCacheInit = undefined,
}) {
let wsClient, authLink, stateLink
const disableHttp = websocketsOnly && !ssr && wsEndpoint
// Apollo cache
if (!cache) {
cache = new InMemoryCache(inMemoryCacheOptions)
}
if (!disableHttp) {
const httpLink = createUploadLink({
uri: httpEndpoint,
...httpLinkOptions,
})
if (!link) {
link = httpLink
} else if (defaultHttpLink) {
link = from([link, httpLink])
}
// HTTP Auth header injection
authLink = setContext((_, { headers }) => {
const authorization = getAuth(tokenName)
const authorizationHeader = authorization ? { authorization } : {}
return {
headers: {
...headers,
...authorizationHeader,
},
}
})
// Concat all the http link parts
link = authLink.concat(link)
}
// On the server, we don't want WebSockets and Upload links
if (!ssr) {
// If on the client, recover the injected state
if (typeof window !== 'undefined') {
// eslint-disable-next-line no-underscore-dangle
const state = window.__APOLLO_STATE__
if (state && state[clientId]) {
// Restore state
cache.restore(state[clientId])
}
}
if (!disableHttp) {
let persistingOpts = {}
if (typeof persisting === 'object' && persisting != null) {
persistingOpts = persisting
persisting = true
}
if (persisting === true) {
link = createPersistedQueryLink(persistingOpts).concat(link)
}
}
// Web socket
if (wsEndpoint) {
wsClient = new SubscriptionClient(wsEndpoint, {
reconnect: true,
connectionParams: () => {
const authorization = getAuth(tokenName)
return authorization ? { authorization, headers: { authorization } } : {}
},
})
// Create the subscription websocket link
const wsLink = new WebSocketLink(wsClient)
if (disableHttp) {
link = wsLink
} else {
link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' &&
operation === 'subscription'
},
wsLink,
link
)
}
}
}
if (clientState) {
console.warn(`clientState is deprecated, see https://vue-cli-plugin-apollo.netlify.com/guide/client-state.html`)
stateLink = withClientState({
cache,
...clientState,
})
link = from([stateLink, link])
}
const apolloClient = new ApolloClient({
link,
cache,
// Additional options
...(ssr ? {
// Set this on the server to optimize queries when SSR
ssrMode: true,
} : {
// This will temporary disable query force-fetching
ssrForceFetchDelay: 100,
// Apollo devtools
connectToDevTools: process.env.NODE_ENV !== 'production',
}),
typeDefs,
resolvers,
...apollo,
})
// Re-write the client state defaults on cache reset
if (stateLink) {
apolloClient.onResetStore(stateLink.writeDefaults)
}
if (onCacheInit) {
onCacheInit(cache)
apolloClient.onResetStore(() => onCacheInit(cache))
}
return {
apolloClient,
wsClient,
stateLink,
}
}
export function restartWebsockets (wsClient) {
// Copy current operations
const operations = Object.assign({}, wsClient.operations)
// Close connection
wsClient.close(true)
// Open a new one
wsClient.connect()
// Push all current operations to the new connection
Object.keys(operations).forEach(id => {
wsClient.sendMessage(
id,
MessageTypes.GQL_START,
operations[id].options
)
})
}
function defaultGetAuth (tokenName) {
if (typeof window !== 'undefined') {
// get the authentication token from local storage if it exists
const token = window.localStorage.getItem(tokenName)
// return the headers to the context so httpLink can read them
return token ? `Bearer ${token}` : ''
}
}
It took literally forever to find your comment with this example.
The code example by @pyankoff did not work for me.
Instead, I had to supply the auth token in connectionParams.authorization
rather than connectionParams.headers.Authorization
:
const wsLink = new WebSocketLink({
uri: WS_URL,
options: {
lazy: true,
reconnect: true,
connectionParams: async () => {
const token = await getToken();
return {
// this works for me
authorization: token ? `Bearer ${token}` : "",
// this did not work for me
//headers: {Authorization: token ? `Bearer ${token}` : ""},
};
},
},
});
@ARMATAV I've tried to implement code as mentioned, but it gives me type error for some of the wsClient functions. For example, when I try to call wsClient.sendMessage({})
, it gives me Property 'sendMessage' is private and only accessible within class 'SubscriptionClient'.
Is there any issue with typescript support?
@ARMATAV I've tried to implement code as mentioned, but it gives me type error for some of the wsClient functions. For example, when I try to call
wsClient.sendMessage({})
, it gives meProperty 'sendMessage' is private and only accessible within class 'SubscriptionClient'.
Is there any issue with typescript support?
If I recall, yes - // @ts-ignore
that thing and it will work I believe
For me I needed to pass the authentication information on every request in a stateless
manner.
WebSockets won't let you pass HttpHeaders as stated, and I did not want a stateful authentication via cookies. To accomplish this, we'll make use of the WebSocket protocols
argument shown below to send the authentication information.
var aWebSocket = new WebSocket(url [, protocols]);
https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket
According to the MDN docs, "The WebSocket.protocol read-only property returns the name of the sub-protocol the server selected;".
Your server implementation will likely have a check for this to ensure protocol compatibility. Good news is we can pass many protocols.
From the apollographql/subscription-transport-ws
source code, the WebSocket
client is instantiated within the connect
private method
private connect() {
this.client = new this.wsImpl(this.url, this.wsProtocols, ...this.wsOptionArguments);
...
}
We can pass the protools synchronously by instantiating the SubscriptionClient
class like this:
const wsClient = new SubscriptionClient(<WS_LINK>, { reconnect: true, lazy: true }, <WebSocket implementation | undefined>, [, <protocols>]);
However I wanted to be able to asynchronously pass in the authentication information once it changes. The class property wsProtocol
in the apollographql/subscription-transport-ws
stores the protocol
information, however it is private.
private wsProtocols: string | string[];
To asynchronously send the WebSocket Authentication information, we need to extend the SubsciptionClient
as below:
import { ExecutionResult } from 'graphql';
import { BehaviorSubject, skip } from 'rxjs';
import { ClientOptions, Observable, OperationOptions, SubscriptionClient } from 'subscriptions-transport-ws';
export class CustomSubscriptionClient extends SubscriptionClient {
protocols = new BehaviorSubject<string[]>(['graphql-ws']); // where `graphql-ws` is the default protocol
constructor(url: string, options?: ClientOptions, webSocketImpl?: WebSocket, webSocketProtocols?: string | string[]) {
super(url, options, webSocketImpl, webSocketProtocols);
this.protocols.pipe(skip(1)).subscribe((wsProtocols) => {
Object.assign(this, { wsProtocols }); // this hack overrides the private variable `wsProtocols` to set our authentication information
this.close(false);
});
}
public request(request: OperationOptions): Observable<ExecutionResult> {
return super.request(request);
}
}
We can then create a WebSocket client and link as follows:
...
const wsClient = new CustomSubscriptionClient(<WS_LINK>, { reconnect: true, lazy: true });
const wsLink = new WebSocketLink(wsClient.use([{ applyMiddleware: wsAuth }]));
Somewhere else in your code, e.g after authentication you can update the authentication information as follows:
wsClient.protocols.next(['graphql-ws', <AUTH_INFO_STRING>])
On logout user you can do
wsClient.protocols.next(['graphql-ws'])
Note: This fix works by restarting the WebSocket client each time you set a new authentication information, it relies on the close
method on the subscription client which restarts the WebSocket client if its isForced
argument is false.
The server will then have access to the authentication information in its subprotocols
headers on each request.
I personally use the python channels
subscription implementation on Django
so my server authentication middleware looks like this:
class WSAuthMiddleware(BaseMiddleware):
async def populate_scope(self, scope):
token = scope.get("subprotocols")[1]
user = None
if token is None:
raise ValueError("WSAuthMiddleware cannot find authorization in scope. ")
# # Add it to the scope if it's not there already
if "user" not in scope:
scope["user"] = AnonymousUser()
if token is not None:
user = await sync_to_async(get_user_by_token, thread_sensitive=True)(
token, scope
)
scope["user"] = user
...
Hope this helps someone.
this work for me =>
connectionParams: async () => {
const token = await getToken();
return {
// this works for me
authorization: token ? `Bearer ${token}` : "",
// this did not work for me
//headers: {Authorization: token ? `Bearer ${token}` : ""},
};
},
Intended outcome:
Since Apollo Client 2 it is not possible to pass custom HTTP Header to WebSocket connection. In Apollo Client 1 it was possible by
Middleware
, but since version 2 it is not. I tried with additional linkconcat
, or by applyingMiddleware
tosubscriptionClient
.Sample with authorized link:
Sample with
subscriptionClient Middleware
Versions