Easy Authentication in Express using Passport

  • Published at 
  • 6 mins reading time

Auth0 was the authentication provider I chose for my blog dashboard. They're simple and easy to set up, but I mainly used them because I haven't had the time to build one myself.

Few months after my blog went live, I begin rewriting authentication logic to the blog API itself. I decided to use password-based authentication because it's the most straightforward, and considering this is for personal use, I don't have to think about email verification and such.

My goal with this rewrite is pretty simple: provide drop in replacement for auth0 authentication logic and easy read/write token for consuming API endpoint.

Choosing library

For this task I choose to use passport (and passport-local) for handling authentication in express, and bcrypt for hashing user password.

User creation and password hash

Implementing password hashing is quite straightforward: I provide static method in my mongo schema to hash password and validate them.

// models/UserModel.js
UserSchema.statics.hashPassword = function hashPassword(plainTextPassword, cb) {
bcrypt.hash(plainTextPassword, BCRYPT_SALT_ROUNDS, cb);
};
UserSchema.methods.validatePassword = function validatePassword(
plainTextPassword,
cb
) {
const hashedPassword = this.password;
bcrypt.compare(plainTextPassword, hashedPassword, cb);
};

To create new user, use hashPassword static method first to get hashed password before creating user model instance.

User.hashPassword(password, (err, hashed) => {
if (err) {
// handle error
}
const user = new User({ email, password: hashed });
user.save();
});

User authentication

Passport has great documentation on how to implement different strategies. The most important part is initialize passport with your strategies using passport.use and call passport.authenticate to return express middleware to authenticate current request.

const strategy = new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
});
const validationCallback = (email, password, done) => {
// validate to db here
// call done() with error and user info
// done(error) or done(null, user);
};
// initialization
passport.use(strategy, validationCallback);
// authentication middleware
const authentication = passport.authenticate('local', (error, user, info) => {
// handle error & user here
});
app.post('/user/login', authentication);

That's basically it.

Read/write token

As I mentioned before, the goal of this migration is to provide different read and write token to API consumer. Blog dashboard should have write access, and blog frontend should have read access only.

The way I implemented this is by using /user/login API endpoint which returns token instead of user. Blog dashboard can ask for read+write token by providing email+password. Blog frontend can call /user/login with empty data to receive read only token.

I added a slight modification to /user/login route handler

import UserToken from './models/UserTokenModel';
app.post('/user/login', async (req, res, next) => {
const { email, password } = req.body;
// read+write token
if (email && password) {
const authenticate = passport.authenticate(
'local',
async (error, user, info) => {
// user exists mean authentication succeed
if (user) {
// also get existing token by email if possible
const token = await UserToken.create({ write: true });
res.json(token);
}
}
);
return authenticate(req, res, next);
}
// read only token
const token = await UserToken.create({ write: false });
res.json(token);
});

This token then will be used in other API endpoints to detect whether a request has proper permission or not. To achieve this, I create a middleware that sits before all API request (except request to /user/login)

// middleware/tokenValidation.js
export function tokenValidation(options = { write: false }) {
return async (req, res, next) {
try {
const tokenId = req.query.token || req.body.token;
if (!tokenId) {
return res.status(403);
}
// request includes token
const token = await UserToken.findOne({ token });
if (!token) {
return res.status(403);
}
if (options.write === true && token.type !== 'read-write') {
return res.status(403);
}
next();
} catch (err) {
next(err);
}
}
}

By creating a function that returns a middleware, we can use this middleware to detect both read-only access and read-write access.

import tokenValidaation from './middleware/tokenValidation';
const ro = tokenValidation({ write: false });
const rw = tokenValidation({ write: true });
app.get('/post/:id', ro, (req, res) => {
// get only need read access
});
app.post('/post/:id', rw, (req, res) => {
// write needs read-write
});
app.delete('/post/:id', rw, (req, res) => {
// delete needs read-write
});

I also added small improvements like using separate expiration time for read only token and read-write token and in-memory LRU cache to retrieve token data so the server doesn't need to query to DB every time.

Categorized under

Webmentions

If you think this article is helpful
© 2023 Fatih Kalifa. All rights reserved.