Meteor REST APIs with JsonRoutes


#1

I’m using JasonRoutes for REST APIs in Meteor now and don’t understand yet why the Bearer Tokens are used and how to use them.

I’m on Meteor 1.5 and using the following libs:

~ accounts-password
~ simple:authenticate-user-by-token
~ simple:json-routes
~ simple:rest-accounts-password
~ simple:rest-json-error-handler

I can log in fine like this:

Login:

curl -H "Content-Type: application/json" -X POST --data '{"email":"test@test.com","password":"test123"}' http://localhost:3000/users/login                                                                                          

{
  "id": "DGQcL5GdzNv7kC6s8",
  "token": "61bxDTvBg3JMHH_A9sU4ub6FyjDVt1l4frvj7KmIgyJ",
  "tokenExpires": "2017-10-05T16:36:49.463Z"
}%

My headers settings:

JsonRoutes.setResponseHeaders({
  "Cache-Control": "no-store",
  "Pragma": "no-cache",
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With"
});

I have an example route here I can call just fine, like this:

curl --header "Content-Type: application/json" --request POST --data '{"userId":"111"}' http://localhost:3000/test


// Example Route

JsonRoutes.add('POST', 'test', (req, res) => {
  debugger;
  console.log(`req.body.userId: ${req.body.userId}`);
  // console.log(`req.headers.authorization: ${req.headers.authorization}`);
  var userId = req.body.userId || null;
  var statusCode = 200;

  var user = Meteor.users.findOne({ '_id': userId });

  JsonRoutes.sendResult(res, {
    code: statusCode,
    data: user,
  });
});

I know how to pass in the Bearer Token too:

curl -H "Content-Type: application/json, Authorization: Bearer 61bxDTvBg3JMHH_A9sU4ub6FyjDVt1l4frvj7KmIgyJ" -X POST  http://localhost:3000/test

I guess I just don’t understand yet why the Bearer Tokens are used and how to use them. Please help.


Official Rest API support in Meteor?
Adding web services to Meteor in 2017
Adding web services to Meteor in 2017
#2

UPDATE:

After “logging in” over HTTP (same code as above), I can get the following code to tick off all the IDs and Tokens,

import { JsonRoutes } from 'meteor/simple:json-routes';

JsonRoutes.add('POST', 'auth/test2', (req, res) => {
  debugger;
  console.log(`req.body.userId: ${req.body.userId}`);
  let data = {};
  let body = req.body || null;

  // The ID passed in via the client on in the body
  let passedInUserId = req.body.userId || null; // passedInUserId
  // The authenticated user's ID will be set by this middleware
  let authUserId = req.userId;
  // A valid login token for a Meteor.user account (requires accounts-base)
  let authToken = req.authToken || null;
  let statusCode = 200;

  var user = Meteor.users.findOne({ '_id': passedInUserId });

  JsonRoutes.sendResult(res, {
    code: statusCode,
    data: {
      PassedInUserId: passedInUserId,
      AuthUserId: authUserId,
      AuthToken: authToken,
      UserFromPassedInUserId: user
    }
  });
});

Using this Authorization Header (I got the Token from the login call earlier):

And This userId passed in on the body:

Now that I’ve established I can get the Auth Token and that the Middle ware is authenticating and retreiving the userId – how do I handle the situation?

I mean is there an automated way to have the middleware kick the request out if call does not have the proper token or do I need to do that mantually?

To do things manually, maybe, I’d have to hitch a ride on the user logging in, and within that process log in to the web service and get my Bearer token, and store it for that user somewhere on the client or server (on a web app outside of the WebApp API). Then if a call is made by the user to the WebApp API, I pass that Bearer token in with the Web API call. ??

There’s also a process where an admin can “impersonate” a user, they actually log in by token as a user. I guess I’d have to hitch a ride on that process and then do the same as above.

That’s seems like one way to do it.


Also what does the HTTP.Post look like to make the call?


#3

Ever been to a club or concert where they card you to make sure you’re 21, and then stamp your hand? The stamp is a Bearer Token. You carry it with you (bearing it); and if it exists, you have access; if it doesn’t, you’re denied.

But there are different types of bearer tokens, right? Some concerts will stamp your hand, while others will give you a plastic bracelet. The stamp washes off, while the plastic is bad for the environment. So different pros/cons to how people implement their auth token.

You want to do the same basic thing with your app. You’re going to wrap the sensitive parts of your code with an if() statement that checks for the existence of that auth token. You’ll see that we repeat those lines of code in each one of our JsonRoutes… see lines 205, 261, 361, 424, 525, 573, and 636.

You can set the AuthToken any number of different ways. Random number generator, phases of the moon, one-time key, oauth protocol, etc. Generally, it’s best that the server sets it, but not 100% necessary.

BearerToken is a slightly older implementation… OAuth 1.0, maybe? Or non-OAuth implementation? Regardless, we don’t actually use it, as we use OAuth instead. But it’s all basically the same thing.

Take a look at the JsonRoutes for the FHIR Patient resource. It’s sort of the gold-standard of the Clinical Meteor project; and we’ve put a lot of time into this file to make sure it conforms to Cerner and Epic (the major EMR vendors). Not to brag, but you’ll find it a very comprehensive JsonRoutes example that showcases a lot of fairly advanced usage for real world scenarios.


#4

Wow, an amazing answer, as always @awatson1978!! I’ll review the code; it’s just was I was scouring the web looking for!

.

Thank you so very much, you came through in my time of need over here. :smiley:

.

Also, I one you a favor now, so please let me know if you ever need a hand!


#5

@aadams it seems @awatson1978 has beat me to it, and wonderfully indeed! :slight_smile:


#6

@serkandurusoy, no please do give your advice here!

Please comment on my idea for how to pass over the token to the API and where to store the token afterwards? For example, does passing over the email/password to the API when the user loggs in makes sense?


#7

Hey @aadams api requests should be stateless, meaning each request has its own independent lifecycle, unlike a meteor application logging in and using that logged in state across the app lifecycle, each api request ends with the response.

Aside: There are architectures that would allow stateful strategies but they are applied for web applications, not rest apis.

Now, given this, there are two more common approaches for handling this during a reqest response lifecycle.

  • Use a middleware right at the beginning, check for auth and end everything with an error response
  • Wrap all your handler functions with an if (!user) /*respond with error*/

For a lot of reasons, the first approach is better, leaner, more manageable.

What’s a middleware? That function body you see inside that app.use() block. That’s just a fancy word for “software that interferes all incoming requests in the middle of the lifecycle somewhere, usually at the beginning”.

Since they all have access to the (req, res, next) function signature (and for a special error handling case, (err, req, res, next)), you can do this at the very beginning:

function tokenIsAValidUser(token) {
  return /* check your db if the token is valid for a user */
}

function sendNoAuthResponse(res) {
  res.writeHead(401) // http://www.restapitutorial.com/httpstatuscodes.html
  res.end(JSON.stringify({ status: 'error', reason: 'You need to be authenticated' }))
}

app.use((req, res, next) => {
  const token = /* get your token from the req object */
  if (!tokenIsAValidUser(token)) sendNoAuthResponse(res)
  next() 
})

Now there might be cases where there is a user logged in but that certain user is not authorized to do some certain things. In those cases, you would apply some common logic like grouping your routes to certain paths and applying middleware for certain paths (middleware itself checks if the path matches) and/or have your individual route handling functions do their own required authorization checks, just like they would check other parameters for a valid requests, and if something’s not valid, return an error response.

Aside: there’s a special middleware function signature (err, req, res, next) which is internally a handler that catches all actual javascript errors thrown by other middleware or route handlers. You can use this mechanism to simply throw from your handlers and create/send the response using this special error handling middleware, you should, at this point, probably do some more in depth reading of how connect/express middleware works

As for the actual authentication mechanism, such as passing in email/password, that’s a very wide topic because there are obviously lots of different auth strategies, all with their pros and cons, especially in terms of security. If you’re relying on Meteor auth, do use the simple:rest packages and their auth middleware because it is the most straightforward and read its source code to understand what’s going on.

If you want to use a more generic authentication strategy, embrace a framework like passport or a third party service like auth0. Do not, I repeat do not, attempt to put together an auth mechanism yourself. It would take ten pages of content here to describe the things that can go wrong (especially in terms of security).


#8

Thank you very much for this. I took your advince and used middle ware to check for auth using JsonRoutes.

This is the code:

If the user is validated, the middleware just calls next() and the process works so far. Yet, if the user is not validated here, it does indeed return, but repose is 'undefined'.

I can add data to the options and it will return 'undefined'.

But if I add anything, such as a statusCode property to options or just nothing, then it returns a 200.

I get an Object that has a 200 statusCode.

I must not be writing middle ware properly, as I’ve check the source code and passing in an Object with just a code option should work:

Here’s the source code snippet in question:


#9

That looks like it should work. Did you try a minimal reproduction? You might be missing something trivial.


#10

Thanks @serkandurusoy!

I pushed JsonRoutes to npm here: https://github.com/aadamsx/fine-rest where I now use it for a Meteor Web API.

Everything works locally, except like I said I can’t return a darned 401 when I need to – just 200 statusCodes to my clients. This was the case before I migrated the code to NPM too by the way. I can code around this, but I would be nice to find out what I’m doing wrong.

Also, I’m testing out CORS now.

If you get a few cycles, check out the code and tell me what you think (or submit a PR to fix what I’m doing wrong :smiley: )?


Also, @sashko, if you have the time and inclination, please review fine-rest and submit a PR with any corrections or additions it needs please. :smiley:

I was just given the feedback here: meteor/meteor-feature-requests#141 that an HTTP-style API won’t happen in Meteor core, so fine-rest could be an option for some like myself.


#11

You can try the error middleware simple:rest-json-error-handler and just throw Meteor Errors with the proper code throw new Meteor.Error(401, "Not-allowed");

Else, like it says in the code, default status code is 200 so you have to overwrite it with 401 or your own code. The last example you gave only sends a 200 OK response as you don’t overwrite the code. The first should return 401 and you might get or not the body for it (the browser could just stop interpreting after it gets an error code - not sure here i had some weird issues over the time).

Anyway, the idea is that the response might always be undefined when throwing errors, the browser / fetch library might ignore the body in that case.

Just an idea :slight_smile:


#12

This is the example above, here I’m sending in the code 401.

When it enters the sendResults function, it’s named options, it breaks down the options object, it deconstructions the code property. If there’s no code passed it, it defaults to 200.

In the example above it should return code is > 400 and pass that back to the client, instead it’s like it never sees code, and just defaults to 200. But more over it returns nothing if I send in a code. In the case above it will send back undefined. it’s only when I send in the following that I get a code at all on the client:

Yet its a problem witht the code because data does actually work as expected.


#13

This code works for me (on a new meteor app):

Meteor.startup(() => {
  // code to run on server at startup
  JsonRoutes.Middleware.use(function(req, res, next) {

    if(req.query.error) {
      JsonRoutes.sendResult(res, {
        code: 401,
        data: {
          result: "ERROR"
        }
      })
    }
    
    next();
  });

  JsonRoutes.add('GET', '/test-route/', function(req, res, next) {
    JsonRoutes.sendResult(res, {
      data: {
        result: "OK"
      }
    });
  });
});

Called with /test-route?error=true throws a correct 401, with /test-route simple returns 200 OK.

Maybe your issue is from the auth package or some other part of your app. The error package above might catch the error for you and give you a clue.


#14

With the npm package it’s working? Notice how you call next() even after you do a sendRequest, while I only call next() if there is no error? I’ll bet thtat’s the difference – I’ll test that out in my code.

Thanks for laying an extra pair of eyes on this man, much appriciated!


#15

There’s no difference. sendResult stops the code there with res.end so next() is never reached.

Can you link the npm package i can’t find it.


#16

Sure, if you look at the top of the repo here: https://github.com/aadamsx/fine-rest, there’s a link to the npm module. Also you can just do a $ meteor npm install --save fine-rest.


#17

Right, when when I originally did it that way, the code would “hang” if next() was called after sendResult(), so I took next() out of the code when sendResult() is called.


#18

It works with fine-rest too.

If it hanged after calling sendResult then you had an error in sendResult somewhere and res.end() wasn’t reached i’d think and then the flow continued until timeout.


#19

Nice, thank you! I’ll try to figure out the issue.


#20

did you make sure you defined the middleware before defining the routes?