Outline
Our data will be stored in MongoDB. MongoDB is a NoSQL database which stores data in collections and documents (as opposed to tables and rows in SQL). Since MongoDB is non-relational, we don't have certain queries that we would have in a SQL database like the JOIN
command, and documents in a collection don't need to have the same schema. We will be using the mongoose.js library for all of our interactions with MongoDB. Mongoose allows us to define schemas and indices for our database, as well as providing callbacks and validations for ensuring our data remains consistent. Mongoose also has a plugin system for reusing code between schemas or importing community libraries to extend the functionality of our models.
We'll be building out each feature in our application in three phases:
- Create the mongoose Models
- Create any helper methods on our models and route middleware required for the feature
- Creating the route to expose the functionality to our users
Creating the User Schema
Mongoose models allow us to access data from MongoDB in an object-oriented fashion. The first step to creating a model is defining the schema for it. Then, we'll need to register the model with Mongoose so that we can use it throughout our application.
Create the following in models/User.js
:
var mongoose = require('mongoose');
var UserSchema = new mongoose.Schema({
username: String,
email: String,
bio: String,
image: String,
hash: String,
salt: String
}, {timestamps: true});
mongoose.model('User', UserSchema);
The {timestamps: true}
option creates a createdAt
and updatedAt
field on our models that contain timestamps which will get automatically updated when our model changes. The last line mongoose.model('User', UserSchema);
registers our schema with mongoose. Our user model can then be accessed anywhere in our application by calling mongoose.model('User')
.
Now that we have all of our fields for our user created let's add some validations to our model. Validations are checks that get run before our model gets saved to ensure we don't commit any dirty data to our database. You can read more about validations and the built-in validations that come with Mongoose here
In models/User.js
, add the following:
var mongoose = require('mongoose');
var UserSchema = new mongoose.Schema({
- username: String,
+ username: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
- email: String,
+ email: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
bio: String,
image: String,
hash: String,
salt: String
}, {timestamps: true});
mongoose.model('User', UserSchema);
We also added the index: true
options to username
and email
to optimize queries that use these fields.
We need our usernames and emails to be unique between users so that users can't sign up with the same information. Mongoose doesn't have a built-in validation for unique fields, but fortunately, we can use the mongoose-unique-validator
plugin to get this functionality.
We'll need to require
the mongoose-unique-validator
library, then, we can use the unique
validator on our username
and email
fields. Finally, we need to register the plugin with our model to enable the unique validator. We're configuring the validator's message to say that the field "is already taken."
var mongoose = require('mongoose');
+var uniqueValidator = require('mongoose-unique-validator');
var UserSchema = new mongoose.Schema({
- username: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
+ username: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
- email: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
+ email: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
bio: String,
image: String,
hash: String,
salt: String
}, {timestamps: true});
+ UserSchema.plugin(uniqueValidator, {message: 'is already taken.'});
mongoose.model('User', UserSchema);
Creating Methods on the User Model
For user authentication to work, we'll need to create some helper methods on our users to set and validate passwords, as well as generate JWT's. To generate and validate hashes, we'll use the pbkdf2 algorithm from the crypto
library that comes with Node.
crypto
library
In models/User.js
, add:
var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
+var crypto = require('crypto');
Next, let's create the method to hash passwords. We'll be generating a random salt for each user. Then we can use crypto.crypto.pbkdf2Sync()
to generate hashes using the salt. pbkdf2Sync()
takes five parameters: The password to hash, the salt, the iteration (number of times to hash the password), the length (how long the hash should be), and the algorithm.
In models/User.js
, add the following before the model gets registered:
+UserSchema.methods.setPassword = function(password){
+ this.salt = crypto.randomBytes(16).toString('hex');
+ this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
+};
mongoose.model('User', UserSchema);
To see if a password is valid for a particular user, we need to run the pbkdf2
with the same number of iterations and key length as our setPassword
function with the salt of the user; then we need to check to see if the resulting hash matches the one that's stored in the database.
+UserSchema.methods.validPassword = function(password) {
+ var hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
+ return this.hash === hash;
+};
mongoose.model('User', UserSchema);
Next, we'll need a method on our model for generating a JWT (JSON Web Token). JWT's are the tokens that will be passed to the front-end that will be used for authentication. The JWT contains a payload (assertions) that is signed by the back-end, so the payload can be read by both the front-end and back-end, but can only be validated by the back-end.
jsonwebtoken
and the application secret in the user model.
In models/User.js
add the following:
var crypto = require('crypto');
+var jwt = require('jsonwebtoken');
+var secret = require('../config').secret;
We need a secret to sign and validate JWT's. This secret should be a random string that is remembered for your application; it's essentially the password to your JWT's. In config/index.js
there's a secret
value which is set to "secret" in development and reads from an environment variable in production.
Now, we have everything that's needed to generate a JWT for a user. For the token's payload, we'll be including three fields:
id
which is the database id of the userusername
which is the username of the userexp
which is a UNIX timestamp in seconds that determines when the token will expire. We'll be setting the token expiration to 60 days in the future.
+UserSchema.methods.generateJWT = function() {
+ var today = new Date();
+ var exp = new Date(today);
+ exp.setDate(today.getDate() + 60);
+
+ return jwt.sign({
+ id: this._id,
+ username: this.username,
+ exp: parseInt(exp.getTime() / 1000),
+ }, secret);
+};
mongoose.model('User', UserSchema);
Lastly, we'll need a method on the user model to get the JSON representation of the user that will be passed to the front-end during authentication. This JSON format should only be returned to that specific user since it contains sensitive information like the JWT.
+UserSchema.methods.toAuthJSON = function(){
+ return {
+ username: this.username,
+ email: this.email,
+ token: this.generateJWT(),
+ bio: this.bio,
+ image: this.image
+ };
+};
mongoose.model('User', UserSchema);
Now our user model is ready to be used for authentication! But before we can use it, we need to require the file so we can use it throughout our application.
In app.js
add:
+ require('./models/User');
app.use(require('./routes'));
Be sure to include models before routes so that our routes will be able to use our models.