Firebase server-side auth with persistent websocket connection

The name of the pictureThe name of the pictureThe name of the pictureClash 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.







share|improve this question

























    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.







    share|improve this question





















      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.







      share|improve this question











      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.









      share|improve this question










      share|improve this question




      share|improve this question









      asked Apr 30 at 21:44









      Frank

      2,927319




      2,927319

























          active

          oldest

          votes











          Your Answer




          StackExchange.ifUsing("editor", function ()
          return StackExchange.using("mathjaxEditing", function ()
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          );
          );
          , "mathjax-editing");

          StackExchange.ifUsing("editor", function ()
          StackExchange.using("externalEditor", function ()
          StackExchange.using("snippets", function ()
          StackExchange.snippets.init();
          );
          );
          , "code-snippets");

          StackExchange.ready(function()
          var channelOptions =
          tags: "".split(" "),
          id: "196"
          ;
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function()
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled)
          StackExchange.using("snippets", function()
          createEditor();
          );

          else
          createEditor();

          );

          function createEditor()
          StackExchange.prepareEditor(
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: false,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          );



          );








           

          draft saved


          draft discarded


















          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



































          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes










           

          draft saved


          draft discarded


























           


          draft saved


          draft discarded














          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













































































          Popular posts from this blog

          Chat program with C++ and SFML

          Function to Return a JSON Like Objects Using VBA Collections and Arrays

          Will my employers contract hold up in court?