Scott Smith

Blog Tutorials Projects Speaking RSS

Beer Locker: Building a RESTful API With Node - Digest

Welcome to part 5 of the Beer Locker series

  1. Getting started
  2. CRUD
  3. Passport
  4. OAuth2 Server
  5. Digest
  6. Username & Password

In our previous article we ended wtih a functional API capable of creating user accounts, locking down API endpoints, only allowing access to a user’s own beer locker, and an OAuth2 server.

Many readers have asked questions about how to use different authentication strategies so I am going to continue this series and delve into many of those strategies.

This article will explore the use of Digest authentication instead of Basic.

Digest

Like the Basic scheme, Digest uses a username and password to authenticate a user. The benefit it provides over Basic is that it uses a challenge-response paradigm to avoid sending the password in the clear.

Here is how it works:

  1. Client sends an unauthenticated request to a server.
  2. Server responds with a 401 “Unauthorized” reponse code along with a special code (called a nonce) and another string representing the authentication realm.
  3. Client responds with the nonce and an encrypted version of the username, password and realm.
  4. Server responds with a 200 OK and the response data if the authentication passes.

Update our Routes

There is something odd going on with the way in which we defined our routes in the original tutorials. In most cases it isn’t an issue but it is for Digest auth. When we tell Express to use our router, we were prefixing it with /api. The problem is that Digest auth uses the URI as part of the scheme and the request object within Express is seeing it without the /api portion of the URI. This will cause authentication to fail. You will need to update server.js routes like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
...

// Create endpoint handlers for /beers
router.route('/api/beers')
  .post(authController.isAuthenticated, beerController.postBeers)
  .get(authController.isAuthenticated, beerController.getBeers);

// Create endpoint handlers for /beers/:beer_id
router.route('/api/beers/:beer_id')
  .get(authController.isAuthenticated, beerController.getBeer)
  .put(authController.isAuthenticated, beerController.putBeer)
  .delete(authController.isAuthenticated, beerController.deleteBeer);

// Create endpoint handlers for /users
router.route('/api/users')
  .post(userController.postUsers)
  .get(authController.isAuthenticated, userController.getUsers);

// Create endpoint handlers for /clients
router.route('/api/clients')
  .post(authController.isAuthenticated, clientController.postClients)
  .get(authController.isAuthenticated, clientController.getClients);

// Create endpoint handlers for oauth2 authorize
router.route('/api/oauth2/authorize')
  .get(authController.isAuthenticated, oauth2Controller.authorization)
  .post(authController.isAuthenticated, oauth2Controller.decision);

// Create endpoint handlers for oauth2 token
router.route('/api/oauth2/token')
  .post(authController.isClientAuthenticated, oauth2Controller.token);

// Register all our routes
app.use(router);

...

Update our Auth Controller

The first thing we need to do is update our Auth Controller. Open controllers/auth.js and implement the Digest strategy as follows. You can leave the Basic auth strategy implementation if you want. We will be updating the isAuthenticated to use Digest instead of Basic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Load required packages
var DigestStrategy = require('passport-http').DigestStrategy;

...

passport.use(new DigestStrategy(
  { qop: 'auth' },
  function(username, callback) {
    User.findOne({ username: username }, function (err, user) {
      if (err) { return callback(err); }

      // No user found with that username
      if (!user) { return callback(null, false); }

      // Success
      return callback(null, user, user.password);
    });
  },
  function(params, callback) {
    // validate nonces as necessary
    callback(null, true);
  }
));

...

exports.isAuthenticated = passport.authenticate(['digest', 'bearer'], { session : false });

We have done 3 main things here.

First, we required the DigestStrategy provided by the passport-http module.

Second, we told Passport to use DigestStrategy. Inside this we told it to use qop: 'auth' which is quality of protection along with two anonymous functions. The first function does our verification of the user and if success we call the callback and supply the user along with their password. The second anonymous function can be used to protect against replay attacks. Nonces should be validated to make sure they are not used again. You can do this by storing issued nonces and removing them as they are used. This will increase the security of your authentication.

Third, we updated isAuthenticated to use digest instead of basic.

Remove hashing of passwords :(

This is the part of using Digest and more specifically the implementation provided by the passport-http module that I like the least. You cannot store the password as a hash. You need to get to the original password in order for the authentication to work. At the very least you will want to encrypt the password. I highly advise you analyze this type of approach for your application. If you must use Digest, you may want to consider implementing your own Digest strategy or updating the existing one where you don’t pass in the password in the success callback. You could instead pass in the MD5 hash of the username:realm:password which you have pre calculated and stored.

So on to the very unsafe update to our User model. Again, for the tutorial we are going to store the password in plain text. You NEVER want to do this in a real situation. You will want to look into cryptography modules such as crypto to do this.

Open up models/user.js and update it to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Load required packages
var mongoose = require('mongoose');

// Define our user schema
var UserSchema = new mongoose.Schema({
  username: {
    type: String,
    unique: true,
    required: true
  },
  password: {
    type: String,
    required: true
  }
});

// Export the Mongoose model
module.exports = mongoose.model('User', UserSchema);

Testing it out

Fire up your trusted tool such as Postman and create a new user as the old ones will have hashed passwords which will not work.

I found using Postman to test Digest a bit unfriendly so I opted instead for curl.

Here is a command you can use to test your API using Digest:

1
curl -v --user smith:smith --digest http://127.0.0.1:3000/api/users

Just change the username:password to whatever you used. This command is great in curl because it will issue the intial unauthenticated request, process the reponse along with the nonce, qop, realm, etc, and finally issue another authenticated request.

Here is what my test looked like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
curl -v --user smith:smith --digest http://127.0.0.1:3000/api/users

* About to connect() to 127.0.0.1 port 3000 (#0)
*   Trying 127.0.0.1...
* Adding handle: conn: 0x7ffb71804000
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7ffb71804000) send_pipe: 1, recv_pipe: 0
* Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0)
* Server auth using Digest with user 'smith'
> GET /api/users HTTP/1.1
> User-Agent: curl/7.30.0
> Host: 127.0.0.1:3000
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< X-Powered-By: Express
< WWW-Authenticate: Digest realm="Users", nonce="x9hR8HiV9szqnvzUU7DsS5ekfq4xNM2p", algorithm=MD5, qop="auth"
< WWW-Authenticate: Bearer realm="Users"
< Date: Sun, 14 Sep 2014 23:05:02 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
* Ignoring the response-body
* Connection #0 to host 127.0.0.1 left intact
* Issue another request to this URL: 'http://127.0.0.1:3000/api/users'
* Found bundle for host 127.0.0.1: 0x7ffb71415150
* Re-using existing connection! (#0) with host 127.0.0.1
* Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0)
* Adding handle: conn: 0x7ffb71804000
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7ffb71804000) send_pipe: 1, recv_pipe: 0
* Server auth using Digest with user 'smith'
> GET /api/users HTTP/1.1
> Authorization: Digest username="smith", realm="Users", nonce="x9hR8HiV9szqnvzUU7DsS5ekfq4xNM2p", uri="/api/users", cnonce="ICAgICAgICAgICAgICAgICAgICAgIDE0MTEzMjk4NDQ=", nc=00000001, qop=auth, response="a9b8ab69c3a44c253a7834bdfe45b26d", algorithm="MD5"
> User-Agent: curl/7.30.0
> Host: 127.0.0.1:3000
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json
< Content-Length: 354
< ETag: "-1301455500"
< Date: Sun, 14 Sep 2014 23:05:02 GMT
< Connection: keep-alive
<
* Connection #0 to host 127.0.0.1 left intact
[...]

Wrap up

You now have the tools needed to implement Digest authentication.

I still strongly caution against the use as it currently stands. I highly dislike and advise against storing passwords encrypted. It is only slightly better than plain text since it is encrypted, but it is only one step away from becoming plain text. The best is to implement your own Digest strategy so you can store your passwords in a way that they can be hashed.

If you have thoughts on this, please share them in the comments section.

I have a lot more tutorials coming so be sure to subscribe to my RSS feed or follow me on Twitter. Also, if there are certain topics you would like me to write on, feel free to leave comments and let me know.

Source code for this part can be found here on GitHub.