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.jsUserSchema.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);};// initializationpassport.use(strategy, validationCallback);// authentication middlewareconst 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.jsexport 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.