RSS Amplifier

Fatih Kalifa · Aug 23, 2017

Easy Authentication in Express using Passport

0
Sign in to vote or save

Fatih Kalifa · Paper

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.

Read the original on fatihkalifa.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.

    Reading · Fatih Kalifa · RSS Amplifier