Skip to content
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

"No auth token" error on service calls #892

Closed
EliSadaka opened this issue Jun 22, 2018 · 30 comments
Closed

"No auth token" error on service calls #892

EliSadaka opened this issue Jun 22, 2018 · 30 comments

Comments

@EliSadaka
Copy link

Steps to reproduce

I'm experiencing an issue where frequently when performing a service call an authenticated user will seemingly lose authentication for a second, causing the service call to fail. After that it looks like they are re-authenticated, but that one service call is lost. The server logs show a "No auth token" error on the before hook of whatever service was called when this occurs. At first I thought the problem was caused by calling other services within a hook and not passing the authentication params to them, but looking at the logs the server produces this error happens even when the only hook on the called service is authenticate('jwt').

I'm not sure what the relevant code that could be causing this issue is. Here is what my authentication.js looks like:

const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const local = require('@feathersjs/authentication-local');
const PasswordValidator = require('password-validator');

module.exports = function(app) {
  const config = app.get('authentication');

  // Setup password validator if a policy is defined
  if (config.passwordPolicy) {
    let validator
    app.getPasswordPolicy = function () {
      // Create on first access, should not be done outside a function because the app has not yet been correctly initialized
      if (validator) return validator
      let { minLength, maxLength, noSpaces } = config.passwordPolicy

      validator = new PasswordValidator()
      if (minLength) validator.is().min(minLength)
      if (maxLength) validator.is().max(maxLength)
      if (noSpaces) validator.not().spaces()
      
      return validator
    }
  }

  // Set up authentication with the secret
  app.configure(authentication(config));
  app.configure(jwt());
  app.configure(local());

  // The `authentication` service is used to create a JWT.
  // The before `create` hook registers strategies that can be used
  // to create a new valid JWT (e.g. local or oauth2)
  app.service('authentication').hooks({
    before: {
      create: [
        authentication.hooks.authenticate(config.strategies),
      ],
      remove: [authentication.hooks.authenticate('jwt')],
    },
    after: {
      create: [
        context => {
          //send data with auth response
          context.result.userId = context.params.user._id;
        }
      ]
    }
  });
};

Here is my channels.js:

const logger = require('winston');

module.exports = function(app) {
  if (typeof app.channel !== 'function') {
    // If no real-time functionality has been configured just return
    return;
  }

  app.on('connection', connection => {
    // On a new real-time connection, add it to the anonymous channel
    app.channel('anonymous').join(connection);
  });

  app.on('logout', connection => {
    //When logging out, leave all channels before joining anonymous channel
    app.channel(app.channels).leave(connection);
    app.channel('anonymous').join(connection);
    logger.log('info', 'logout');
  });

  app.on('login', (authResult, { connection }) => {
    // connection can be undefined if there is no
    // real-time connection, e.g. when logging in via REST
    if (connection) {
      // Obtain the logged in user from the connection
      const { user } = connection;

      // The connection is no longer anonymous, remove it
      app.channel('anonymous').leave(connection);

      // Add it to the authenticated user channel
      app.channel('authenticated').join(connection);
      logger.log('info', 'login');

      // User joins their own private channel
      //logger.log('info', typeof user._id === 'string')
      app.channel(`user/${user._id.toString()}`).join(connection);

      //if the user is a member of any chatrooms, join their channel
      app
        .service('rooms')
        .find({
          query: {
            $limit: 25,
            rmembers: {
              $in: [user._id]
            }
          },
          user: user
        })
        .then(roomArray => {
          roomArray.data.forEach(room => {
            app.channel(`rooms/${room._id.toString()}`).join(connection);
          });
        });
    }
  });

  // When a user is removed, make all their connections leave every channel
  app.service('users').on('removed', user => {
    app.channel(app.channels).leave(connection => {
      return user._id === connection.user._id;
    });
  });

  // Publish user create events to all authenticated users
  app.service('users').publish('created', (data, context) => {
    return app.channel('authenticated');
  });

  // Publish user updated events to that user
  app.service('users').publish('updated', (data, context) => {
    return app.channel(`user/${data._id}`);
  });

  // Publish user patched events to that user
  app.service('users').publish('patched', (data, context) => {
    return app.channel(`user/${data._id}`);
  });

  // Publish all messages and rooms service events only to its room channel
  app.service('messages').publish((data, context) => {
    //logger.log('info', 'message sent');
    return app.channel(`rooms/${data.roomId}`);
  });

  app.service('rooms').publish((result, context) => {
    return app.channel(`rooms/${result._id}`);
  });

  //Publish challenge completion and power rewards to the required user
  app.service('user-challenges').publish((result, context) => {
    return app.channel(`user/${result.userId}`);
  });

  app.service('user-powers').publish((result, context) => {
    return app.channel(`user/${result.userId}`);
  });

  // Publish room-activity create events only to its room channel
  app.service('room-activity').publish('created', (data, context) => {
    return app.channel(`rooms/${data.roomId}`);
  });

  // Publish notifications to their recipients
  app.service('notifications').publish('created', (data, context) => {
    return context.data.map(user => {
      return app.channel(`user/${user}`);
    })
  });
};

Here is what I have on the client side in regards to authentication:

this.socket = io(...); //set to ip address of server
	this.app = feathers()
		.configure(socketio(this.socket, { timeout: 10000 }))
		.configure(
			authentication({
				storage: AsyncStorage
			})
		);

...

login(username, password) {
	this.app
		.authenticate({
			strategy: "local",
			uname: username,
			pword: password
		})
		.then(
			action("authSuccess", user => {
				// User is authenticated
				this.isAuthenticated = true;
				this.userId = user.userId;
			})
		)
		.catch(error => alert(error.message));
}

If there's any other code you think may be causing the problem and you'd like to look at just let me know.

Expected behavior

The service call should complete successfully.

Actual behavior

The service call fails and returns a "No auth token" error.

System configuration

Module versions (especially the part that's not working):
Server:
@feathersjs/authentication: 2.1.5
@feathersjs/authentication-jwt: 2.0.1
@feathersjs/authentication-local: 1.2.1
@feathersjs/configuration: 1.0.2
@feathersjs/errors: 3.3.0
@feathersjs/express: 1.2.2
@feathersjs/feathers: 3.1.5
@feathersjs/socketio: 3.2.1
Client:
@feathersjs/authentication-client: 1.0.2
@feathersjs/client: 3.4.5
@feathersjs/feathers: 3.1.5
@feathersjs/socketio-client: 1.1.0

NodeJS version: 9.11.1

React Native Version: 0.55.4

@daffl
Copy link
Member

daffl commented Jun 28, 2018

There are several other issues about authentication on socket reconnection. I will probably tackle all of them for the next version I am currently working on. Directly related is probably #854

@pribeh
Copy link

pribeh commented Jun 28, 2018

@daffl Thanks for the response. Is there any way you can think of to mitigate this or work around it? For some users of our (react native) app, the no-auth token happens far more frequently than others. On certain devices, users can't signup/login or post content because of how consistently it's happening.

@daffl
Copy link
Member

daffl commented Jun 28, 2018

Are you using the recommended Socket configuration for React native?

const socket = io('http://api.my-feathers-server.com', {
  transports: ['websocket'],
  forceNew: true
});

@pribeh
Copy link

pribeh commented Jun 28, 2018

@daffl I'm going to confirm with Eli but I believe the following is how we've setup our socket configuration (in our mainstore doc).

constructor() {
	this.socket = io("http://**.**.***.***3030"); 

	this.app = feathers()
		.configure(socketio(this.socket, { timeout: 10000 }))
		.configure(
			authentication({
				storage: AsyncStorage
			})
		);
}

Our mainstore (which contains our service calls) is quite a big file at the moment, if you'd like I can share that with you over PM.

@pribeh
Copy link

pribeh commented Jun 28, 2018

We've deployed an update with the correction. We believe this may resolve the issue for us. We'll confirm as soon as our users do.

@daffl
Copy link
Member

daffl commented Jun 29, 2018

Unauthenticated requests on reconnection are probably still an issue but it should happen very rarely. I don't think we ever noticed it as a major issue in our React Native applications (as long as the websocket transport is enforced).

@daffl
Copy link
Member

daffl commented Jul 3, 2018

Closing since it appears to be solved.

@daffl daffl closed this as completed Jul 3, 2018
@pribeh
Copy link

pribeh commented Jul 3, 2018

It is. Thanks @daffl

@EliSadaka
Copy link
Author

It seems like we're still experiencing this issue even after implementing the recommended Socket configuration that @daffl posted above. For some of our users it happens quite often. Is there something else that could be causing it? Any help would be appreciated. I'd be willing to share more of our code if that would be helpful.

@pribeh
Copy link

pribeh commented Aug 21, 2018

@daffl if you would like access to our git repo just let us know and we can share it with you.

@daffl daffl reopened this Aug 21, 2018
@pribeh
Copy link

pribeh commented Aug 24, 2018

Hi @daffl, we shared our repo with you just in case.

@pribeh
Copy link

pribeh commented Aug 27, 2018

I will do my best to provide some more information that may help to reproduce, test and solve the problem.

As more users are onboarding, we're noticing Android users are impacted in particular but this does occur for iOS users as well. One of the troubles with isolating the root cause is that it is intermittent and appears to increase in frequency with bad network connections or when switching networks (wifi/data). Here are the different instances in which timeouts appear:

  • Attempting service calls to S3 which require authentication and are also accompanied with patch updates to mongoose. This is happening frequently and appears to impact Android users mostly.
  • Switching from wifi to data (users often get timeouts or are logged out). Certain users describe being unable to login on certain wifi/data networks at all or only being able to infrequently but fine on others. This happens across both client types.
  • Not sure if this is impacted by network switching or congestion but after a few minutes or more of using the client, connections to feathers from the client seem to drop and timeouts occur. This happens on either device type.

We're not entirely sure if the either instance shares the same root cause but it is our suspicion (after getting a lot more feedback from users) that the issues described before simply did not go away after implementing "transports: ['websocket'], forceNew: true" but simply decreased in frequency on our own test devices at the time. My best hypothesis is that we still require some other form of failsafe to reconnect/authenticate the client with feathers. It's as if the service call isn't happening in the right order (even though it seems like it should). I have experience working with websockets before but not to the extent of using it to handle every single request from client to server for multiple types of services so I feel under-qualified to judge.

Network type and integrity is impacting our ability to reproduce the issue so we discovered that we had to turn on network congestion emulation to even reproduce the issue at times (given how good our connection is at work).

We have been able to reproduce this with another simpler setup than our own.

@8BallBomBom
Copy link
Contributor

Been experiencing this issue a lot recently.
It would seem that sometimes when the socket reconnects, the backend doesn't reauth the client automatically like it usually would and the authenticated event also doesn't fire on the client.
So what i currently have in place is a lot of checking when the socket gets reconnected to then run the auth functions before attempting to do anything else with the backend.
But of-course that sometimes slips up as some commands that were trying to go through as the connection issue occurred tend to re-attempt once the connections back which then results in this error.

@daffl
Copy link
Member

daffl commented Aug 27, 2018

This issues is probably the same as https://github.com/feathersjs/authentication/issues/577

I think I mentioned somewhere else, that this issue is probably caused somewhere in https://github.com/feathersjs/authentication/blob/master/lib/socket/update-entity.js#L3. The socket disconnects, deletes all properties but does not re-authenticates on reconnection. Since the original authors are unfortunately unavailable to debug this more, the only way I see to move this forward is the fix in the next auth version I am working on in this branch.

@pribeh
Copy link

pribeh commented Aug 27, 2018

@daffl Is it ready for testing? We'd be happy to test it.

@homerjam
Copy link

FWIW I have resolved this in my app by manually (re-)authenticating on the client side on load

@pribeh
Copy link

pribeh commented Aug 29, 2018

@homerjam @8BallBomBom would either of you be able to share examples of what you're doing to re-connect and/or re-authenticate?

@homerjam
Copy link

@pribeh https://gist.github.com/homerjam/4bdd2afa4af5f712fcc7b4dc2391583e#file-plugins-auth-js-L32-L46

I'm basically grabbing the cookie and calling authenticate - the example here uses feathers-vuex which just wraps @feathers/client

@8BallBomBom
Copy link
Contributor

@pribeh Don't really have much to show.
Pretty much just re-calling the authenticate function whenever socketio reconnects.
Don't have anything decent in place to prevent api calls/queue them up to execute after auth.
But i do at least have some simple checks in place to prevent some stuff going through when not authed.
The issue is though randomly there can be timing issues and some requests can get through the gaps which then causes this error.

@EliSadaka
Copy link
Author

Our app has a functionality that allows users to tap on a chat notification while the app is in the background to immediately open up that chatroom, but there is an issue where there is frequently a timeout error whenever the app tries to load the chat messages when doing this. I tested out the following code to re-authenticate whenever our app returns from the background, and while it authenticates successfully the service calls to load chat messages still fail.

AppState.addEventListener('change', async (state) => {
	if (state === 'active') {
		this.app.authenticate()
	}
})

My theory is that when loading the chat messages, the server performs multiple nested service calls that require authentication and the authentication payload isn't properly being passed to one of them which causes the service call to fail. What doesn't make sense though is that this rarely happens normally, but it almost always happens when the app returns to the foreground from tapping on a chat notification.

I'm guessing that the app disconnects from the server after it's been in the background long enough, and when returning from the background the service call to fetch the chat messages triggers before the re-authentication can complete, causing the service call to fail. However, we still occasionally get these no auth token errors when users are actively inside the app and have already been authenticated. I'm really not sure where the root of the problem is.

@homerjam
Copy link

homerjam commented Sep 3, 2018

@EliSadaka do you need an await in there to delay loading of the chat messages?

@claustres
Copy link
Contributor

Well I am not sure if this is relevant and will help but here are some insights from our experience since we also experimented this kind of socket re-connection issue, especially when running on mobiles (eg with app in background or unstable network connection).

Due to async operations the client logic can be really complicated in SPA. Let's say your app requires user data (thus authentication) and you have a couple of pages where you gather data from a couple of services. First you must ensure that when performing a reload from any page everything goes always as expected (you often have random behaviors with async ops). Otherwise you have an async issue in your own business logic.

Once you are ok with this, you must then ensure that upon socket re-connection you don't need to re-run this business logic, notably with real-time apps. Indeed I doubt that simply reconnect the socket on the backend side under-the-hood will cover all use cases: during the connection failure a lot of things could have happened on the backend leading to inconsistent state of the client. You at least need to send an event to the client saying "hey we have reconnected" so that the business logic can be run again to update the client state whenever required, which is almost equivalent to a "thin page reload". In hybrid mobile apps things can be worse because you don't really control what the system does under the hood, it might perform a full page reload or whatever.

In our production app we ended in performing a complete reload on re-connection https://github.com/kalisio/kApp/blob/master/src/components/Index.vue#L74 to make things easy (but we ensured that reload work as expected first). I am not saying it is the best thing to do but it improved our user experience.

@8BallBomBom
Copy link
Contributor

@EliSadaka I'm doing something similar but what i found was calling disconnect on the socket directly every time the app went into the background was the best solution.
Otherwise when the app came back to the foreground after long periods everything would get locked up complaining about timeouts before attempting to re-connect which would then cause more auth errors as some service calls might go through during that period.
Even with the manual forced disconnection and re-connection sometimes theres gaps for this error/failure in-between.

@claustres
Copy link
Contributor

@EliSadaka I had a quick look to your repo and I am not sure your are doing async ops right in your store, don't mix await and promises this might not work as you expect: https://hackernoon.com/a-common-misconception-about-async-await-in-javascript-33de224bd5f.

@claustres
Copy link
Contributor

By the way which Feathers version are you using ? Indeed, this might be related #979.

@EliSadaka
Copy link
Author

On our server we have the following packages:

@feathersjs/feathers: 3.1.7
@feathersjs/socketio: 3.2.2

On our client we have the following packages:

@feathersjs/feathers: 3.1.7
@feathersjs/socketio-client: 1.1.0

@oscarlomas09
Copy link

Has there been any development on this? I am using Nuxt+FeathersJS and I have been running into the same issue of 'No Auth Token'
I have setup the client with the recommendations stated above

`
import feathers from '@feathersjs/feathers';
import socketio from '@feathersjs/socketio-client';
import auth from '@feathersjs/authentication-client';
import io from 'socket.io-client';

export const host = process.env.API_HOST || 'http://localhost:3030';

export default (origin, storage) => {
const socket = io(host, {
transports: ['websocket'],
forceNew: true,
extraHeaders: {
origin: origin || ''
}
});

const feathersClient = feathers()
.configure(socketio(socket))
.configure(
auth({
storage: storage,
})
);

return feathersClient;
};
`

@wong813
Copy link

wong813 commented Apr 9, 2019

I traped for this error for whole day, and finally find the solution

npm install cookie-parser - - save

var cookieParser = require('cookie-parser')

app.use(cookieParser())

@daffl
Copy link
Member

daffl commented Jun 6, 2019

This should be fixed in the new authentication client of Feathers v4. See the Migration guide for more information on how to upgrade.

@daffl daffl closed this as completed Jun 6, 2019
@alimuddinhasan
Copy link

I traped for this error for whole day, and finally find the solution

npm install cookie-parser - - save

var cookieParser = require('cookie-parser')

app.use(cookieParser())

Is this still valid and you never experience the same error again?
Because I still use feathers 3 and cannot move to version 4 for now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants