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.
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:
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.