Node Basic authentication and access control

Using Passport to protect endpoints

Node Basic authentication and access control

by John Vincent


Posted on April 21, 2017


Build app to demonstrate basic authentication.

From Thinkful course Node 3.3.1.

Final Result

My Git repository

Getting Started

Clone repository

cd MyDevelopment/github-clones

git clone https://github.com/Thinkful-Ed/node-basic-auth

Create Github repository node-basic-auth

Copy source files from:

MyDevelopment/github-clones/node-basic-auth

to:

MyDevelopment/github/thinkful/node-basic-auth

Commit to repository

cd MyDevelopment/github/thinkful
create-repo node-basic-auth

Install dependencies

cd node-basic-auth
npm install

Start app

npm start

Test App Using Postman

List users

GET localhost:8080/users

return no records.

Add a User:

POST localhost:8080/users
Headers:
    key: content-type
    value: application/json
Body: raw

{
    "username": "foo",
    "password": "password",
    "firstName": "foo"
}

Record is created

status: 201
json:
{
  "username": "foo",
  "firstName": "foo",
  "lastName": ""
}

Query from database shows record has been created.

List users

GET localhost:8080/users

Access Control

Endpoint with access control

GET localhost:8080/users/me

returns:

status: 401 Unauthorized
content: Unauthorized

Headers:
WWW-Authenticate →Basic realm="Users"

To authenticate, we need to use the "basic" strategy at realm "Users".

Add Authorization Header

Postman

GET localhost:8080/users/me
Authorization: Basic Auth

Username: foo
Password: password

Update Request which creates a Header.

Send now returns:

status: 200
content:
{
  "user": {
    "username": "foo",
    "firstName": "foo",
    "lastName": ""
  }
}

Encrypting Passwords

models.js

UserSchema.methods.validatePassword = function(password) {
    return bcrypt.compare(password, this.password);
};

UserSchema.statics.hashPassword = function(password) {
    return bcrypt.hash(password, 10);
};

Data validation in router.js

router.post('/', (req, res) => {
    if (!req.body) {
        return res.status(400).json({message: 'No request body'});
    }

    if (!('username' in req.body)) {
        return res.status(422).json({message: 'Missing field: username'});
    }

    let {username, password, firstName, lastName} = req.body;

    if (typeof username !== 'string') {
        return res.status(422).json({message: 'Incorrect field type: username'});
    }

    username = username.trim();

    if (username === '') {
        return res.status(422).json({message: 'Incorrect field length: username'});
    }

    if (!(password)) {
        return res.status(422).json({message: 'Missing field: password'});
    }

    if (typeof password !== 'string') {
        return res.status(422).json({message: 'Incorrect field type: password'});
    }

    password = password.trim();

    if (password === '') {
        return res.status(422).json({message: 'Incorrect field length: password'});
    }

    // check for existing user
    return User.find({username})
        .count()
        .exec()
        .then(count => {
            if (count > 0) {
                return res.status(422).json({message: 'username already taken'});
            }
            // if no existing user, hash password
            return User.hashPassword(password);
        })
        .then(hash => {
            return User
                .create({
                    username: username,
                    password: hash,
                    firstName: firstName,
                    lastName: lastName
                });
        })
        .then(user => {
            return res.status(201).json(user.apiRepr());
        })
        .catch(err => {
            res.status(500).json({message: 'Internal server error'});
        });
});

Protecting Endpoints with Passport

router.js

const passport = require('passport');

...

const basicStrategy = new BasicStrategy((username, password, callback) => {
    let user;
    User.findOne({username: username})
        .exec()
        .then(_user => {
            user = _user;
            if (!user) {
                return callback(null, false, {message: 'Incorrect username'});
            }
            return user.validatePassword(password);
        })
        .then(isValid => {
            if (!isValid) {
                return callback(null, false, {message: 'Incorrect password'});
            } else {
                return callback(null, user);
            }
        });
});

passport.use(basicStrategy);
router.use(passport.initialize());

Protect endpoint

router.get('/me',
    passport.authenticate('basic', {session: false}), (req, res) => res.json({
        user: req.user.apiRepr()
    })
);

Note:

session: true
    authenticate once to make successive requests.

session: false
    supply credentials on each request.