nodejs-mongodb_thv5ac

in Development

How to Write an API In One Week Part 2

nodejs-mongodb_thv5ac

Greetings, and welcome to part 2 of how to create a simple REST API written in NodeJS and Express, that uses MongoDB to store data, and JSON Web Tokens(JWT) to provide a simple authentication mechanism. In the first part we learned how to create a simple REST API that we used to store our cellared beers. In this part, we’ll go over how to add users to our API, and authenticate them with JSON Web Tokens. Our authentication mechanism will have the following permissions/rules:

  • If you are a user of role ‘user’, you’re allowed to Create, Read, Update, and Delete beers.
  • If you are a user of role ‘admin’, you’ll be able to Create, Read, Update and Delete beers and users (a new collection we will add).

In order to keep this article short-ish, I’ll only include the code for files we add, and files we modified from part 1. All of the code can be found in it’s complete form on my github account here: https://github.com/nortone/restapi, under part 2. If you are just arriving here, it’s advantageous to check out part 1 of this series here.

Let’s get to it.

First thing we’ll want to do is install the nodejs modules that are required to add authentication to our project. Let’s get into our project dir:

cd ~/restapi

First one is jwt-simple which implements JSON Web Tokens in nodejs:

npm install jwt-simple --save

Next one is bcrypt, which will provide us a way to encrypt user passwords before we store them in the database. Before we install bcrypt there is another module dependency which we’ll need called node-gyp, which will build bcrypt for us. We’ll install this one globally:

npm install node-gyp -g

Once that finishes:

npm install bcrypt --save

Alright, time to code. The first thing we’ll want to do is create a new directory:

mkdir -p ~/restapi/auth

As a quick review, let’s look at the directory structure and explain how our code is organized:

~restapi/

        |
   auth/          → where we’ll store our user authentication/validation code
   config/      → where we’ll store our configs
   dao/           → where we’ll store our data access objects
   models/   → where we’ll store our data models
   routes/     → our API routes will go here

Before we can add authentication, we’ll need a data model and a data access object for our users. Let’s create those now:

User Data Model:

Create:

~/restapi/models/user.js

With the code:

var mongoose = require('mongoose');
 
var userSchema = mongoose.Schema({
  username: String,
  password: String,
  role: String
});
 
module.exports = mongoose.model('User', userSchema);

User Data Access Object:

Create:

~/restapi/dao/users.js

With the code:

var bcrypt = require('bcrypt');
var User = require('../models/user');
var auth = require('../auth/auth');
 
var users = {
 
  getAll: function(req, res) {
    // let's load in the users key
    var key = (req.body && req.body.x_key) || (req.query && req.query.x_key) || req.headers['x-key'];
    if (key) {
      // let's see if this user is admin
      auth.isUserAdmin(key ,function (allow,err) {;// The key would be the logged in user's username
        if (allow) {
          User.find(function (err,users) {
            if (err) {
              console.log(err);
            } else {
              res.send(users);
            }
          });  
        } else {
          res.status(403);
          res.json({
            "status": 403,
            "message": "Not Authorized"
          });
        }
      });
    } else {
      res.status(401);
      res.json({
        "status": 401,
        "message": "Invalid Credentials",
      });
    }
  },
 
  getOne: function(req, res) {
    var id = req.params.id;
    // let's load in the users key
    var key = (req.body && req.body.x_key) || (req.query && req.query.x_key) || req.headers['x-key'];
    if (key) {
      // let's see if this user is admin
      auth.isUserAdmin(key ,function (allow,err) {;// The key would be the logged in user's username
        if (allow) {
          User.findOne({ username: id }, function (err,user) {
            if (err) {
              console.log(err);
            } else {
              res.send(user);
            }
          });
        } else {
          res.status(403);
          res.json({
            "status": 403,
            "message": "Not Authorized"
          });
        }
      });
    } else {
      res.status(401);
      res.json({
        "status": 401,
        "message": "Invalid Credentials",
      });
    }
  },
 
  create: function(req, res) {
    var body = req.body;
    // let's load in the users key
    var key = (req.body && req.body.x_key) || (req.query && req.query.x_key) || req.headers['x-key'];
    if (key) {
      // let's see if this user is admin
      auth.isUserAdmin(key ,function (allow,err) {;// The key would be the logged in user's username
        if (allow) {
          // we'll need to encrypt the password before we store it
          auth.encryptPass(body.password, function(hash) {
            var newuser = new User({
              username: body.username,
              password: hash,
              role: body.role,
              clientid: body.clientid
            });
            newuser.save(function(err,newuser) {
              if (err) {
                return console.error(err);
              } else {
                res.json(newuser);
              }
            });  
          });
        } else {
          res.status(403);
          res.json({
            "status": 403,
            "message": "Not Authorized"
          });
        }
      });
    } else {
      res.status(401);
      res.json({
        "status": 401,
        "message": "Invalid Credentials",
      });
    }
  },
 
  update: function(req, res) {
    var updateuser = req.body;
    var id = req.params.id;
    // let's load in the users key
    var key = (req.body && req.body.x_key) || (req.query && req.query.x_key) || req.headers['x-key'];
    if (key) {
      // let's see if this user is admin
      auth.isUserAdmin(key ,function (allow,err) {;// The key would be the logged in user's username
        if (allow) {
          User.findOneAndUpdate({username:id},updateuser, function (err,user) {
            if (err) {
              console.log(err);
            } else {
              res.json(user);
            }
          });
        } else {
          res.status(403);
          res.json({
            "status": 403,
            "message": "Not Authorized"
          });
        }
      });
    } else {
      res.status(401);
      res.json({
        "status": 401,
        "message": "Invalid Credentials",
      });
    }
  },
 
  delete: function(req, res) {
    var id = req.params.id;
    // let's load in the users key
    var key = (req.body && req.body.x_key) || (req.query && req.query.x_key) || req.headers['x-key'];
    if (key) {
      // let's see if this user is admin
      auth.isUserAdmin(key ,function (allow,err) {;// The key would be the logged in user's username
        if (allow) {
          User.remove({username:id}, function (err,user) {
            if (err) {
              console.log(err);
            } else {
              res.json(true);
            }
          });
        } else {
          res.status(403);
          res.json({
            "status": 403,
            "message": "Not Authorized"
          });
        }
      });
    } else {
      res.status(401);
      res.json({
        "status": 401,
        "message": "Invalid Credentials",
      });
    }
  }
};
 
module.exports = users;

As you can see, these are very similar to the model and data access object for our beers. You will also notice, however, that in the data access object for a user, there is logic to determine whether a user is of role ‘admin’ and has the proper permission to work with our users.

Let’s go over the user data access object. You’ll see that it contains 5 functions:

  • getall()   → will read all of our users from the db
  • getOne()   → will read one user from the db
  • create()   → will add a user to the db
  • update()   → will update a user in the db
  • delete()   → will delete a user from the db

Note that all of the above functions will only work if a user is of role ‘admin’. You can see the logic for this in each function by checking for the auth.isUserAdmin() section of the code. This function is brought into our project by a new javascript file called auth.js which we will get to in a moment.

Now that we have our user data model and data access object created, let’s update our routes to allow access to our new users collection. To do this we’ll modify the following file:

~/restapi/routes/index.js

To make sure it looks like this:

var express = require('express');
var router = express.Router();
 
var auth = require('../auth/auth');
var beer = require('../dao/beers');
var user = require('../dao/users');
 
 
/*
 * Login, accessible by anyone, you can find the logic for login function in auth/validate.js
 */
router.post('/login', auth.login);
 
/*
 * Routes for beers
 */
router.get('/beers', beer.getAll);
router.get('/beer/:id', beer.getOne);
router.post('/beer/', beer.create);
router.put('/beer/:id', beer.update);
router.delete('/beer/:id', beer.delete);
 
/*
 * Routes for users
 *  -Only accessible by role admin.
 *  -The logic that controls that is located in the user data access object)
 */
router.get('/users', user.getAll);
router.get('/user/:id', user.getOne);
router.post('/user/', user.create);
router.put('/user/:id', user.update);
router.delete('/user/:id', user.delete);
 
module.exports = router;

Notice the new routes for /user*. These routes are identical to the routes we created for our beers, only they are using the user data access object. In addition, you’ll also notice a new route for /login. This route handles authentication of a user by accepting username/password as a POST. Now, we’re ready to create our authentication code. Let’s first start with the file that will provide the login function (amongst others) that our /login route calls:

~/restapi/auth/auth.js

The code will look like this:

var jwt = require('jwt-simple');
var User = require('../models/user');
var config = require('../config/config');
var bcrypt = require('bcrypt');
 
var auth = {
 
  login: function(req, res) {
 
    var username = req.body.username || '';
    var password = req.body.password || '';
    console.log('u: ' + username);
    console.log('p: ' + password);
    if (username == '' || password == '') {
      res.status(401);
      res.json({
        "status": 401,
        "message": "Invalid credentials"
      });
      return;
    }
 
    // Fire a query to your DB and get the user object if it exists
    auth.getUser(username, function(dbUserObj,err) {
      if (!dbUserObj) { // If authentication fails, we send a 401 back
        res.status(401);
        res.json({
          "status": 401,
          "message": "Invalid credentials"
        });
        return;
      }
 
      if (dbUserObj) {
 
        // If authentication is success, we will generate a token
        // and dispatch it to the client
        bcrypt.compare(password,dbUserObj.password, function(err, passmatch) {
            if (passmatch == true) {
              res.json(genToken(dbUserObj));
            } else {
              res.status(401);
              res.json({
                "status": 401,
                "message": "Invalid credentials"
              });
              return;
            }
        });
      }
    });
 
  },
 
  getUser: function(username,callback) {
      User.findOne({ username: username }, function (err,user) {
        if (err) {
          console.log(err);
          callback(false);
        } else {
          callback(user);
        }
      });
  },
 
  isUserAdmin: function(username,callback) {
      User.findOne({ username: username }, function (err,user) {
        if (user.role == 'admin') {
          callback(true);
        } else {
          callback(false);
        }
      });
  },
  encryptPass: function(password,callback) {
      // this will auto-gen a salt
      // bcrypt.hash(password,size of hash, function)
      bcrypt.hash(password, 12, function(err, hash) {
        if (hash) {
          callback(hash);
        } else {
          console.log(err);
        }
      });
  }
}
 
/** private methods **/
 
// generate token
function genToken(user) {
  var expires = expiresIn(7); // 7 days
  var token = jwt.encode({
    exp: expires
  }, config.jwtsecret);
 
  return {
    token: token,
    expires: expires,
    user: user.username
  };
}
 
function expiresIn(numDays) {
  var dateObj = new Date();
  return dateObj.setDate(dateObj.getDate() + numDays);
}
 
module.exports = auth;

Let’s explain a few things in this file. Here’s the list of functions and what they do:

  • login()                  → will authenticate a user who calls our /login route
  • getUser()            → this will get a user object, if the user exists in the db
  • isUserAdmin()   → this was mentioned above, and will check if the given user has a role of ‘admin’.
  • encryptPass()     → will encrypt a given password to store in db

Private functions:

  • genToken()       → will generate JSON Web token to be used for user validation
  • expiresIn()        → helper function to create an expiration time for the token created in genToken()

In order for our genToken() function to work, we need to provide a salt. We’ll add that to our config.

Modify your config:

~/restapi/config/config.js

So it looks like this:

/** Declare config values **/
 
// define config object
var config = {};
 
// mongodb connection
config.db = "mongodb://localhost/restapi";
 
// JWT salt/secret
config.jwtsecret = "xUspTR6giWEJ96LkDi9SBhChpB8PJUQl4wLOk6MJtmYJSb4XHdNUkNrXWFA9J81";
 
 
module.exports = config;

You can use the salt I used in the code above, or create your own. If you want to create your own, a great tool that I like to use to create random, cryptographic-strength text is Steve Gibson’s Perfect Passwords tool. Obviously, this is a development example of how to build a REST API. If we ever use this in production, we’ll want to take measures to protect the JWT salt, because if it gets into the wrong hands, the security will be broken for your API until you change your salt.

Alright, the auth.js file is ready and will provide functions that we can use to authenticate a user, but we’ll need a middleware piece of code that validates a user request. This code will use the functions we’ve created in auth.js. Let’s do that now.

Create this file:

~/restapi/auth/validate.js

With this code:

var jwt = require('jwt-simple');
var getUser = require('./auth').getUser;
var config = require('../config/config');
 
 
module.exports = function(req, res, next) {
 
  // let's load in the values of the access token and the access key from our headers
  var token = req.headers['x-access-token'];
  var key = req.headers['x-key'];
 
  // let's check to see if user has valid token or key
  if (token || key) {
    try {
      var decoded = jwt.decode(token, config.jwtsecret);
 
      if (decoded.exp <= Date.now()) {
        res.status(400);
        res.json({
          "status": 400,
          "message": "Token Expired"
        });
        return;
      }
 
      // Authorize the user for access
      getUser(key ,function (dbUser,err) { /** The key would be the logged in user's username */
          // this checks to make sure we get a user object
          if (dbUser) {
              next(); // user exists, move on
          } else {
            /**
             * No user with this name exists, respond back with a 401
             *(this error could also mean there was a problem retrieving the user object from the db)
             *
             */
            res.status(401);
            res.json({
              "status": 401,
              "message": "Invalid User"
            });
            return;
          }
      }); // end getUser()
 
    } catch (err) {
      res.status(500);
      res.json({
        "status": 500,
        "message": "Token Error."
      });
    }
  } else {
      res.status(401);
      res.json({
        "status": 401,
        "message": "Invalid Token or Key"
      });
    return;
  }
};

Before I explain what’s going on in this code, let’s update the main script of our app server.js so that it will use this validate.js code when certain routes are accessed.

Modify:

~/restapi/server.js

So it looks like this:

var express = require('express');
var path = require('path');
var logger = require('morgan');
var bodyParser = require('body-parser');
var config = require('./config/config');
 
var app = express();
 
var mongoose = require('mongoose');
var db = mongoose.connection;
 
// connect to the db
mongoose.connect(config.db);
 
app.use(logger('dev'));
app.use(bodyParser.json());
 
 
app.all('/*', function(req, res, next) {
  // CORS headers
  res.header("Access-Control-Allow-Origin", "*"); // restrict it to the required domain
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
  // Set custom headers for CORS
  res.header('Access-Control-Allow-Headers', 'Content-type,Accept');
  // If someone calls with method OPTIONS, let's display the allowed methods on our API
  if (req.method == 'OPTIONS') {
    res.status(200);
    res.write("Allow: GET,PUT,POST,DELETE,OPTIONS");
    res.end();
  } else {
    next();
  }
});
 
// start db
db.on('error', console.error);
db.once('open', function() {
 
  /**
   *
   * In case we want other routes to be unprotected, let's just make sure that
   * /beer* and /user* are validated, the rest will not be authenticated like the
   * /login route for instance
   * 
   */
  app.all('/beer*', [require('./auth/validate')]);
  app.all('/user*', [require('./auth/validate')]);
 
  app.use('/',require('./routes'));
 
  // If no route is matched, return a 404
  app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
  });
 
  // Start the server
  app.set('port', process.env.PORT || 3000);
 
  var server = app.listen(app.get('port'), function() {
    console.log('Express server listening on port ' + server.address().port);
  });
});

Note that we’ve added these lines(lines 46-47):

  app.all('/beer*', [require('./auth/validate')]);
  app.all('/user*', [require('./auth/validate')]);

This means that every time we access /beer* or /user*, we’ll make sure to run the code in validate.js first. What does it mean to ‘validate’ a user? In this case, it means that we check for a valid token, and if the token is valid, we allow the request to continue, or we send a proper response to the user letting them know why the request can’t continue.

Excellent, we finally made it to the testing phase. Now we need to add users. “But wait”, you may be saying to yourself. “How can we add users if we don’t have any users in the system?” It’s a bit of a chicken and egg problem. We’ve discovered that our app has another dependency: our app needs to be seeded with a super user. Let’s handle that.

Since we don’t want to store plain text passwords, we’re going to need a way to encrypt the password we want to use. Using the bcrypt module, let’s write a quick script to do that for us.

Create:

~/restapi/encryptpw.js

And add this code:

var bcrypt = require('bcrypt');
 
// bcrypt.hash('password to encrypt',salt -or- number of rounds if you want to auto-gen salt, callback)
bcrypt.hash('pass123',12, function(err, hash) {
	if (err) {
		console.log("error = " + err);
	} else {
		console.log(hash);
	}
});

I’ve used ‘pass123’ as the password to encrypt, but you can put whatever you like there. Just make sure to remember the password, because you’ll need it once we get to the login example. When you run this:

cd ~/restapi
node encryptpw

You should see something like this as the output:

$2a$12$Xwm/nnErOCPDi.9DL3uuiO/exJ12Dyz4FbyH0PwzhqN9HgwMxGxS2

**Note: since we’re calling this function in a way that auto-generates a salt, this output will be different every time you run it. While that seems counter productive, It’s ok because bcrypt stores the salt within the outputted hash. I personally prefer the auto generation of the salt, but if you want to specify your own salt, you need to use this as the format of the salt:

$Vers$log2(NumRounds)$saltvalue

In my example above, I’ve used the number ‘12’ in place of the salt. This number represents the number or ‘rounds’ it will use to auto-generate a salt. In this case it’s 12 rounds or 2^12 iterations.
Alright, we’ve got our password, let’s add the super user to our user collection.

We’ll add the user to mongo by connecting to the mongo db directly.

Type:

mongo

Then once you get the mongo prompt, change to the db we want to use:

use restapi

And insert the user:

db.users.insert([{"username":"eric@jixee.me","password":"$2a$12$Xwm/nnErOCPDi.9DL3uuiO/exJ12Dyz4FbyH0PwzhqN9HgwMxGxS2","role":"admin"}]);

You should see something like:

BulkWriteResult({
        "writeErrors" : [ ],
        "writeConcernErrors" : [ ],
        "nInserted" : 1,
        "nUpserted" : 0,
        "nMatched" : 0,
        "nModified" : 0,
        "nRemoved" : 0,
        "upserted" : [ ]
})

Ok, NOW we’re ready to test.

Let’s fire up our app:

cd ~restapi
nodemon

If all goes well, your app should start and will be ready to accept connections. Let’s throw some requests at it. The first thing we need to test is our login process. We’ll use cURL for all of our interactions with the API:

curl -H "content-type:application/json" http://localhost:3000/login -d '{
  "username":"eric@jixee.me",
  "password":"pass123" 
}'

You should get a response that looks similar to:

{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MzkxOTEzNjZ9.NU66ACAODW64N9DMJ7Fq9DEBSu8C3megXoFxR2XWtg0","expires":1424739191366,"user":"eric@jixee.me"}

In the successful login response, you receive the ‘token’, when it ‘expires’, and the ‘user’ for which the token is valid. Let’s test out the token and display all of our users (we will only be able to do this as an ‘admin’ user). Something to be aware of is that I’ll be using my tokens for the following cURL examples. Your token will be different, so make sure to use the tokens you receive.

Since we now have implemented JWT auth in our application, we will need to provide a few more headers with our request. These new headers are needed to access any routes on our API that are not the /login route. Previously, we only needed to use this header:

Content-Type: application/json

Now, however, we’ll need to include a couple more:

x-access-token   → contains the token we received from /login
x-key                    → this is our username, so in this case, eric@jixee.me

Put them all together so it looks like this:

curl -XGET -H "content-type:application/json" -H "x-access-token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MzkxOTEzNjZ9.NU66ACAODW64N9DMJ7Fq9DEBSu8C3megXoFxR2XWtg0" -H "x-key:eric@jixee.me" http://localhost:3000/users

You should see the following with the user you created:

[{"_id":"54caa27a0a26a1995fb85081","username":"eric@jixee.me","password":"$2a$12$Xwm/nnErOCPDi.9DL3uuiO/exJ12Dyz4FbyH0PwzhqN9HgwMxGxS2","role":"admin"}]

If you see the above, then you made a proper request, and your token worked! At this point, you can now use that token to perform CRUD functions on users and beers. Let’s add a user with the role ‘user’.

curl -X POST -H "Content-Type: application/json" -H "x-access-token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MzkxOTEzNjZ9.NU66ACAODW64N9DMJ7Fq9DEBSu8C3megXoFxR2XWtg0" -H "x-key:eric@jixee.me" http://localhost:3000/user -d '{
    "username":"newuser@jixee.me",
    "password":"pass123",
    "role":"user"
}'

If it works you should see:

{"__v":0,"username":"newuser@jixee.me","password":"$2a$12$B1JJZ8px56xyZpoMHBN//OoMlfI4R9trrwefHJMae.4fgOsKYlQ9C","role":"user","_id":"54e2aa26dcedcae107b52c9b"}

Notice how the password it displays is the encrypted version. If you look closer at the code for our users data access object(~/restapi/dao/users.js), you’ll see that in the create() function, we are taking the given password and encrypting it before we store it.

Now, let’s get a token for this new user:

curl -H "content-type:application/json" http://localhost:3000/login -d '{
    "username":"newuser@jixee.me",
    "password":"pass123"
}'

If all goes well, you should see:

{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3NDYxNjUzMTZ9.1OSOd74ClVAVrrcecAqMFostq2JES_6VYc6O962jXRk","expires":1424746165316,"user":"newuser@jixee.me"}

Now that we’ve created a new user with the role of ‘user’, let’s make sure this user doesn’t have access to any of the /user* routes:

curl -XGET -H "content-type:application/json" -H "x-access-token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3NDYxNjUzMTZ9.1OSOd74ClVAVrrcecAqMFostq2JES_6VYc6O962jXRk" -H "x-key:newuser@jixee.me" http://localhost:3000/users

You’ll see:

{"status":403,"message":"Not Authorized"}

Great! Our permissions are working. Now let’s make sure this user can work with the beers collection:

curl -XGET -H "content-type:application/json" -H "x-access-token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3NDYxNjUzMTZ9.1OSOd74ClVAVrrcecAqMFostq2JES_6VYc6O962jXRk" -H "x-key:newuser@jixee.me" http://localhost:3000/beers

If your database is still intact from part 1, you should see something like this:

[
    {
        "_id": "54c6ce5949038d6a3a050236",
        "beerid": "blacktuesday",
        "brewery": "The Bruery",
        "abv": "19.20",
        "year": "2013",
        "cellardate": "2013-08-01T07:00:00.000Z",
        "style": "Imperial Stout",
        "description": "bourbon barrel aged",
        "notes": "store at 55 degress celsius",
        "total": 2,
        "__v": 0
    },
    {
        "_id": "54c7158ee63d39743df82f2f",
        "beerid": "parabola",
        "brewery": "Firestone Walker Brewing Co.",
        "abv": "13.00",
        "year": "2014",
        "cellardate": "2014-04-15T07:00:00.000Z",
        "style": "Russian Imperial Stout",
        "description": "bourbon barrel aged",
        "notes": "store at 55 degrees celsius",
        "total": 2,
        "__v": 0
    }
]

If your new user was able to access the /beer* routes, you’re done! You’ve successfully written a REST API with simple JWT auth. Now you’ve got a basic framework to build whatever REST API you can think up. I encourage you to expand on this and build something you can use in a work or school setting.

I spend most of time in terminals, but a REST API is much sweeter with an interface. Stay tuned for another series that will build a simple Angular UI that interacts with our API.

I hope this was helpful. Comments are welcome.

Go forth and build!

  • Tree Machine Records

    Just a heads up, there are a few places where ‘&&’ gets converted to && in the code. Same situation with a ‘<='

    • Eric

      Hi, thank you for the heads up. That was sitting there like that for a long time. Fixed. Also, I’m not sure if you knew, but I also have this code up on Github, in case you wanted to give it another shot: https://github.com/nortone/restapi

      • Tree Machine Records

        Thanks Eric, I got it working by resetting the database collection and built and AngularJS UI. Now on to adding authentication.

        • Eric

          Great! Sounds like you’re havin’ fun. :)

  • Tree Machine Records

    Also having a lot of trouble getting POST to work.

    • Eric

      I’m wondering if you’re getting weird characters by copying and pasting the curl line from this post. What error does it throw when you try to POST?

  • Tree Machine Records

    Have you posted about Authentication with AngularJS yet? Really struggling to solve it after trying several tutorials.