Passwordless is a modern node.js module for Express that allows authentication and authorization without passwords by simply sending one-time password (OTPW) tokens via email or other means. It utilizes a very similar mechanism as the reset password feature of classic websites. The module was inspired by Justin Balthrop's article "Passwords are Obsolete"
Check out a demo and further documentation on https://passwordless.net or have a look at an example.
Token-based authentication is... * Faster to implement compared to typical user auth systems (you only need one form) * Better for your users as they get started with your app quickly and don't have to remember passwords * More secure for your users avoiding the risks of reused passwords
The following should provide a quick-start in using Passwordless. If you need more details check out the example, the deep dive, or the documentation. Also, don't hesitate to raise comments and questions on GitHub.
$ npm install passwordless --save
You'll also want to install a TokenStore such as MongoStore and something to deliver the tokens (be it email, SMS or any other means). For example:
$ npm install passwordless-mongostore --save
$ npm install emailjs --save
If you need to store your tokens differently consider developing a new TokenStore and let us know.
You will need: * Passwordless * A TokenStore to store the tokens such as MongoStore * Something to deliver the tokens such as emailjs for email or twilio for text messages / SMS
var passwordless = require('passwordless');
var MongoStore = require('passwordless-mongostore');
var email = require("emailjs");
This is very much depending on how you want to deliver your tokens, but if you use emailjs this could look like this:
var smtpServer = email.server.connect({
user: yourEmail,
password: yourPwd,
host: yourSmtp,
ssl: true
});
passwordless.init() will take your TokenStore, which will store the generated tokens as shown below for a MongoStore:
// Your MongoDB TokenStore
var pathToMongoDb = 'mongodb://localhost/passwordless-simple-mail';
passwordless.init(new MongoStore(pathToMongoDb));
passwordless.addDelivery(deliver) adds a new delivery mechanism. deliver is called whenever a token has to be sent. By default, the mechanism you choose should provide the user with a link in the following format:
http://www.example.com/token={TOKEN}&uid={UID}
That's how you could do this with emailjs:
// Set up a delivery service
passwordless.addDelivery(
function(tokenToSend, uidToSend, recipient, callback) {
var host = 'localhost:3000';
smtpServer.send({
text: 'Hello!\nAccess your account here: http://'
+ host + '?token=' + tokenToSend + '&uid='
+ encodeURIComponent(uidToSend),
from: yourEmail,
to: recipient,
subject: 'Token for ' + host
}, function(err, message) {
if(err) {
console.log(err);
}
callback(err);
});
});
app.use(passwordless.sessionSupport());
app.use(passwordless.acceptToken({ successRedirect: '/'}));
sessionSupport() makes the login persistent, so the user will stay logged in while browsing your site. Make sure to have added your session middleware before this line. Have a look at express-session how to setup sessions if you are unsure. Please be aware: If you decide to use cookie-session rather than e.g. express-session as your middleware you have to set passwordless.init(tokenStore, {skipForceSessionSave:true})
acceptToken() will accept incoming tokens and authenticate the user (see the URL in step 5). While the option successRedirect is not strictly needed, it is strongly recommended to use it to avoid leaking valid tokens via the referrer header of outgoing HTTP links. When provided, the user will be forwarded to the given URL as soon as she has been authenticated.
Instead of accepting tokens on any URL as done above you can also restrict the acceptance of tokens to certain URLs:
// Accept tokens only on /logged_in (be sure to change the
// URL you deliver in step 5)
router.get('/logged_in', passwordless.acceptToken(),
function(req, res) {
res.render('homepage');
});
The following takes for granted that you've already setup your router var router = express.Router(); as explained in the express docs
You will need at least URLs to: * Display a page asking for the user's email (or phone number, ...) * Receive these details (via POST) and identify the user
For example like this:
/* GET login screen. */
router.get('/login', function(req, res) {
res.render('login');
});
/* POST login details. */
router.post('/sendtoken',
passwordless.requestToken(
// Turn the email address into an user's ID
function(user, delivery, callback, req) {
// usually you would want something like:
User.find({email: user}, callback(ret) {
if(ret)
callback(null, ret.id)
else
callback(null, null)
})
// but you could also do the following
// if you want to allow anyone:
// callback(null, user);
}),
function(req, res) {
// success!
res.render('sent');
});
What happens here? passwordless.requestToken(getUserId) has two tasks: Making sure the email address exists and transforming it into a proper user ID that will become the identifier from now on. For example user@example.com becomes 123 or 'u1002'. You call callback(null, ID) if all is good, callback(null, null) if you don't know this email address, and callback('error', null) if something went wrong. At this stage, please make sure that you've added middleware to parse POST data (such as body-parser).
Most likely, you want a user registration page where you take an email address and any other user details and generate an ID. However, you can also simply accept any email address by skipping the lookup and just calling callback(null, user).
In an even simpler scenario and if you just have a fixed list of users do the following:
// GET login as above
var users = [
{ id: 1, email: 'marc@example.com' },
{ id: 2, email: 'alice@example.com' }
];
/* POST login details. */
router.post('/sendtoken',
passwordless.requestToken(
function(user, delivery, callback) {
for (var i = users.length - 1; i >= 0; i--) {
if(users[i].email === user.toLowerCase()) {
return callback(null, users[i].id);
}
}
callback(null, null);
}),
function(req, res) {
// success!
res.render('sent');
});
All you need is a form where users enter their email address, for example:
<html>
<body>
<h1>Login</h1>
<form action="/sendtoken" method="POST">
Email:
<input name="user" type="text">
<input type="submit" value="Login">
</form>
</body>
</html>
By default, Passwordless will look for a field called user submitted via POST.
You can protect all pages that should only be accessed by authenticated users by using the passwordless.restricted() middleware, for example:
/* GET restricted site. */
router.get('/restricted', passwordless.restricted(),
function(req, res) {
// render the secret page
});
You can also protect a full path, by adding:
router.use('/admin', passwordless.restricted());
Passwordless stores the user ID in req.user (this can be changed via configuration). So, if you want to display the user's details or use them for further requests, do something like:
router.get('/admin', passwordless.restricted(),
function(req, res) {
res.render('admin', { user: req.user });
});
You could also create a middleware that is adding the user to any request and enriching it with all user details. Make sure, though, that you are adding this middleware after acceptToken() and sessionSupport():
app.use(function(req, res, next) {
if(req.user) {
User.findById(req.user, function(error, user) {
res.locals.user = user;
next();
});
} else {
next();
}
})
Just call passwordless.logout() as in:
router.get('/logout', passwordless.logout(),
function(req, res) {
res.redirect('/');
});
Redirect non-authorised users who try to access protected resources with failureRedirect (default is a 401 error page):
router.get('/restricted',
passwordless.restricted({ failureRedirect: '/login' });
Redirect unsuccessful login attempts with failureRedirect (default is a 401 or 400 error page):
router.post('/login',
passwordless.requestToken(function(user, delivery, callback) {
// identify user
}, { failureRedirect: '/login' }),
function(req, res){
// success
});
After the successful authentication through acceptToken(), you can redirect the user to a specific URL with successRedirect:
app.use(passwordless.acceptToken(
{ successRedirect: '/' }));
While the option successRedirect is not strictly needed, it is strongly recommended to use it to avoid leaking valid tokens via the referrer header of outgoing HTTP links on your site. When provided, the user will be forwarded to the given URL as soon as she has been authenticated. If not provided, Passwordless will simply call the next middleware.
Error flashes are session-based error messages that are pushed to the user with the next request. For example, you might want to show a certain message when the user authentication was not successful or when a user was redirected after accessing a resource she should not have access to. To make this work, you need to have sessions enabled and a flash middleware such as connect-flash installed.
Error flashes are supported in any middleware of Passwordless that supports failureRedirect (see above) but only(!) if failureRedirect is also supplied:
- restricted() when the user is not authorized to access the resource
- requestToken() when the supplied user details are unknown
As an example:
router.post('/login',
passwordless.requestToken(function(user, delivery, callback) {
// identify user
}, { failureRedirect: '/login', failureFlash: 'This user is unknown!' }),
function(req, res){
// success
});
The error flashes are pushed onto the passwordless array of your flash middleware. Check out the connect-flash docs how to pull the error messages, but a typical scenario should look like this:
router.get('/mistake',
function(req, res) {
var errors = req.flash('passwordless'), errHtml;
for (var i = errors.length - 1; i >= 0; i--) {
errHtml += '
' + errors[i] + '
';
}
res.send(200, errHtml);
});
Similar to error flashes success flashes are session-based messages that are pushed to the user with the next request. For example, you might want to show a certain message when the user has clicked on the token URL and the token was accepted by the system. To make this work, you need to have sessions enabled and a flash middleware such as connect-flash installed.
Success flashes are supported by the following middleware of Passwordless:
- acceptToken() when the token was successfully validated
- logout() when the user was logged in and was successfully logged out
- requestToken() when the token was successfully stored and send out to the user
Consider the following example:
router.get('/logout', passwordless.logout(
{successFlash: 'Hope to see you soon!'} ),
function(req, res) {
res.redirect('/home');
});
The messages are pushed onto the passwordless-success array of your flash middleware. Check out the connect-flash docs how to pull the messages, but a typical scenario should look like this:
router.get('/home',
function(req, res) {
var successes = req.flash('passwordless-success'), html;
for (var i = successes.length - 1; i >= 0; i--) {
html += '
' + successes[i] + '
';
}
res.send(200, html);
});
For some token-delivery channels you want to have the shortest possible token (e.g. for text messages). One way to do so is to remove the user ID from the token URL and to only keep the token for itself. The user ID is then kept in the session. In practice his could look like this: A user types in his phone number, hits submit, is redirected to another page wh
$ claude mcp add passwordless \
-- python -m otcore.mcp_server <graph>