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
feat: Authentication by Websocket #3955
Comments
In the WS example it works as we use cookies for auth, but agree it would be nice to do this |
@KATT i am ready to help, but we need to establish which solution should be selected as default. All of them has tradeoffs. Do you prefer to discuss it on meeting: or enumerate goals here. My proposal:
What will be covered in docs
Please let me know if you agree or want to modify this plan? |
This got me curious. Is it actually possible to add headers when establishing a websocket connection? All my research into this had me believe that it isn't possible to create a websocket Or would this involve implementing the upgrade/handshake manually? Regardless, I recently added the possibility to provide the connectionUrl as a getter function for the createWsClient. In my case this works fine, by setting the auth token as a query parameter in the connectionUrl. But I guess this exposes the token in a higher degree compared to if it was in a header.🤔 |
Feel free to submit PRs to this -- I'm happy with any solution that copies existing solutions out there like how Apollo, urql, or Relay does this |
4 hours later, and a lot of debugging, I finally managed to fix the issue, which was for some reason due to me not defining onStarted as an event to the websocket. For me, the issue was resolved as I use cookie auth anyways. It's just that I had a bug with the way I initialized it that forced me to authenticate a different way, I will delete my original comment now because it is misleading. |
This feature would prove exceptionally valuable and impressive, particularly if it includes built-in support for managing refresh tokens. Its utility would be evident in single-page applications (SPAs) or across various mobile and desktop applications, thereby broadening the reach and application of tRPC. |
I don't have any bandwidth to work on this, but pledged $100 to help support this work. Would love this feature. |
I am doing it by having a trpc mutation handler called export const createContext = <TRequest, TResponse>(
opts: NodeHTTPCreateContextFnOptions<TRequest, TResponse>,
) => {
return {
...ctx,
req: opts.req,
ws: opts.res instanceof WebSocket ? opts.res : undefined,
};
}; const router = router({
authenticate: publicProcedure.input(authenticateSchema).mutation(async ({ input, ctx }) => {
const { session } = await getSessionByAccessToken(input.accessToken);
if (!session) throw ctx.errors.unauthorized();
if (ctx.ws) {
// Set the access token on the websocket connection
ctx.ws.accessToken = input.accessToken;
}
return {
accessToken: input.accessToken,
expiresAt: jwt.exp * 1000,
user: session.user,
};
}),
}); Now when you call the authenticate route with the websocket link it saves the access token if valid inside the unique websocket connection. After that inside an authenticate middleware you can get the access token on header or websocket connection like this: const accessToken = ctx.ws?.accessToken || ctx.req.headers.authorization;
if (!accessToken) throw ctx.errors.unauthorized();
// your other logic I am also using refresh tokens the same way. Just implement a refresh token mutation handler with input of access and refresh token, set newly created access token on websocket connection For me this is the most intuitive method to authenticate and reauthenticate users inside a (long running) websocket connection without having to handle reconnections after invalidating old access tokens or when tokens expires during active websocket connection. |
I've been researching, and you can prompt the client to redo the WebSocket connection if the access token changes. /**
* Helper function for using authentication on WebSockets.
* @param getUrl A function that returns the URL of the API with the access token.
*/
const protectedWsClient = (getUrl: () => string) => {
let client: TRPCWebSocketClient | undefined;
let prevUrl: string;
return {
close() {
client?.close();
client = undefined;
},
request(op: Operation, callbacks: TCallbacks) {
const url = getUrl();
if (!client || prevUrl !== url) {
client?.close();
prevUrl = url;
client = createWSClient({ url });
}
return client.request(op, callbacks);
},
getConnection() {
if (!client) {
throw new Error("No WebSocket connection.");
}
return client.getConnection();
},
};
}; Then create the // ...
wsLink({
client: protectedWsClient(() => {
const baseUrl = url.replace(/^https?/, "ws");
const tokenQuery = `session=${getAccessToken()}`;
return `${baseUrl}?${tokenQuery}`;
}),
})
// ... And on back-end get the access token from URL: const getQueryParam = (url: string | undefined, paramName: string): string | undefined => {
if (!url) return undefined;
const params = new URLSearchParams(url.split("?")[1]);
const param = params.get(paramName);
return param ?? undefined;
};
const accessToken = ctx.req?.headers?.authorization?.split(" ")[1] ?? getQueryParam(ctx.req?.url, "session"); To make all of this work automatically, you can use the It works like a charm, but you expose the access token directly in the WebSocket connection endpoint — which, in theory, wouldn't be a problem with token rotation in the refresh token and a short lifespan for the access token. Additionally, in general, using URL query parameters on WebSocket are considered safe practice because:
Credits to jitnouz. |
Hello, I've been following this issue as when it was initially created, we had been started to use tRPC and try to get rid of nestJS / graphQL. For websockets we ended up rolling our own, for multiple reasons. But I can provide some feedback on what solution we ended up implemented for authentication (and authorization), in case it helps:
|
Hi @troch, that sounds good. |
How do you pass the websocket connection in a mutation? I'm using Fastify adapter and the websocket connection is only shared with subscription, so it's impossible to set the access token in the context of the websocket during mutation. In that case |
I think it is working for us because we are using the WebSocket for mutations and query as well. For subscription only WebSocket my implementation cannot work. It comes from const handler = applyWSSHandler({
wss,
router: options.trpcRouter,
createContext: (opts) => { // <- here opts.res is instance of WebSocket
return createContext?.(opts);
},
}); |
@ps73 Of course! You are not using splitLink, just a direct WebSocket connection to the API. In my use case, I think the API would be overwhelmed with so many unnecessary WebSocket connections connected to the server at the same time. Even if I had a second client just for WebSocket, if an HTTP connection updated the access token, the WebSocket connection would not have the new value. I will explore alternative solutions, but I appreciate your response! |
Would a possible solution be to support client-side middleware when using wsLink? |
Following this. I've had to write some hacks in my codebases for a solution to get auth to work, albeit rather janky. In the meantime, could some docs be written on how to handle authorization headers with websockets? |
Describe the feature you'd like to request
We know that websocket do not use http headers, but during handshake http request can be used.
Page 14 RFC 6455 https://www.rfc-editor.org/rfc/rfc6455#page-14
Current implementation sending these headers
But I would like to add my custom headers here, eg Authorization.
Connected reddit topic of other user:
https://www.reddit.com/r/node/comments/117fgb5/trpc_correct_way_to_authorize_websocket/
Describe the solution you'd like to see
This problem was solved in apollo client by
in issue apollographql/apollo-client#3967
or even better
Probably when this function will return new headers (eg new bearer token will be added) we probably need restart all operations.
so it seems to be complicated.
Describe alternate solutions
Alternatively we can think about
onOpen
function increateWSClient
or if it is called after handshake, then maybe add functionbeforeOpen
and give end user control over connecting and reconnecting when headers will be changed.Additional information
I can try to contribute with this but need to know if this feature will be accepted and how to design it.
👨👧👦 Contributing
I wrote article about approaches to this problem, so you can read details and discuss here which one should be preferred as official, and maybe instead of this feature we will add docs covering this problem.
https://preciselab.io/trpc/
Summary:
lazy
flag like in apollo clientTRP-60
Funding
The text was updated successfully, but these errors were encountered: