Passport Local Mongoose Argon
Passport Local Mongoose Argon is a Mongoose plugin,
a fork of the original Passport-Local-Mongoose that simplifies building username and password logins with Passport.
This fork uses Argon2 in this Node.js binding for extra security when hashing and storing passwords.
This is a work in progress!
Acknowledgements
This module is very heavily based on the work of Christoph Walcher for the original Passport Local Mongoose plugin and Ranieri Althoff for the Argon2 Node binding. All credit goes to them and their collaborators.
BEFORE Installation
The Argon2 implementation also requires a global instalation of node-gyp
and GCC
.
The easiest way to do this on Windows is to npm install --global --production windows-build-tools
and then npm install -g node-gyp
;
On MacOS and Unix you'll need to follow these instructions.
Installation
AFTER you've installed the dependencies listed above you can go ahead and simply npm install --save passport-local-mongoose-argon
and then require it in your app.
Compatibility
This package was tested against projects built with passport-local-mongoose @ 4.x.x. It should work as a drop-in replacement in almost any project on a recent version.
It WILL NOT work when updating from older versions due to breaking changes.
It WILL NOT work with hashes from a non-Argon2 algorithm. It is NOT backward compatible with the PBKDF2 versions.
This SHOULD NOT be used in production until further testing. If you find bugs, especially security-related, raise an issue on this repo.
Tutorials
Michael Herman gives a comprehensible walk through for setting up mongoose,
passport, passport-local and passport-local-mongoose for user authentication in his blog post User Authentication With Passport.js
For more details on the original passport-local-mongoose you can check out its repo. Most of its excellent functionality has been preserved and some of the examples are reproduced below.
Usage
Plugin Passport Local Mongoose Argon
First you need to plugin Passport Local Mongoose into your User schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const passportLocalMongooseArgon = require('passport-local-mongoose-argon');
const User = new Schema({});
User.plugin(passportLocalMongooseArgon);
module.exports = mongoose.model('User', User);
You're still free to define your User how you like. Passport Local Mongoose Argon will add a username and hash field to store the username and the hashed password.
Additionally Passport Local Mongoose Argon adds some methods to your Schema. See the API Documentation section for more details.
Configure Passport/Passport-Local
You should configure Passport/Passport-Local as described in the Passport Guide.
Passport Local Mongoose Argon supports this setup by implementing a LocalStrategy
and serializeUser/deserializeUser functions.
To setup Passport Local Mongoose Argon use this code
const User = require('./models/user');
passport.use(User.createStrategy());
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
Options
When plugging in Passport Local Mongoose Argon additional options can be provided to configure
the hashing algorithm as well as the plugin itself.
User.plugin(passportLocalMongoose, options);
Argon options map directly to the Argon2 hashing options, and can even be changed in production. Older Argon2 hashes will still work. However, the defaults listed below should be good enough for all cases.
Argon Options
- timeCost: specifies the number of passes over memory; hashing time goes up linearly with this option
- memoryCost: specifies the amount of memory each call can afford; expressed in 2^(X) kibibytes. Ex: memoryCost: 12 is equal to 4,096 KiB/ call
- parallelism: specifies the number of threads available to each call.
- type: 0, 1, 2 for Argon2d, Argon2i and Argon2id respectively
- hashLength: specifies the hashLength in bytes
Argon Defaults are set in accordance with the Argon2 RFC and are in theory safe for every use case. You don't need to change them unless you're going for something very specific. If you find hashing takes too long even with timeCost set to 1, gradually lower memoryCost and parallelism.
Argon Defaults
- timeCost: 1,
- memoryCost: 12,
- parallelism: 8,
- type: 2 // meaning Argon2id
- hashLength: 32 // meaning 256 bits
On top of the Argon options you also have a large number of configuration options kept from the original plugin, to enhance your db models and authentication security.
Plugin Options
- interval: specifies the interval in milliseconds between login attempts. Default: 100
- usernameField: specifies the field name that holds the username. Defaults to 'username'. This option can be used if you want to use a different
field to hold the username for example "email".
- usernameUnique : specifies if the username field should be enforced to be unique by a mongodb index or not. Defaults to true.
- hashField: specifies the field name that holds the password hash value. Defaults to 'hash'.
- attemptsField: specifies the field name that holds the number of login failures since the last successful login. Defaults to 'attempts'.
- lastLoginField: specifies the field name that holds the timestamp of the last login attempt. Defaults to 'last'.
- selectFields: specifies the fields of the model to be selected from mongodb (and stored in the session). Defaults to 'undefined' so that all fields of the model are selected.
- usernameLowerCase: convert username field value to lower case when saving an querying. Defaults to 'false'.
- populateFields: specifies fields to populate in findByUsername function. Defaults to 'undefined'.
- limitAttempts: specifies whether login attempts should be limited and login failures should be penalized. Default: false.
- maxAttempts: specifies the maximum number of failed attempts allowed before preventing login. Default: Infinity.
- passwordValidator: specifies your custom validation function for the password in the form 'function(password,cb)'. Default: validates non-empty passwords.
- usernameQueryFields: specifies alternative fields of the model for identifying a user (e.g. email).
- findByUsername: Specifies a query function that is executed with query parameters to restrict the query with extra query parameters. For example query only users with field "active" set to
true
. Default: function(model, queryParameters) { return model.findOne(queryParameters); }
. See the examples section for a use case.
Where's the salt?
The Argon2 Node binding generates a 128-bit salt using crypto.randomBytes for each password in compliance with the RFC. The salt, and the hashing options get stored directly in the hash. This lets you change current hashing options as time goes on while still being able to verify previously stored hashes with different options.
If you want even more security you can add your own pepper manually to the user's password before calling the register / change password methods.
Error Messages
Override default error messages by setting options.errorMessages.
- MissingPasswordError 'No password was given'
- AttemptTooSoonError 'Account is currently locked. Try again later'
- TooManyAttemptsError 'Account locked due to too many failed login attempts'
- IncorrectPasswordError 'Password or username are incorrect'
- IncorrectUsernameError 'Password or username are incorrect'
- MissingUsernameError 'No username was given'
- UserExistsError 'A user with the given username is already registered'
Examples
For a complete example implementing a registration, login and logout see the
login example.
API Documentation
Instance methods
setPassword(password, cb)
asynchronous method to set a user's password hash
changePassword(oldPassword, newPassword, cb)
asynchronous method to change a user's password hash. If oldPassword does
not match the user's old password an IncorrectPasswordError
is passed to cb.
authenticate(password, cb)
asynchronous method to authenticate a user instance
resetAttempts(cb)
asynchronous method to reset a user's number of failed password attempts (only defined if options.limitAttempts
is true)
callback arguments
- err
- null unless the hasing algorithm throws an error
- thisModel
- the model getting authenticated if authentication was successful otherwise false
- passwordErr
- an instance of
AuthenticationError
describing the reason the password failed, else undefined.
Using setPassword()
will only update the document's password fields, but will not save the document.
To commit the changed document, remember to use Mongoose's document.save()
after using setPassword()
.
Error Handling
IncorrectPasswordError
: specifies the error message returned when the password is incorrect. Defaults to 'Incorrect password'.IncorrectUsernameError
: specifies the error message returned when the username is incorrect. Defaults to 'Incorrect username'.MissingUsernameError
: specifies the error message returned when the username has not been set during registration. Defaults to 'Field %s is not set'.MissingPasswordError
: specifies the error message returned when the password has not been set during registration. Defaults to 'Password argument not set!'.UserExistsError
: specifies the error message returned when the user already exists during registration. Defaults to 'User already exists with name %s'.AttemptTooSoonError
: Occurs if the option limitAttempts
is set to true and a login attept occures while the user is still penalized.TooManyAttemptsError
: Returned when the user's account is locked due to too many failed login attempts.
All those errors inherit from AuthenticationError
, if you need a more general error class for checking.
Static methods
Static methods are exposed on the model constructor. For example to use createStrategy function use
const User = require('./models/user');
User.createStrategy();
- authenticate() Generates a function that is used in Passport's LocalStrategy
- serializeUser() Generates a function that is used by Passport to serialize users into the session
- deserializeUser() Generates a function that is used by Passport to deserialize users into the session
- register(user, password, cb) Convenience method to register a new user instance with a given password. Checks if username is unique. See login example.
- findByUsername() Convenience method to find a user instance by it's unique username.
- createStrategy() Creates a configured passport-local
LocalStrategy
instance that can be used in passport.
Examples
Allow only "active" users to authenticate
First we define a schema with an additional field active
of type Boolean.
var UserSchema = new Schema({
active: Boolean
});
When plugging in Passport Local Mongoose we set usernameUnique
to avoid creating a unique mongodb index on field username
. To avoid
non active users to be queried by mongodb we can specify the option findByUsername
that allows us to restrict a query. In our case
we want to restrict the query to only query users with field active
set to true
. The findByUsername
MUST return a Mongoose query.
UserSchema.plugin(passportLocalMongoose, {
usernameUnique: false,
findByUsername: function(model, queryParameters) {
queryParameters.active = true;
return model.findOne(queryParameters);
}
});
To test the implementation we can simply create (register) a user with field active
set to false
and try to authenticate this user
in a second step:
var User = mongoose.model('Users', UserSchema);
User.register({username:'username', active: false}, 'password', function(err, user) {
if (err) { ... }
var authenticate = User.authenticate();
authenticate('username', 'password', function(err, result) {
if (err) { ... }
});
});
License
Passport Local Mongoose Argon is licensed under the MIT license.