Firebase server-side auth with persistent websocket connection
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
0
down vote
favorite
I've been diving a bit into JavaScript for the first time in forever, generally trying things out, and getting acquainted.
I gave my toy project the following requirements:
- single-page react app
- uses firebase for authentication
- matching server-side session with a persistent websocket connection.
- the connection isn't "established" until the server has validated the firebase authentication.
Since I needed to do a query-response handshake over the websocket, I decided that I might as well generalize that, since I need to do it a lot more further down the line.
client-side session.js
:
import * as firebase from 'firebase';
import EventTarget from 'event-target-shim';
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a server.
* The server is expected to be using the matching ServerSession class.
*/
class ClientSession extends EventTarget
/** Creates a session.
* @param Object cfg - The session connection info
* @param cfg.firebaseConfig - A firebase configuration, as expected by firebase.initializeApp()
* @param cfg.endpoint - The url to use when connecting the websocket.
*/
constructor(cfg)
super();
this._config = cfg;
this._webSocket = null;
this._firebaseApp = firebase.initializeApp(cfg.firebaseConfig);
this.firebase_auth = firebase.auth(this._firebaseApp);
this._next_reply_id = 0;
this._reply_handlers = ;
this._setupFirebaseAuthListener();
/**
* Sends a message to the server.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
*/
send(type, data)
const msg =
type: type,
data: data,
;
this._webSocket.send(JSON.stringify(msg));
/**
* Sends a message to the server, and expect an associated reply.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
* @returns Promise resolves to the data returned by the server.
*/
sendQuery(type, data)
const msg =
type: type,
reply_key: this._generateReplyKey(),
data: data,
;
var self = this;
return new Promise((resolve, reject) =>
self._reply_handlers[msg.reply_key] =
resolve: resolve,
reject: reject
;
self._webSocket.send(JSON.stringify(msg));
);
_handleMessage(msg)
if('reply_key' in msg)
var handler = this._reply_handlers[msg.reply_key];
if('error' in msg)
handler.reject(msg.error);
else
handler.resolve(msg.data);
delete this._reply_handlers[msg.reply_key];
else
this.dispatchEvent(new CustomEvent(msg.type, detail: msg.data));
_generateReplyKey()
return String(this._next_reply_id++);
_setupFirebaseAuthListener()
this.firebase_auth.onAuthStateChanged(user =>
if(user)
// We do not qualify as "connected" until the serverside Session
// has been established.
this._setupWebSocket();
else
if(this._webSocket)
// Notification will be handled by the websocket closing.
this._webSocket.close();
this._webSocket = null;
);
_setupWebSocket()
var ws = new WebSocket(this._config.endpoint);
this._webSocket = ws;
ws.onopen = e =>
// Send the login handshake.
firebase.auth(this._firebaseApp).currentUser.getIdToken()
.then(tok =>
return this.sendQuery(SESSION_AUTH_MSG, token: tok);
)
.then(rep =>
this.dispatchEvent(new Event('session_authenticated'));
)
.catch(e =>
this.dispatchEvent(new CustomEvent('error', detail: e));
);
;
ws.onclose = e =>
this.dispatchEvent(new Event('session_disconnected'));
// If we are still logged in to firebase, then odds are this
// was just the connection timing out.
if(firebase.auth(this._firebaseApp).currentUser)
// TODO: expanding backoff perhaps?
setTimeout(this._setupWebSocket.bind(this), 1000);
;
ws.onError = e =>
// TODO: retry connection?
;
ws.onmessage = e =>
try
this._handleMessage(JSON.parse(e.data));
catch(err)
console.error(`bad message received: $e.data, $err.message`);
;
export default ClientSession;
server-side session.js
:
const EventEmitter = require('events');
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a client.
* The client is expected to be using the matching ClientSession class.
*/
class ServerSession extends EventEmitter
/** Creates a session.
* @param fb_admin - firebase administration, as returned by admin.initializeApp()
* @param ws - A firebase configuration, as expected by firebase.initializeApp()
*/
constructor(fb_admin, ws)
super();
this._ws = ws;
this.on(SESSION_AUTH_MSG, (msg) =>
fb_admin.auth().verifyIdToken(msg.data.token)
.then( (token) =>
var uid = token.uid;
this.firebaseUid = uid;
this.reply(msg, );
).catch((error) =>
// Handle error
this.replyFailure(msg, error.message);
);
);
ws.on('message', (msg) =>
try
var msg_obj = JSON.parse(msg);
if( this._isAuthenticated()
catch(e)
console.error(`bad message received: $e`);
);
/**
* Sends a reply to a message
* @param orig - The original message you are replying to
* @param data - the payload of the reply
*/
reply(orig, data)
const msg =
reply_key: orig.reply_key,
data: data
;
this._send(msg);
/**
* Notifies that a reply for a message could not be generated
* @param orig - The original message you are replying to
* @param err_msgr - The error message.
*/
replyFailure(orig, err_msg)
const msg =
reply_key: orig.reply_key,
error: err_msg
;
this._send(msg);
/**
* Sends a message to the client
* @param Object msg - The message to send.
*/
send(type, msg)
const real_msg =
type: type,
data: msg
;
this._send(real_msg);
_isAuthenticated()
return 'firebaseUid' in this;
_send(msg)
this._ws.send(JSON.stringify(msg));
module.exports = ServerSession;
Usage example:
server-side:
const Session = require('./session')
const WebSocket = require('ws')
const admin = require('firebase-admin');
const serviceAccount = ...redacted...
admin.initializeApp(
credential: admin.credential.cert(serviceAccount),
databaseURL: ...redacted...
);
var server = new WebSocket.Server(port: 10000);
server.on('connection', (ws, req) =>
console.log('connection established');
var session = new Session(admin, ws);
session.on("foo", e =>
session.send("bar", e.data*2);
);
session.on("ping", e =>
session.reply(e, message:"Hello from server.");
);
);
client-side (lives inside a react component):
var cfg =
firebaseConfig: fb_cfg,
endpoint: "ws://localhost:10000"
;
this._session = new ClientSession(cfg);
this._session.addEventListener('session_authenticated', e =>
this._session.sendQuery("ping", )
.then(d =>
console.log("ping reply: " + d.message);
);
this._session.send("foo", 2);
);
this._session.addEventListener("bar" , e =>
console.log(`bar received: $e.detail`);
);
N.B.
- My current project does not need the server to make query->reply messages to the client, hence the ommision.
- Streaming replies is an obvious next step, but I've got other fish to fry before hand.
- I'm really not a big fan of the session handling events getting mixed in with the user-specified events (hence the big
session_
prefixes, but apart from creating an extra layer of indirection for the user, it "felt" like the right way to tackle this. - I don't like checking for
firebaseUid in this
on every single received message in the server. I guess I could swap out the "message" handler after handshaking, but that also feels icky.
Since I'm a huge JavaScript neophite, I'm sure there's a lot to improve in there. So I'm looking
for any and all feedback on style, general practices, functionallity, etc.
javascript node.js firebase
add a comment |Â
up vote
0
down vote
favorite
I've been diving a bit into JavaScript for the first time in forever, generally trying things out, and getting acquainted.
I gave my toy project the following requirements:
- single-page react app
- uses firebase for authentication
- matching server-side session with a persistent websocket connection.
- the connection isn't "established" until the server has validated the firebase authentication.
Since I needed to do a query-response handshake over the websocket, I decided that I might as well generalize that, since I need to do it a lot more further down the line.
client-side session.js
:
import * as firebase from 'firebase';
import EventTarget from 'event-target-shim';
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a server.
* The server is expected to be using the matching ServerSession class.
*/
class ClientSession extends EventTarget
/** Creates a session.
* @param Object cfg - The session connection info
* @param cfg.firebaseConfig - A firebase configuration, as expected by firebase.initializeApp()
* @param cfg.endpoint - The url to use when connecting the websocket.
*/
constructor(cfg)
super();
this._config = cfg;
this._webSocket = null;
this._firebaseApp = firebase.initializeApp(cfg.firebaseConfig);
this.firebase_auth = firebase.auth(this._firebaseApp);
this._next_reply_id = 0;
this._reply_handlers = ;
this._setupFirebaseAuthListener();
/**
* Sends a message to the server.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
*/
send(type, data)
const msg =
type: type,
data: data,
;
this._webSocket.send(JSON.stringify(msg));
/**
* Sends a message to the server, and expect an associated reply.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
* @returns Promise resolves to the data returned by the server.
*/
sendQuery(type, data)
const msg =
type: type,
reply_key: this._generateReplyKey(),
data: data,
;
var self = this;
return new Promise((resolve, reject) =>
self._reply_handlers[msg.reply_key] =
resolve: resolve,
reject: reject
;
self._webSocket.send(JSON.stringify(msg));
);
_handleMessage(msg)
if('reply_key' in msg)
var handler = this._reply_handlers[msg.reply_key];
if('error' in msg)
handler.reject(msg.error);
else
handler.resolve(msg.data);
delete this._reply_handlers[msg.reply_key];
else
this.dispatchEvent(new CustomEvent(msg.type, detail: msg.data));
_generateReplyKey()
return String(this._next_reply_id++);
_setupFirebaseAuthListener()
this.firebase_auth.onAuthStateChanged(user =>
if(user)
// We do not qualify as "connected" until the serverside Session
// has been established.
this._setupWebSocket();
else
if(this._webSocket)
// Notification will be handled by the websocket closing.
this._webSocket.close();
this._webSocket = null;
);
_setupWebSocket()
var ws = new WebSocket(this._config.endpoint);
this._webSocket = ws;
ws.onopen = e =>
// Send the login handshake.
firebase.auth(this._firebaseApp).currentUser.getIdToken()
.then(tok =>
return this.sendQuery(SESSION_AUTH_MSG, token: tok);
)
.then(rep =>
this.dispatchEvent(new Event('session_authenticated'));
)
.catch(e =>
this.dispatchEvent(new CustomEvent('error', detail: e));
);
;
ws.onclose = e =>
this.dispatchEvent(new Event('session_disconnected'));
// If we are still logged in to firebase, then odds are this
// was just the connection timing out.
if(firebase.auth(this._firebaseApp).currentUser)
// TODO: expanding backoff perhaps?
setTimeout(this._setupWebSocket.bind(this), 1000);
;
ws.onError = e =>
// TODO: retry connection?
;
ws.onmessage = e =>
try
this._handleMessage(JSON.parse(e.data));
catch(err)
console.error(`bad message received: $e.data, $err.message`);
;
export default ClientSession;
server-side session.js
:
const EventEmitter = require('events');
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a client.
* The client is expected to be using the matching ClientSession class.
*/
class ServerSession extends EventEmitter
/** Creates a session.
* @param fb_admin - firebase administration, as returned by admin.initializeApp()
* @param ws - A firebase configuration, as expected by firebase.initializeApp()
*/
constructor(fb_admin, ws)
super();
this._ws = ws;
this.on(SESSION_AUTH_MSG, (msg) =>
fb_admin.auth().verifyIdToken(msg.data.token)
.then( (token) =>
var uid = token.uid;
this.firebaseUid = uid;
this.reply(msg, );
).catch((error) =>
// Handle error
this.replyFailure(msg, error.message);
);
);
ws.on('message', (msg) =>
try
var msg_obj = JSON.parse(msg);
if( this._isAuthenticated()
catch(e)
console.error(`bad message received: $e`);
);
/**
* Sends a reply to a message
* @param orig - The original message you are replying to
* @param data - the payload of the reply
*/
reply(orig, data)
const msg =
reply_key: orig.reply_key,
data: data
;
this._send(msg);
/**
* Notifies that a reply for a message could not be generated
* @param orig - The original message you are replying to
* @param err_msgr - The error message.
*/
replyFailure(orig, err_msg)
const msg =
reply_key: orig.reply_key,
error: err_msg
;
this._send(msg);
/**
* Sends a message to the client
* @param Object msg - The message to send.
*/
send(type, msg)
const real_msg =
type: type,
data: msg
;
this._send(real_msg);
_isAuthenticated()
return 'firebaseUid' in this;
_send(msg)
this._ws.send(JSON.stringify(msg));
module.exports = ServerSession;
Usage example:
server-side:
const Session = require('./session')
const WebSocket = require('ws')
const admin = require('firebase-admin');
const serviceAccount = ...redacted...
admin.initializeApp(
credential: admin.credential.cert(serviceAccount),
databaseURL: ...redacted...
);
var server = new WebSocket.Server(port: 10000);
server.on('connection', (ws, req) =>
console.log('connection established');
var session = new Session(admin, ws);
session.on("foo", e =>
session.send("bar", e.data*2);
);
session.on("ping", e =>
session.reply(e, message:"Hello from server.");
);
);
client-side (lives inside a react component):
var cfg =
firebaseConfig: fb_cfg,
endpoint: "ws://localhost:10000"
;
this._session = new ClientSession(cfg);
this._session.addEventListener('session_authenticated', e =>
this._session.sendQuery("ping", )
.then(d =>
console.log("ping reply: " + d.message);
);
this._session.send("foo", 2);
);
this._session.addEventListener("bar" , e =>
console.log(`bar received: $e.detail`);
);
N.B.
- My current project does not need the server to make query->reply messages to the client, hence the ommision.
- Streaming replies is an obvious next step, but I've got other fish to fry before hand.
- I'm really not a big fan of the session handling events getting mixed in with the user-specified events (hence the big
session_
prefixes, but apart from creating an extra layer of indirection for the user, it "felt" like the right way to tackle this. - I don't like checking for
firebaseUid in this
on every single received message in the server. I guess I could swap out the "message" handler after handshaking, but that also feels icky.
Since I'm a huge JavaScript neophite, I'm sure there's a lot to improve in there. So I'm looking
for any and all feedback on style, general practices, functionallity, etc.
javascript node.js firebase
add a comment |Â
up vote
0
down vote
favorite
up vote
0
down vote
favorite
I've been diving a bit into JavaScript for the first time in forever, generally trying things out, and getting acquainted.
I gave my toy project the following requirements:
- single-page react app
- uses firebase for authentication
- matching server-side session with a persistent websocket connection.
- the connection isn't "established" until the server has validated the firebase authentication.
Since I needed to do a query-response handshake over the websocket, I decided that I might as well generalize that, since I need to do it a lot more further down the line.
client-side session.js
:
import * as firebase from 'firebase';
import EventTarget from 'event-target-shim';
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a server.
* The server is expected to be using the matching ServerSession class.
*/
class ClientSession extends EventTarget
/** Creates a session.
* @param Object cfg - The session connection info
* @param cfg.firebaseConfig - A firebase configuration, as expected by firebase.initializeApp()
* @param cfg.endpoint - The url to use when connecting the websocket.
*/
constructor(cfg)
super();
this._config = cfg;
this._webSocket = null;
this._firebaseApp = firebase.initializeApp(cfg.firebaseConfig);
this.firebase_auth = firebase.auth(this._firebaseApp);
this._next_reply_id = 0;
this._reply_handlers = ;
this._setupFirebaseAuthListener();
/**
* Sends a message to the server.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
*/
send(type, data)
const msg =
type: type,
data: data,
;
this._webSocket.send(JSON.stringify(msg));
/**
* Sends a message to the server, and expect an associated reply.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
* @returns Promise resolves to the data returned by the server.
*/
sendQuery(type, data)
const msg =
type: type,
reply_key: this._generateReplyKey(),
data: data,
;
var self = this;
return new Promise((resolve, reject) =>
self._reply_handlers[msg.reply_key] =
resolve: resolve,
reject: reject
;
self._webSocket.send(JSON.stringify(msg));
);
_handleMessage(msg)
if('reply_key' in msg)
var handler = this._reply_handlers[msg.reply_key];
if('error' in msg)
handler.reject(msg.error);
else
handler.resolve(msg.data);
delete this._reply_handlers[msg.reply_key];
else
this.dispatchEvent(new CustomEvent(msg.type, detail: msg.data));
_generateReplyKey()
return String(this._next_reply_id++);
_setupFirebaseAuthListener()
this.firebase_auth.onAuthStateChanged(user =>
if(user)
// We do not qualify as "connected" until the serverside Session
// has been established.
this._setupWebSocket();
else
if(this._webSocket)
// Notification will be handled by the websocket closing.
this._webSocket.close();
this._webSocket = null;
);
_setupWebSocket()
var ws = new WebSocket(this._config.endpoint);
this._webSocket = ws;
ws.onopen = e =>
// Send the login handshake.
firebase.auth(this._firebaseApp).currentUser.getIdToken()
.then(tok =>
return this.sendQuery(SESSION_AUTH_MSG, token: tok);
)
.then(rep =>
this.dispatchEvent(new Event('session_authenticated'));
)
.catch(e =>
this.dispatchEvent(new CustomEvent('error', detail: e));
);
;
ws.onclose = e =>
this.dispatchEvent(new Event('session_disconnected'));
// If we are still logged in to firebase, then odds are this
// was just the connection timing out.
if(firebase.auth(this._firebaseApp).currentUser)
// TODO: expanding backoff perhaps?
setTimeout(this._setupWebSocket.bind(this), 1000);
;
ws.onError = e =>
// TODO: retry connection?
;
ws.onmessage = e =>
try
this._handleMessage(JSON.parse(e.data));
catch(err)
console.error(`bad message received: $e.data, $err.message`);
;
export default ClientSession;
server-side session.js
:
const EventEmitter = require('events');
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a client.
* The client is expected to be using the matching ClientSession class.
*/
class ServerSession extends EventEmitter
/** Creates a session.
* @param fb_admin - firebase administration, as returned by admin.initializeApp()
* @param ws - A firebase configuration, as expected by firebase.initializeApp()
*/
constructor(fb_admin, ws)
super();
this._ws = ws;
this.on(SESSION_AUTH_MSG, (msg) =>
fb_admin.auth().verifyIdToken(msg.data.token)
.then( (token) =>
var uid = token.uid;
this.firebaseUid = uid;
this.reply(msg, );
).catch((error) =>
// Handle error
this.replyFailure(msg, error.message);
);
);
ws.on('message', (msg) =>
try
var msg_obj = JSON.parse(msg);
if( this._isAuthenticated()
catch(e)
console.error(`bad message received: $e`);
);
/**
* Sends a reply to a message
* @param orig - The original message you are replying to
* @param data - the payload of the reply
*/
reply(orig, data)
const msg =
reply_key: orig.reply_key,
data: data
;
this._send(msg);
/**
* Notifies that a reply for a message could not be generated
* @param orig - The original message you are replying to
* @param err_msgr - The error message.
*/
replyFailure(orig, err_msg)
const msg =
reply_key: orig.reply_key,
error: err_msg
;
this._send(msg);
/**
* Sends a message to the client
* @param Object msg - The message to send.
*/
send(type, msg)
const real_msg =
type: type,
data: msg
;
this._send(real_msg);
_isAuthenticated()
return 'firebaseUid' in this;
_send(msg)
this._ws.send(JSON.stringify(msg));
module.exports = ServerSession;
Usage example:
server-side:
const Session = require('./session')
const WebSocket = require('ws')
const admin = require('firebase-admin');
const serviceAccount = ...redacted...
admin.initializeApp(
credential: admin.credential.cert(serviceAccount),
databaseURL: ...redacted...
);
var server = new WebSocket.Server(port: 10000);
server.on('connection', (ws, req) =>
console.log('connection established');
var session = new Session(admin, ws);
session.on("foo", e =>
session.send("bar", e.data*2);
);
session.on("ping", e =>
session.reply(e, message:"Hello from server.");
);
);
client-side (lives inside a react component):
var cfg =
firebaseConfig: fb_cfg,
endpoint: "ws://localhost:10000"
;
this._session = new ClientSession(cfg);
this._session.addEventListener('session_authenticated', e =>
this._session.sendQuery("ping", )
.then(d =>
console.log("ping reply: " + d.message);
);
this._session.send("foo", 2);
);
this._session.addEventListener("bar" , e =>
console.log(`bar received: $e.detail`);
);
N.B.
- My current project does not need the server to make query->reply messages to the client, hence the ommision.
- Streaming replies is an obvious next step, but I've got other fish to fry before hand.
- I'm really not a big fan of the session handling events getting mixed in with the user-specified events (hence the big
session_
prefixes, but apart from creating an extra layer of indirection for the user, it "felt" like the right way to tackle this. - I don't like checking for
firebaseUid in this
on every single received message in the server. I guess I could swap out the "message" handler after handshaking, but that also feels icky.
Since I'm a huge JavaScript neophite, I'm sure there's a lot to improve in there. So I'm looking
for any and all feedback on style, general practices, functionallity, etc.
javascript node.js firebase
I've been diving a bit into JavaScript for the first time in forever, generally trying things out, and getting acquainted.
I gave my toy project the following requirements:
- single-page react app
- uses firebase for authentication
- matching server-side session with a persistent websocket connection.
- the connection isn't "established" until the server has validated the firebase authentication.
Since I needed to do a query-response handshake over the websocket, I decided that I might as well generalize that, since I need to do it a lot more further down the line.
client-side session.js
:
import * as firebase from 'firebase';
import EventTarget from 'event-target-shim';
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a server.
* The server is expected to be using the matching ServerSession class.
*/
class ClientSession extends EventTarget
/** Creates a session.
* @param Object cfg - The session connection info
* @param cfg.firebaseConfig - A firebase configuration, as expected by firebase.initializeApp()
* @param cfg.endpoint - The url to use when connecting the websocket.
*/
constructor(cfg)
super();
this._config = cfg;
this._webSocket = null;
this._firebaseApp = firebase.initializeApp(cfg.firebaseConfig);
this.firebase_auth = firebase.auth(this._firebaseApp);
this._next_reply_id = 0;
this._reply_handlers = ;
this._setupFirebaseAuthListener();
/**
* Sends a message to the server.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
*/
send(type, data)
const msg =
type: type,
data: data,
;
this._webSocket.send(JSON.stringify(msg));
/**
* Sends a message to the server, and expect an associated reply.
* @param string type - the message type, used for dispatching on the server.
* @param Object data - the message's payload.
* @returns Promise resolves to the data returned by the server.
*/
sendQuery(type, data)
const msg =
type: type,
reply_key: this._generateReplyKey(),
data: data,
;
var self = this;
return new Promise((resolve, reject) =>
self._reply_handlers[msg.reply_key] =
resolve: resolve,
reject: reject
;
self._webSocket.send(JSON.stringify(msg));
);
_handleMessage(msg)
if('reply_key' in msg)
var handler = this._reply_handlers[msg.reply_key];
if('error' in msg)
handler.reject(msg.error);
else
handler.resolve(msg.data);
delete this._reply_handlers[msg.reply_key];
else
this.dispatchEvent(new CustomEvent(msg.type, detail: msg.data));
_generateReplyKey()
return String(this._next_reply_id++);
_setupFirebaseAuthListener()
this.firebase_auth.onAuthStateChanged(user =>
if(user)
// We do not qualify as "connected" until the serverside Session
// has been established.
this._setupWebSocket();
else
if(this._webSocket)
// Notification will be handled by the websocket closing.
this._webSocket.close();
this._webSocket = null;
);
_setupWebSocket()
var ws = new WebSocket(this._config.endpoint);
this._webSocket = ws;
ws.onopen = e =>
// Send the login handshake.
firebase.auth(this._firebaseApp).currentUser.getIdToken()
.then(tok =>
return this.sendQuery(SESSION_AUTH_MSG, token: tok);
)
.then(rep =>
this.dispatchEvent(new Event('session_authenticated'));
)
.catch(e =>
this.dispatchEvent(new CustomEvent('error', detail: e));
);
;
ws.onclose = e =>
this.dispatchEvent(new Event('session_disconnected'));
// If we are still logged in to firebase, then odds are this
// was just the connection timing out.
if(firebase.auth(this._firebaseApp).currentUser)
// TODO: expanding backoff perhaps?
setTimeout(this._setupWebSocket.bind(this), 1000);
;
ws.onError = e =>
// TODO: retry connection?
;
ws.onmessage = e =>
try
this._handleMessage(JSON.parse(e.data));
catch(err)
console.error(`bad message received: $e.data, $err.message`);
;
export default ClientSession;
server-side session.js
:
const EventEmitter = require('events');
const SESSION_AUTH_MSG = '_session_auth';
/**
* Class representing a firebase-authenticated session with a client.
* The client is expected to be using the matching ClientSession class.
*/
class ServerSession extends EventEmitter
/** Creates a session.
* @param fb_admin - firebase administration, as returned by admin.initializeApp()
* @param ws - A firebase configuration, as expected by firebase.initializeApp()
*/
constructor(fb_admin, ws)
super();
this._ws = ws;
this.on(SESSION_AUTH_MSG, (msg) =>
fb_admin.auth().verifyIdToken(msg.data.token)
.then( (token) =>
var uid = token.uid;
this.firebaseUid = uid;
this.reply(msg, );
).catch((error) =>
// Handle error
this.replyFailure(msg, error.message);
);
);
ws.on('message', (msg) =>
try
var msg_obj = JSON.parse(msg);
if( this._isAuthenticated()
catch(e)
console.error(`bad message received: $e`);
);
/**
* Sends a reply to a message
* @param orig - The original message you are replying to
* @param data - the payload of the reply
*/
reply(orig, data)
const msg =
reply_key: orig.reply_key,
data: data
;
this._send(msg);
/**
* Notifies that a reply for a message could not be generated
* @param orig - The original message you are replying to
* @param err_msgr - The error message.
*/
replyFailure(orig, err_msg)
const msg =
reply_key: orig.reply_key,
error: err_msg
;
this._send(msg);
/**
* Sends a message to the client
* @param Object msg - The message to send.
*/
send(type, msg)
const real_msg =
type: type,
data: msg
;
this._send(real_msg);
_isAuthenticated()
return 'firebaseUid' in this;
_send(msg)
this._ws.send(JSON.stringify(msg));
module.exports = ServerSession;
Usage example:
server-side:
const Session = require('./session')
const WebSocket = require('ws')
const admin = require('firebase-admin');
const serviceAccount = ...redacted...
admin.initializeApp(
credential: admin.credential.cert(serviceAccount),
databaseURL: ...redacted...
);
var server = new WebSocket.Server(port: 10000);
server.on('connection', (ws, req) =>
console.log('connection established');
var session = new Session(admin, ws);
session.on("foo", e =>
session.send("bar", e.data*2);
);
session.on("ping", e =>
session.reply(e, message:"Hello from server.");
);
);
client-side (lives inside a react component):
var cfg =
firebaseConfig: fb_cfg,
endpoint: "ws://localhost:10000"
;
this._session = new ClientSession(cfg);
this._session.addEventListener('session_authenticated', e =>
this._session.sendQuery("ping", )
.then(d =>
console.log("ping reply: " + d.message);
);
this._session.send("foo", 2);
);
this._session.addEventListener("bar" , e =>
console.log(`bar received: $e.detail`);
);
N.B.
- My current project does not need the server to make query->reply messages to the client, hence the ommision.
- Streaming replies is an obvious next step, but I've got other fish to fry before hand.
- I'm really not a big fan of the session handling events getting mixed in with the user-specified events (hence the big
session_
prefixes, but apart from creating an extra layer of indirection for the user, it "felt" like the right way to tackle this. - I don't like checking for
firebaseUid in this
on every single received message in the server. I guess I could swap out the "message" handler after handshaking, but that also feels icky.
Since I'm a huge JavaScript neophite, I'm sure there's a lot to improve in there. So I'm looking
for any and all feedback on style, general practices, functionallity, etc.
javascript node.js firebase
asked Apr 30 at 21:44
Frank
2,927319
2,927319
add a comment |Â
add a comment |Â
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f193305%2ffirebase-server-side-auth-with-persistent-websocket-connection%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password