Comments (5)
I've been running with the above approach for several months and it works, including reconnecting but it's got some issues.
- There is no way to send a bearer token in the header from the browser, and I'd rather not use cookies, nor have access tokens in URLs, even if it is secured over wss.
- There is no way to send an access denied message back to the client in
server.on('upgrade')
.- Despite this being the recommended auth approach here: https://github.com/websockets/ws/#client-authentication, it does not appear capable of being graceful. I tried it and the browser does not not get the unauthorized message.
- I've tried a few approaches like
socket.close(code, reason)
but nothing seems to reach the browser. - I have concluded the only way to send a message back to the browser is to complete the handshake and open the connection.
So I set about rewriting last night and while researching, stumbled across this issue. So here's my new approach.
Authenticate websocket connections over websockets
Today I rewrote my authentication implementation to authenticate over websockets before handing off to YJS and I like it a lot better.
Basically, on server:
- Don't authenticate in
wss.handleUpgrade
- Allow the websocket to connect and send the access token as text, then call internal
authenticate
handler - If auth is invalid, send an access-denied message and close the websocket
- if valid, call the real
_setupWSConnection
same on client:
- when connecting a new websocket, hijack the websocket handlers from yjs until authenticated
- send the access token over the websocket
- await response
- if authenticated, then setup the websocket. If access-denied, disconnect provider (permanently) to prevent undesired econnects.
Unfortunately it's a bit invasive, but at this point I have my own local embedded fork of y-websocket
server code rewritten as a class, so I tried it and it worked.
Sample Code
Allow the connection, wire up to the message handler and set conn.authenticated = false
. But do NOT call setupWSConnection
yet.
_onConnection (conn, req, { docName = req.url.slice(1), gc = true } = {}) {
conn.authenticated = false;
conn.on('message', message => {
this._onMessage(conn, conn.doc, message);
});
}
In message handler, if the message is a string, authenticate with it.
Once authenticated, forward messages to original YJS message handler.
async _onMessage (conn, doc, message) {
if (typeof message === 'string') {
const authenticated = await this._authenticate(conn, message);
conn.authenticated = authenticated;
if (authenticated) {
conn.send('authenticated');
this._setupWSConnection(conn, conn.docName, conn.gc);
} else {
conn.send('access-denied');
conn.close();
}
} else if (conn.authenticated) {
this._onCollabMessage(conn, doc, new Uint8Array(message));
} else {
// being precautious but not actually using this yet. may not need it
conn.preauthenticatedMessages.push(message);
}
}
On the client I currently monkey-patch provider.ws
to plug in the auth handlers but I might move this into the y-websocket client:
const provider = new WebsocketProvider(VUE_APP_YS_ENDPOINT, docName , ydoc);
const onConnecting = () => {
const provider_onmessage = provider.ws.onmessage;
provider.ws.onmessage = event => {
const { data } = event
if (typeof data === 'string') {
switch (data) {
case 'authenticated':
provider.ws.onmessage = provider_onmessage
provider_onopen();
break;
case 'authentication-failed':
provider.disconnect();
break;
default:
break;
}
}
}
const provider_onopen = provider.ws.onopen
provider.ws.onopen = event => {
provider.ws.send(accessToken);
}
}
onConnecting();
Because the y-websocket client calls setupWS
on every reconnection attempt, I have to monkey-patch it every time to get it to go through authentication again.
So I have y-websocket emit a 'status' event with a status of 'connecting', and I use that to re-apply the authentication handlers as new websockets are created.
provider.on('status', ({status}) => {
if (status === 'connecting') {
onConnecting()
}
})
This was the shortest distance I could find to a working implementation.
The server is difficult to extend from the outside, with the binary protocol and lack of class methods to override or options to override behavior. I'm not sure how best to extend it or whether it is intended to be forked rather than extended, but here are some ideas:
- emit events (this is just all around useful and I'm also doing it with persistence to trigger indexing)
- on the server I emit
authenticated
,opened
,closed
,connected
,disconnected
and use this for tracking live docs and connections. - on client I just added another
status
event ofconnecting
- on the server I emit
- to avoid having to shuffle event handlers around?
- export a class where users can easily override specific bits
- provide a bunch of named options that override the default internal functions
- OR implement authentication in
y-websocket
- provide generic auth implementation as part of binary protocol and add handling to
y-websocket
, i.e.- servers create the
y-websocket
server with anauthenticate: async function authenticate(accessToken, docName, conn)
option which tellsy-websocket
to use the auth protocol up-front and call thatauthenticate
handler when it receives an access token. - after creating the provider, clients call
await doc.provider.authenticate(accessToken)
in order to go fromconnecting
toconnected
.
- servers create the
- some benefits I see are
- eliminating the confusion and questions around doing authentication with
y-websocket
- avoiding half-baked
y-websocket
implementations across the community using the ungraceful 'upgrade' approach with cookies or tokens in URLs - massively improved compatibility of secure
y-websocket
client apps and servers built by different developers - potentially also use for web-rtc / p2p auth?
- eliminating the confusion and questions around doing authentication with
- provide generic auth implementation as part of binary protocol and add handling to
from y-websocket.
To do auth and stop client reconnections we can use websocket's connection.close(CUSTOM_CODE). It's fast, because websockets are not used to send auth messages.
On the server side:
export const WEBSOCKET_AUTH_FAILED = 4000;
this.wss.on("connection", this.onConnection);
protected onConnection = async (connection: WebSocket, request: http.IncomingMessage) => {
console.log("socket on connection");
// authenticate can get JWT from URL or cookie
if (!this.authenticate(request)) {
connection.close(WEBSOCKET_AUTH_FAILED);
}
...
}
On the client side (modify y-websocket.js), see // Do not reconnect if auth failed:
websocket.onclose = (e) => {
provider.ws = null
provider.wsconnecting = false
if (provider.wsconnected) {
provider.wsconnected = false
provider.synced = false
// update awareness (all users except local left)
awarenessProtocol.removeAwarenessStates(provider.awareness, Array.from(provider.awareness.getStates().keys()).filter(client => client !== provider.doc.clientID), provider)
provider.emit('status', [{
status: 'disconnected'
}])
} else {
provider.wsUnsuccessfulReconnects++
}
// Do not reconnect if auth failed
if(e.code === WEBSOCKET_AUTH_FAILED) {
console.log("Auth failed", e.code);
return;
}
// Start with no reconnect timeout and increase timeout by
// log10(wsUnsuccessfulReconnects).
// The idea is to increase reconnect timeout slowly and have no reconnect
// timeout at the beginning (log(1) = 0)
setTimeout(setupWS, math.min(math.log10(provider.wsUnsuccessfulReconnects + 1) * reconnectTimeoutBase, maxReconnectTimeout), provider)
}
from y-websocket.
I took @WinstonFassett's code and made a fork that supports generic access token authentication. The server exports an authenticate
method, and the client adds the auth
field to the WebsocketProvider
constructor. Pass your access token on auth
and it will be forwarded to the authenticate
method:
Server:
const { createServer } = require('y-websocket-auth/server')
const server = createServer({
// accessToken is passed as { auth: ACCESS_TOKEN }
// in the WebsocketProvider constructor on the client-side
authenticate: (accessToken: string) => {
// do authentication
return true
}
})
server.listen(port, host, () => {
console.log(`running at '${host}' on port ${port}`)
})
Client:
const wsProvider = new WebsocketProvider(
'ws://localhost:1234',
'my-roomname',
ydoc,
{ auth: ACCESS_TOKEN }
)
Hope this helps others. Happy to accept contributions if you'd like to extend the API.
from y-websocket.
Hey @WinstonFassett, thanks for your details post on authN. I you changes to y-protocol and y-websocket to add support didn't get merged into the main project right?
Is forking the best approach here? just want to make sure I'm not missing a trick.
from y-websocket.
I took @WinstonFassett's code and made a fork that supports generic access token authentication. The server exports an
authenticate
method, and the client adds theauth
field to theWebsocketProvider
constructor. Pass your access token onauth
and it will be forwarded to theauthenticate
method:Server:
const { createServer } = require('y-websocket-auth/server') const server = createServer({ // accessToken is passed as { auth: ACCESS_TOKEN } // in the WebsocketProvider constructor on the client-side authenticate: (accessToken: string) => { // do authentication return true } }) server.listen(port, host, () => { console.log(`running at '${host}' on port ${port}`) })Client:
const wsProvider = new WebsocketProvider( 'ws://localhost:1234', 'my-roomname', ydoc, { auth: ACCESS_TOKEN } )Hope this helps others. Happy to accept contributions if you'd like to extend the API.
the demo did not mention the auth failed logic. the key is how to handle the failed of auth.
from y-websocket.
Related Issues (20)
- Callback object content is empty
- Could not find a declaration file for module 'y-websocket' HOT 3
- Offline modus or pending connect
- Extraneous updates where origin == null when using full reload
- how to detect the server side ping message
- Websocket closes on tab minify/switch after +/- 30s. How to handle?
- Server-side reloaded ydoc version is getting overwritten by client-side version upon page refresh
- Missing doc data binding when refreshing the page
- the new heap suddenly increase HOT 3
- how can I stop y-websocket-server? HOT 1
- Version 1.5.2 breaks y-websocket in the browser HOT 4
- Can't get text data on server HOT 6
- WebSocket connection to 'wss://localhost:9999/' failed HOT 1
- Persistance directory HOT 1
- "type":"module" / SyntaxError: Cannot use import statement outside a module
- cannot connect to y-websocket with node HOT 1
- yrb-actioncable sync awareness
- In bin/utils.js, closeConn(..) doesn't handle correctly if persistence is null HOT 1
- npx y-websocket does not work HOT 1
- How to verify permissions in websocket
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from y-websocket.