itaylor / redux-socket.io

An opinionated connector between socket.io and redux
410 stars 53 forks source link

Handling authentication #13

Closed sylvainbx closed 8 years ago

sylvainbx commented 8 years ago

Hi, How to handle authentication using redux-socket.io? For example, I'm using JWT with socketio-jwt but socket.io requires to configure the token at connection time, eg.

const socket = io.connect(API_ROOT, {
  'query': 'token=' + jwt_token
});

As jwt_token is retrieved at login time, I store it in the redux's store but redux-socket.io need to be configured before the store is instantiated.

How can I solve this vicious circle?

itaylor commented 8 years ago

So you have a login process that happens before the socket is created, it creates some token and then you want to pass that token to the io.connect.

I've not had this pattern in the apps that I work on, typically I either get the auth token via the websocket, or auth already happened on a separate page and I don't need to fetch it.

I see two options though...

  1. Keep the token outside of the store and don't set up the store until you're ready to set up the token (this would preclude you from using the Redux store before you setup socket.io).
  2. Make a proxy middleware that is a passthough while socketIoMiddleware is not yet set up, and once it has been set up runs the socketIoMiddleware. You'd create this proxy middleware along with the store.
import { createStore, applyMiddleware } from 'redux';
import createSocketIoMiddleware from 'redux-socket.io';
import io from 'socket.io-client';
function reducer(state = {}, action){
  switch(action.type){
    case 'message':
      return Object.assign({}, {message:action.data});
    default:
      return state;
  }
}
let socketIoMiddleware = null;
const proxyMiddleware = store => next => action => {
  if (socketIoMiddleWare !== null) {
     return socketIoMiddleware(store)(next)(action);
  }
  return next(action);
}
let store = applyMiddleware(proxyMiddleware)(createStore)(reducer);
store.subscribe(()=>{
  console.log('new client state', store.getState());
});
store.dispatch({type:'server/hello', data:'Hello!'});

//Later after your login code has run and you have your token
const socket = io.connect(API_ROOT, {
  'query': 'token=' + jwt_token
});
socketIoMiddleware = createSocketIoMiddleware(socket, "server/");;

Note that I haven't run the above code or anything, it's just meant to be illustrative of the approach, which would be to set up the store first, and let the middleware start taking effect later.

sylvainbx commented 8 years ago

Thanks for your hints. I finally achieved to solve my authentication issue by implementing a solution based on your second proposal.

Ron537 commented 6 years ago

Loading the middleware dynamic after fetching user's data indeed work, but the action is dispatched multiple times.

Here is my client code :

export const proxyMiddleware: Middleware = (api: MiddlewareAPI<any>) => next => (action: any) => {
    if (!middleware) {
        const store = api.getState();
        const auth = store ? store.authentication : {};

        if (auth.idToken) {
            const socket = io(SOCKET_URL, {
                query: {
                    token: auth.idToken
                }
            });

            middleware = createSocketIoMiddleware(socket, 'server/');
        } else {
            return next(action);
        }
    }
    return middleware(api)(next)(action);
};

On server side :

this.webSocket = SocketIO(this.server, { pingTimeout: 30000 });
        this.webSocket.use(async (socket: SocketIO.Socket, next: (err?: any) => void) => {
            if (socket.handshake.query && socket.handshake.query.token) {
                const { token } = socket.handshake.query;
                const user = await this.googleAuth.getUser(token);

                if (user) {
                    const sockets = this.app.get('sockets') || {};
                    socket.id = user._id;
                    if (!sockets[user._id]) {
                        sockets[user._id] = socket;
                        this.app.set('sockets', sockets);
                    }
                    return next();
                }
            }

            return next(new Error('Authentication faild'));
        });

Then on the server I emit an action to a single client but on the client the action is dispatched multiple times.

If I directly subscribe to the same event by socket.on(eventName) , it fires only once...

Any directions how to fix it?

bodo22 commented 6 years ago

Has anyone acheived a working version of this? I am trying to create socket connection at runtime, dynamically, and have tried the following implementation:

let dynamicMiddleware = null;

const setupDynamicMiddleware = store => next => action => {
  if (dynamicMiddleware !== null) {
    console.log(dynamicMiddleware);
    return dynamicMiddleware(store)(next)(action);
  }
  return next(action);
}

const unsetMiddleware = () => {
  dynamicMiddleware = null;
}

const setMiddleware = middleware => {
  dynamicMiddleware = middleware;
}

export default setupDynamicMiddleware;
export {
  setMiddleware,
  unsetMiddleware,
}

and connecting it the the store via

  // let rootMiddleware = applyMiddleware(...otherMiddlewares, setupDynamicMiddleware);

but it seems like that for every redux action, another listener is added to the middleware, causing my browser to crash. I am certain that the middleware is created & set only once via the setMiddleware function.

itaylor commented 6 years ago

Yeah, I think you'd want to only run the (store) part of the middleware call chain only once. Maybe something like this?

let dynamicMiddleware = null;
let initedDynamicMiddleware = null;

const setupDynamicMiddleware = store => next => action => {
  if (dynamicMiddleware && !initedDynamicMiddleware) {
    initedDynamicMiddleware = dynamicMiddleware(store);
  }
  if (initedDynamicMiddleware) {
    return initedDynamicMiddleware(next)(action);
  }
  return next(action);
}

const unsetMiddleware = () => {
  dynamicMiddleware = null;
  initedDynamicMiddleware = null;
}

const setMiddleware = middleware => {
  dynamicMiddleware = middleware;
  initedDynamicMiddleware = null;
}

export default setupDynamicMiddleware;
export {
  setMiddleware,
  unsetMiddleware,
}
ALWISHARIFF commented 3 years ago
import thunk from "redux-thunk";
import createSocketIoMiddleware from "redux-socket.io";
import io from "socket.io-client";
import { setMiddleware, unsetMiddleware } from "./socketio";
import setupDynamicMiddleware from "./socketio";
import { composeWithDevTools } from "redux-devtools-extension";
import { userLoginReducer } from "./reducers/userReducers";

const userInfoFromStorage = localStorage.getItem("userInfo")
  ? JSON.parse(localStorage.getItem("userInfo"))
  : null;
const reducer = combineReducers({
  userLogin: userLoginReducer,
});

const initialState = {
  userLogin: { userInfo: userInfoFromStorage },
};
const middleware = [thunk, setupDynamicMiddleware];
let store = createStore(
  reducer,

  initialState,
  composeWithDevTools(applyMiddleware(...middleware))
);
  let token = store.getState().userLogin.userInfo.token;

if (token) {

  setMiddleware(
    createSocketIoMiddleware(
      io("http://localhost:3000", { query: `token=${token}` }),
      "server/"
    )
  );
} else {
  unsetMiddleware();
}

export default store;

I am trying to use the functions setMiddleware and unsetmiddleware but Cannot read property 'token' of null has anyone implemented a solution to this>