I need a SMS-authentication in my Meteor app.
Let's say I have a simple form (in React-style, because I'm using React in frontend):
<form onSubmit={ this.submitPhone() }>
<input type='text' size='10' placeholder='Your phone here' />
<input type='submit' value='Send me a code'/>
</form>
User enters his phone number and submits the form. After that SMS-code is sent to the entered number. And a new form appears:
<form onSubmit={ this.submitCode() }>
<input type='text' size='5' placeholder='Enter code' />
<input type='submit' value='Sign In'/>
</form>
If user enters his code correctly, then Meteor should know that the user is logged in (with some _id, I think). If the code is not correct, then an error message is shown.
I found Twilio service and this package and it looks like it is exactly what I need. But I don't know how to use it at all.
I have tried only Meteor's default Accounts UI way of authentication a few months ago in the tutorials, but actually I don't know how to do such things, especially via SMS. I don't need such things like roles in my app, I even don't need usernames, passwords and e-mails. I just need to have a base of user _id and phone. So all I need is make user be able to sign in (first time signin is signup in this way).
Thank for your help, a detailed answer is really what I need this time.
First, you need to install one of the following packages:
http (To call the Twilio API directly from your methods) or,
accolver:twilio-meteor (You can try using the services this package provides off of the official Twilio-Node Helper Library)
Next, you should also install the okland:accounts-phone package to help enable login via phone number. Their GitHub provides easy to follow instructions on how to set it up.
Password
I would strongly recommend creating user accounts with a password, along with the phone number, since it is a good security feature to have, and is also required by default on Meteor Accounts package.
Verification Process
I will be giving an example using server-side Meteor methods, for frontend you can write your React handlers accordingly.
This example will be using the HTTP package, in your code you can modify it to include other wrapper packages like twilio-meteor if you wish.
Step 1:
Register your user and send verification SMS.
createNewUser method:
'createNewUser': function (password, phoneNumber) {
var min = 10000;
var max = 99999;
var random = Math.floor(Math.random() * (max - min + 1)) + min;
var verified = Meteor.users.find({username: phoneNumber}).fetch();
if (verified.length > 0) {
if (verified.length == 1 && verified[0].profile.isMobileVerified == 'NO') {
Meteor.users.remove({username: phoneNumber});
var user = {username: phoneNumber, password: password, profile: { randomSms: random, isMobileVerified: 'NO' }};
Meteor.call("sendSMS", random, phoneNumber);
Accounts.createUser(user);
return returnSuccess('Successfully created', phoneNumber);
} else {
return returnFaliure('Mobile number already exists', phoneNumber);
}
} else {
var user = {username: phoneNumber, password: password, profile: { randomSms: random, isMobileVerified: 'NO' }};
Meteor.call("sendSMS", random, phoneNumber);
Accounts.createUser(user);
return returnSuccess('Successfully created', phoneNumber);
}
},
sendSMS method:
'sendSMS': function (code, mobile) {
console.log(mobile);
HTTP.call(
"POST",
'https://api.twilio.com/{yyyy-dd-mm}/Accounts/' +
'{TWILIO_APPKEY}' + '/SMS/Messages.json', {
params: {
From: '+11234567890',
To: mobile,
Body: "Greetings! Your OTP is " + code
},
auth: '{TWILIO_APPKEY}' + ':' + '{TWILIO_PASSWORD}'
},
// Print error or success to console
function (error) {
if (error) {
console.log(error);
}
else {
console.log('SMS sent successfully.');
}
}
);
}
Step 2:
Ask user for verification code and check code input by user
verifySMS method:
'verifySMS': function (code, userid) {
console.log(userid);
var sms = Meteor.users.findOne({username: userid}).profile.randomSms;
if (sms == code) {
Meteor.users.update({username: userid}, {
$set: {"profile.isMobileVerified": "YES", "profile.randomSms": "--"}
});
return returnSuccess("Yes");
} else {
return returnSuccess("No");
}
},
Step 3:
From your React code handling, if code matches, approve the user, else display appropriate error message.
UPDATE to handle specific use case by OP:
(Example indicative of React code)
To have the user authenticated via SMS OTP code everytime before login, you will need to use the sendSMS method every time the user tries to login, update it in a collection of stored AuthCodes, verify the code each time, and handle case accordingly.
React Form:
You will need to render a form something like this inside your react JSX code container.
<form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
<input
type="text"
ref="phoneNumberInput"
placeholder="Enter Phone Number"
/>
</form>
Write React function to login user:
handleSubmit() {
event.preventDefault();
// Find the phone number field via the React ref
const phoneNumber = ReactDOM.findDOMNode(this.refs.phoneNumberInput).value.trim();
Meteor.call('sendAuthCode', Meteor.userId(), phoneNumber, function(error, result) {
// Show a popup to user that code has been sent
});
}
Then, similar as above, create another form to have the user input the code sent to them, and send that to server for verification, e.g.
handleAuthCheck() {
event.preventDefault();
// Find the phone number field via the React ref
const phoneNumber = ReactDOM.findDOMNode(this.refs.phoneNumberInput).value.trim();
const code = ReactDOM.findDOMNode(this.refs.codeInput).value.trim();
Meteor.call('verifyAuthCode', Meteor.userId(), phoneNumber, code, function(error, result) {
// handle result accordingly
// you need to decide how you are going to login user
// you can create a custom module for that if you need to
});
}
AuthCodes Collection:
You will need to define a collection in a file and export it, so that it can be imported where needed.
export const AuthCodes = new Mongo.Collection('authcodes');
Meteor server methods:
Send SMS:
'sendAuthCode': function(userId, phoneNumber) {
var min = 10000;
var max = 99999;
var code = Math.floor(Math.random() * (max - min + 1)) + min;
Meteor.call("sendSMS", code, phoneNumber);
AuthCodes.insert({
userId: userId,
phoneNumber: phoneNumber,
code: code
});
}
Verify Code:
'verifyAuthCode': function(userId, phoneNumber, code) {
var authCode = AuthCodes.findOne({ phoneNumber: phoneNumber, code: code }) // You can also add userId check for added verification
if(typeof authCode !== "undefined" && authCode) {
// verification passed
return true;
} else {
// verification failed
return false;
}
}
Related
How do you check whether a user is logged in via third party (Google, Facebook, ...) in the Meteor framework? Also, is this possible from the client?
There are multiple ways to do it. On the Server side you would have a function like Accounts.onCreateUser((options, user) => {... }).
If you already publish minimum data of the user, you can add a key using onCreateUser and save something like: loginVia: "email" or "FB" etc. Then you publish that key or get its value with a method.
The straight forward solution is to check if the social service exists if look for a particular service.
For Example:
const isFBUser: Meteor.users.find({ _id :.... }, { 'services.facebook': { $exists: true } }).count() // results in 1 record or 0 records = true / false
of if you want to know if the user is coming via email and not third party you can check for emails
const isThirdParty = Meteor.users.find({_id: ...}, emails: { $exists: true })
It is pretty common to also use a merge accounts system so that someone coming from FB with the email gigi#gmail.com will letter be allowed to log in to you app with the email instead of the social account. In this case, you would need to eventually save the source of the last login.
I'll leave here for you part of my onCreateUser as example of how to pull data out of a 3rd party user and save it in the use profile. On the same lines you can save the 3rd party source (as suggested above)
if (user.services) {
const fb = user.services.facebook
const google = user.services.google
let avatar = null
let fbi = null // I use this to keep a record of the FB user Id
let ggli = null // // I use this to keep a record of the Google user Id
if (fb) {
/**
* I upload to S3 and I don't wait for a response. A little risky...
*/
put_from_url(`https://graph.facebook.com/${fb.id}/picture?width=500&height=500`, `avatar/${fb.id}.jpg`, (err, res) => {
if (err) {
console.log('Could not upload FB photo to S3, ', err)
} else {
// console.log(res)
}
})
user.profile = extend(user.profile, {
firstName: fb.first_name,
lastName: fb.last_name,
email: fb.email,
displayName: fb.name,
gender: startCase(toLower(fb.gender)),
avatar: `${fb.id}.jpg`
})
avatar = `${fb.id}.jpg`
fbi = fb.id
roles = ['user', 'social']
}
if (google) {
/**
* I upload to S3 and I don't wait for a response. A little risky...
*/
put_from_url(google.picture + '?sz=500', `avatar/${google.id}.jpg`, err => {
if (err) {
console.log('Could not upload Google photo to S3, ', err)
}
})
user.profile = extend(user.profile, {
firstName: google.given_name,
lastName: google.family_name,
email: google.email,
displayName: google.name,
gender: startCase(toLower(google.gender)),
avatar: `${google.id}.jpg`
})
avatar = `${google.id}.jpg`
ggli = google.id
roles = ['user', 'social']
}
/**
* Create a slug for each user. Requires a display name for all users.
*/
let slug
slug = Meteor.call('/app/create/slug', user.profile.displayName, 'user')
Also please check the user object structure:
And check this out. Users via 3rd party don't have the email field so you can check its existence.
There are some irreversible actions that user can do in my app. To add a level of security, I'd like to verify that the person performing such an action is actually the logged in user. How can I achieve it?
For users with passwords, I'd like a prompt that would ask for entering user password again. How can I later verify this password, without sending it over the wire?
Is a similar action possible for users logged via external service? If yes, how to achieve it?
I can help with the first question. As of this writing, meteor doesn't have a checkPassword method, but here's how you can do it:
On the client, I'm going to assume you have a form with an input called password and a button called check-password. The event code could look something like this:
Template.userAccount.events({
'click #check-password': function() {
var digest = Package.sha.SHA256($('#password').val());
Meteor.call('checkPassword', digest, function(err, result) {
if (result) {
console.log('the passwords match!');
}
});
}
});
Then on the server, we can implement the checkPassword method like so:
Meteor.methods({
checkPassword: function(digest) {
check(digest, String);
if (this.userId) {
var user = Meteor.user();
var password = {digest: digest, algorithm: 'sha-256'};
var result = Accounts._checkPassword(user, password);
return result.error == null;
} else {
return false;
}
}
});
For more details, please see my blog post. I will do my best to keep it up to date.
I haven't done this before, but I think you will need something like this on your server
Accounts.registerLoginHandler(function(loginRequest) {
console.log(loginRequest)
var userId = null;
var username = loginRequest.username;
// I'M NOT SURE HOW METEOR PASSWORD IS HASHED...
// SO YOU NEED TO DO A BIT MORE RESEARCH ON THAT SIDE
// BUT LET'S SAY YOU HAVE IT NOW
var password = loginRequest.password;
var user = Meteor.users.findOne({
$and: [
{username: username},
{password: password}
]
});
if(!user) {
// ERROR
} else {
// VERIFIED
}
});
then you can call this function from the client side like this:
// FETCH THE USERNAME AND PASSWORD SOMEHOW
var loginRequest = {username: username, password: password};
Accounts.callLoginMethod({
methodArguments: [loginRequest]
});
I have a project on github for different purpose, but you can get a sense of how it is structured: https://github.com/534N/apitest
Hope this helps,
I have found the best way to validate the users password is to use the Accounts.changePassword command and
pass in the same password for old and new password. https://docs.meteor.com/api/passwords.html#Accounts-changePassword
Accounts.changePassword(this.password, this.password, (error) => {
if(error) {
//The password provided was incorrect
}
})
If the password provided is wrong, you will get an error back and the users password will not be changed.
If the password is correct, the users password will be updated with the same password as is currently set.
I sent enrollment email to the user and when he enters password and other details I'm trying to reset the password but it is throwing error
uncaught error extpected to find a document to change
As you can see in the mage
I've subscribed to the user record
my code
this.route('enroll', {
path: '/enroll-account/:token',
template: 'enroll_page',
onBeforeAction: function() {
Meteor.logout();
Session.set('_resetPasswordToken', this.params.token);
s = this.subscribe('enrolledUser', this.params.token).wait();
}
}),
After I'm displaying form and on the submit event
onSubmit: function(creds) {
var options = {
_id: Meteor.users.findOne()._id,
name: creds.name
}
var token=Session.get('_resetPasswordToken');
Meteor.call('updateUser', options, function(error, result) {
if(!error) {
Accounts.resetPassword(token, creds.password, function(error) {
if (error) {
toastr.error("Sorry we could not update your password. Please try again.");
return false;
}
else{
toastr.error("Logged In");
Router.go('/');
}
});
} else {
toastr.error("Sorry we could not update your password. Please try again.");
return false;
}
});
this.resetForm();
this.done();
return false;
}
Everything is working fine but resetpassword callback is not triggering and the above error is displaying in console.
my token is get deleted from the user record and I'm able to login using login form but
From the docs
Reset the password for a user using a token received in email. Logs the user in afterwards.
I'm not able to automatically login after resetting the password,above error is throwing
What am I missing here?
this.subscribe('enrolledUser', this.params.token).wait();
here you're subscribing using resetPassword token
when you call Accounts.resetPassword method the method will reset the password and delete the token from user record.
So your subscription is lost and there are no records available in client side to modify
(That is waht the error Expected to find a document to change)
Instead on first subscription save the user Id and subscribe to the user record using Id
so the subscription will not be lost
path: '/enroll-account/:token',
template: 'enroll_page',
onBeforeAction: function() {
Meteor.logout();
Session.set('_resetPasswordToken', this.params.token);
s = this.subscribe('enrolledUser', this.params.token).wait();
},
onAfterAction:function(){
if(this.ready()){
var userid=Meteor.users.findOne()._id;
Meteor.subscribe("userRecord",userid);
}
}
Alternatively, you could do something like as follows in your publication. This worked for me (but mine was a slightly more involved query than this).
Meteor.publish('enrolledUser', function (token) {
check(token, String);
return Meteor.users.find({
$or: [{
_id: this.userId
}, {
'services.password.reset.token': token
}]
});
});
From the docs, it says
Reset the password for a user using a token received in email. Logs the user in afterwards.
So basically, you have to subscribe to the logged in user after the fact as well. A little silly, but whatever.
In my application I want to seed the database with users and send them an enrollment link to activate their account (and choose a password). I also want them to verify/change some profile data.
On the server I seed the database like this:
Meteor.startup(function () {
if(Meteor.users.find().count() === 0) {
var user_id = Accounts.createUser({ email: 'some#email.com', profile: { some: 'profile' } });
Accounts.sendEnrollmentEmail(user_id);
}
})
The enrollment link is sent as expected, but I want to create a custom template for when the url in the email is clicked. Preferably handled by iron-router. (Not using the accounts-ui package).
I tried things like redirecting the user to a custom route like this:
var doneCallback, token;
Accounts.onEnrollmentLink(function (token, done) {
doneCallback = done;
token = token;
Router.go('MemberEnroll')
});
which is not working (it changes the url but not rendering my template)
I also tried to change the enroll URL on the server like this:
Accounts.urls.enrollAccount = function (token) {
return Meteor.absoluteUrl('members/enroll/' + token);
};
But when I do this, the Accounts.onEnrollmentLink callback does not fire.
Also, changing the URL is not documented so I'm not sure its a good practice at all.
Any help is appreciated.
In my application I'm doing like this
this.route('enroll', {
path: '/enroll-account/:token',
template: 'enroll_page',
onBeforeAction: function() {
Meteor.logout();
Session.set('_resetPasswordToken', this.params.token);
this.subscribe('enrolledUser', this.params.token).wait();
},
data: function() {
if(this.ready()){
return {
enrolledUser: Meteor.users.findOne()
}
}
}
})
As enrollment url is like this
http://www.yoursite.com/enroll-account/hkhk32434kh42hjkhk43
when users click on the link they will redirect to this template and you can render your template
In my publication
Meteor.publish('enrolledUser', function(token) {
return Meteor.users.find({"services.password.reset.token": token});
});
After taking the password from the user
Accounts.resetPassword(token, creds.password,function(e,r){
if(e){
alert("Sorry we could not reset your password. Please try again.");
}else{
alert("Logged In");
Router.go('/');
}
})
enroll link
Accounts.urls.enrollAccount = function (token) {
return Meteor.absoluteUrl('enroll-account/' + token);
};
Im afraid now isnt possible, what i did is changing the html and css using "rendered" function but it has some probs with delay
Meteor.startup(function(){
Template["_enrollAccountDialog"].rendered = function(){
document.getElementById('enroll-account-password-label').innerHTML = 'Escolha sua senha';
$('.accounts-dialog').css('background-color','#f4f5f5');
$('.accounts-dialog').css('text-align','center');
$('.accounts-dialog').removeAttr('width');
document.getElementById('login-buttons-enroll-account-button').className = ' create-account-button';
document.getElementById('login-buttons-enroll-account-button').innerHTML = 'Criar conta';
}
});
Using Meteor accounts (and accounts-ui) is there an easy way to make new user sign-ups invitation only? For example by providing an invitation link or an invitation code.
The only thing related I could find in the Meteor documentation is Meteor.sendEnrollmentEmail but it doesn't solve my problem.
You can do this with the built in package, but I found it alot easier and powerful to roll a simple implementation.
You'll need to:
Create a collection, eg UserInvitations to contain the invites to become a user.
Create UI for making UserInvitations / insert some using meteor mongo
Using iron-router or similar create a route, eg:
Router.map ->
#route 'register',
path: '/register/:invitationId'
template: 'userRegistration'
data: ->
return {
invitationId: #params.invitationId
}
onBeforeAction: ->
if Meteor.userId()?
Router.go('home')
return
When the form in userRegistration is submitted - call
Accounts.createUser({invitationId: Template.instance().data.invitationId /*,.. other fields */})
On the server, make an Accounts.onCreateUser hook to pass through the invitationId from options to the user
Accounts.onCreateUser(function(options, user){
user.invitationId = options.invitationId
return user;
});
Also, on the server make an Accounts.validateNewUser hook to check the invitationId and mark the invitation as used
Accounts.validateNewUser(function(user){
check(user.invitationId, String);
// validate invitation
invitation = UserInvitations.findOne({_id: user.invitationId, used: false});
if (!invitation){
throw new Meteor.Error(403, "Please provide a valid invitation");
}
// prevent the token being re-used.
UserInvitations.update({_id: user.invitationId, used: false}, {$set: {used: true}});
return true
});
Now, only users that have a valid unused invitationId can register.
EDIT: Oct 2014 - Updated to use meteor 0.9.x API's
To do it with the built in stuff, you can plumb together the existing Accounts.sendEnrollmentEmail - however it's a little more complicated than the other solution given.
Using the example code below, call the enroll method as such:
Meteor.call('enroll', 'john.smith', 'js#harvard.edu', {name: 'John Smith'});
Meteor will then email the user a link (You can configure the template with Accounts.emailTemplates)
When they click the link, meteor calls the function passed to Accounts.onEnrollmentLink - in this case you can take them to a password setup page; but you have to mess around with their done callback.
Modify the following code, where it says INSERT XXX HERE ; then in your code call SomeGlobalEnrollmentObjectThing.cancel() if the user cancels, or SomeGlobalEnrollmentObjectThing.complete(theUsersNewPassword) if they submit the new password.
if (Meteor.isServer){
Meteor.methods({
"enroll": function(username, email, profile){
var userId;
check(username, String);
check(email, String); // Or email validator
check(profile, {
name: String
}); // your own schema
// check that the current user is privileged (using roles package)
if (!Roles.isInRole(this.userId, 'admin')){
throw new Meteor.Error(403);
}
userId = Accounts.createUser({
username: username,
email: email,
profile: profile
});
Accounts.sendEnrollmentEmail(userId);
}
});
} else {
// uses `underscore`, `reactive-var` and `tracker` packages
function Enrollment(){
this.computation = null;
this.token = new ReactiveVar(null);
this.password = new ReactiveVar(null);
this.cancelled = new ReactiveVar(false);
this.done = null;
this._bind();
}
_.extend(Enrollment.prototype, {
_bind: function(){
Accounts.onEnrollmentLink(_.bind(this.action, this));
},
reset: function(){
this.token.set(null);
this.password.set(null);
this.cancelled.set(false);
this.done = null;
if (this.computation !== null){
this.computation.stop();
this.computation = null;
}
},
cancel: function(){
this.cancelled.set(true);
},
complete: function(password){
this.password.set(password);
},
action: function(token, done){
this.reset();
this.token.set(token);
this.done = done;
this.computation = Tracker.autorun(_.bind(this._computation, this));
// --- INSERT REDIRECT LOGIC HERE [TAKE TO PASSWORD SETUP PAGE]--- //
},
_computation: function(){
var password;
if (this.cancelled.get()){
this.reset();
this.done();
// --- INSERT REDIRECT LOGIC HERE [USER CANCELLED]--- //
} else {
password = this.password.get();
if (password !== null){
Accounts.resetPassword(this.token.get(), password, _.bind(this._complete, this));
}
}
},
_complete: function(err){
// TODO - check if we were reset before callback completed
this.reset();
this.done();
if (err){
// --- INSERT REDIRECT LOGIC HERE [RESET FAILED] --- //
} else {
// --- INSERT REDIRECT LOGIC HERE [SUCCESS] --- //
}
}
});
SomeGlobalEnrollmentObjectThing = new Enrollment();
}
I have created a specific solution to this, since all the other solutions only allow you to explicitly create password-based accounts. The t3db0t:accounts-invite package allows account creation with any service only when you allow them, such as with an 'accept invitation' route. Live demo here.