Meteor custom login error: access denied

695 views Asked by At

Using [email protected], accounts-base, accounts-password, and Meteorhacks cluster.

A little background:

I'm writing a web-app that allows users to buy snacks from our cafe as a proof-of-concept for Meteor. This app is broken up into two services within the cluster: the cafe service where snacks and purchases are managed, and the user service which manages users and authentication. We desire the separation because in the future we will have other services that subscribe to the same group of users, and we don't want to manage users in multiple places.

What works

Users log into the user-service with LDAP credentials. If a Meteor user account with their name doesn't exist, a new account is created using their ldap information. These accounts are published with the default properties that Meteor allows. Users are given a default PIN number as a password to use on subscribing services (for now they can't change it).

The cafe app subscribes to the Meteor.users publication on the user-service, which then shows all active users. Since the cafe app is designed to run on a tablet we want to minimize typing, so we allow users to choose their account by touching their username. After that, they are provided a pin pad to type in their pin (password) which will be hashed and sent over the cluster to be authenticated on the user-service. The user-service then calls Accounts._checkPassword to authenticate the request, and will return an object with the userId and/or any errors. We're using a custom login method called loginWithPin which is basically the same as loginWithPassword. All of this works beautifully.

What doesn't work

The problem arises when the authentication API returns it's result. All is correct until you enter in the right password. Our custom method loginWithPin is always returning an error, but the odd one is when you type in the correct PIN. The authentication process passes on the user-service layer and passes back an object like { userId: '...' }, but the custom method throws a 403 error with Access Denied as the reason.

I was under the assumption that a custom login method returning an object with a userId would actually log in the user by that id. That, and I can't find anywhere in accounts-base or accounts-password that would throw a 403 Access Denied error.

TLDR

My custom loginWithPin method returns a [403] Access Denied error when using the correct pin, but authentication is happening on another app's API where the user's password actually lives.


Cafe app code

client/enter_pin/enter_pin.js

//...
if ( //pin fully entered ) {
  Meteor.loginWithPin(password.value, function( err ) {
    if ( Meteor.user() ) {
      // login successful, route to cafe
      Router.go('/cafe');
    } else {
      console.error('An error occurred while logging in: ', err);

      // other stuff to reset pinpad
    }
  }
}

client/login.js

Meteor.loginWithPin = function( pin, callback ) {
  var username = Session.get('selectedUser');

  check(username, String);
  // hash pin before sending it across cluster connection
  pin = Package.sha.SHA256(pin);

  // send login request
  Accounts.callLoginMethod({
    methodArguments: [{
      user: {
        username: username
      },
      pin: pin
    }],
    userCallback: callback
  });
};

server/login.js

Accounts.registerLoginHandler(function( request ) {
  // use runAsync here since login request is asynchronous and we want to pause execution until this returns
  var response = Async.runSync(function( done ) {
    ClusterConnection.call('authenticateUser', request.user.username, request.pin, function( err, res ) {
      if ( !err && !res.error ) {
        console.log('successful login');
        done(null, res);
      } else {
        console.log('unsuccessful login');
        done(null);
      }
    });
  });

  // this should be either { userId: '...' }, or null if an error is present
  return response.result;
});

Service API

server/api.js

Meteor.methods({
  'authenticateUser': function( username, pin ) {
    check(username, String);
    check(pin, String);

    var user = Meteor.users.findOne({username: username});
    var password = {digest: pin, algorithm: 'sha-256'};
    return Accounts._checkPassword(user, password);
  }
});

The error that is firing with Access Denied is the one found in client/enter_pin/enter_pin.js if Meteor.user() is undefined, which is the case because a login hasn't happened. Yet this error fires even if the authentication API successfully returns an object with a userId in it.


UPDATE

cafe app server/cluster.js (where we're connecting to the user-service)

Cluster.connect('mongodb://localhost:27017/service-discovery');
Cluster.register('cafe');

EmployeeConn = Cluster.discoverConnection('employees');
EmployeeConn.subscribe('employees');

Meteor.users = new Mongo.Collection('users', {connection: EmployeeConn});

I believe the main problem is with the last line here, in that I'm redefining the Meteor.users collection. However, I'm at a loss as to how exactly we would sync the data we get from the user-service to Meteor.users on the server without overwriting it.

1

There are 1 answers

0
webdeb On

However, I'm at a loss as to how exactly we would sync the data we get from the user-service to Meteor.users on the server without overwriting it.

Meteor is build to provide a simple SyncData workflows.

For example you could just use another separate Collection for each of your Services, and have a reference to the Meteor.users Collection..

Then you would subscribe to the Services Collections where you need it.

In your Meteor.users you would have a new field:

pinBasedServices: [
  {
   service: 'cafe',
   pin: 'XXX',
   lastLogin: '',
   generatedHashTokenFromTheServiceToGetTheDataFrom:'somehash',
   /* whatever */
  },
  {
   service: 'cheeseCakeService',
   pin: 'XYZ',
   /* So on.. */
  }
]

I hope, I could give you another direction of thinking to not have to overwrite Meteor Collections.. Just build on it.

You can also observe Meteor Collections if you want to have data syncs in the way you need it.