Netflix / falcor

A JavaScript library for efficient data fetching
http://netflix.github.io/falcor
Apache License 2.0
10.48k stars 445 forks source link

[best practises] express-jwt made 100% with Falcor [without app.post('/login', authenticate, (req, res) ] #760

Closed przeor closed 8 years ago

przeor commented 8 years ago

Hello,

I am working on an open source starter kit with the login/registration made in React+Redux+Falcor+Express+MongoDB ...

I was looking around for some, but I really didn't find any good example so I want to make one.

Before going further with my question: DO YOU KNOW OF ANY GOOD EXAMPLE? Please let me know.

I want to attach best practises into my starter kit so this is why I have that question.

FRONTEND code:

const falcor = require('falcor');
const FalcorDataSource = require('falcor-http-datasource');
const $ref = falcor.Model.ref;
const $atom = falcor.Model.atom;

let headers;

if(localStorage.token) {
    headers = {
      headers: {
        'Authorization': 'Bearer' + localStorage.token
      }
    };
}

// const model = new FalcorDataSource('/model.json');

const model = new falcor.Model({    
  source: new FalcorDataSource('/model.json', headers)
});

export default model;

Backend Problem

The lack of my experience in Falcor shows on the backend.

I need expose somehow 2 different models on backend so I can have ONE model with some routes auth-required while other non-auth-required: https://gist.github.com/przeor/a66594dfbfd25ef120dc (server.js file - too long to post it here)

Question:

How in one model in Falcor, to expose some endpoints as non-secured (not required valid auth token) like traditional:

app.post('/login', authenticate, (req, res) => {
  let token = jwt.sign({
    username: mockedUser.username
  }, jwtSecret);
  res.send({
    token: token,
    user: mockedUser
  });
});

and other to expose as they require authentication like:

app.get('/api/me', (req, res) => {
  res.send({ user: req.user, works: "yes" });
});

I've already tried to make this:

app.use('/api', expressJwt({secret: jwtSecret}).unless({
  path: [
    // here goes unauthenticated paths from Falcor like:
    'http://localhost:3000/model.json?paths=%5B%5B%22articles%22%2C%22length%22%5D%5D&method=get'
  ]
}));

BUT that above example with using .unless paths doesn't work (don't affect the route as unauthenticated) :-(

How to approach the problem correctly with express-jwt?

A summary: I want to have one Falcor model with some paths that doesn't need valid AUTH Bearer (like [["login"]] or [["articles","length"]] routes for example) while other paths does require them (like [["me"]] or [["articles", "add"]]) :-) ... I want to use 100% pure Falcor's model for login routes and all other that are secured as well. In the current approach I am using https://github.com/mzabriskie/axios on the front-end, but I want get rid off it after you will help me with best practise.

EDIT: P.S. Also I am wondering if maybe I am making any mistake or typo with _app.use('/api', expressJwt({secret: jwtSecret}).unless_ ?

I'm looking forward to your hints.

joshdmiller commented 8 years ago

It's probably best to clarify straightaway that there may not always be a distinction between routes that require authorization and routes that don't (though some applications may make due with this distinction). For example, a blog application may have a recentPostList path that returns the most recent posts. When the user is not authenticated, they are all public posts; when the user is authenticated, they may include drafts, for example.

Authentication properly takes place therefore not within Express, but within the Falcor router. Express could, using middleware, decrypt the JWT and resolve to a user (e.g. req.user), but allow the unauthenticated user by setting the same to null. Your route can either use the user or discard it, as needed.

As far as the creation of the JWT, you can either use a falcor route or an express route (I won't take a position here), and it doesn't change the above. Simply return the token where requested, add it to the header, and decrypt it on incoming requests if it was provided.

For a very early stage example of React+Express+Mongo+Falcor, take a look at StoryShop's frontend and backend.

przeor commented 8 years ago

@joshdmiller Is my understanding correct, that I shall:

1) ommit using express-jwt that works fine with standard express-routing 2) instead of using express-jwt, then I shall implement my own way of checking if a user has access to a certain api endpoint?

What I am missing in those two above steps?

P.S. thank you for your time

joshdmiller commented 8 years ago

Essentially yes. express-jwt is a very simple wrapper around the core JWT functionality, so the ask is quite small - a couple of lines of code.

The best way to think about it is that express gets you to model.json and the falcor router takes it from there, including authorization.

But again, a user doesn't have access to an endpoint, but to a resource attached to a model path. Think about a path like usersById[{keys:ids}]. There's no clean way to have express authorize that because falcor could batch two users in the same request. The user may have access to one and not the other, but both express and the falcor router receive them at the same time in the same request - and must return both path values back, one with data and one with an error. So the falcor route's methods must handle the authorization. It cannot (easily) be express middleware.

Note: this is based on my usage of falcor. If a member of the team has a different opinion, I'd defer to them.

przeor commented 8 years ago

@joshdmiller interesting - thank you! That helps for my understanding a lot in the initial stage.

I am interested if anyone else has more to input into this conversation, maybe other ideas - if not, then we can close the thread :+1:

przeor commented 8 years ago

@joshdmiller thank you for your open source project! IT IS GREAT !

In the same time, I didn't find in your front-end app any login feature? Or I didn't spot it correctly in the codebase?

@joshdmiller could you take a look on that issue, maybe you will have a better idea what is best way to approach my problem on the Falcor's frontend here: https://github.com/Netflix/falcor/issues/762

joshdmiller commented 8 years ago

You're welcome! There will be a lot more in the next few weeks as we approach beta. The frontend login stuff was not on master, but it is now. Here's a summary:

On successful logins we fire a Redux action, storing validated tokens in the Redux store (which is synced with local storage). That token is, when present, added to as a header to falcor HTTP requests.

On the server side, we validate the token if it was provided, setting a user prop on the request object à la PassportJS, et al. On falcor routes, we can use that object if present to determine access.

As I mentioned before, this is very much in flux right now and there are some holes and problems we're working to solve - as well as add more robust testing. This should serve more as inspiration in the short term. But we'd like the project to be helpful to others using this tech - we've certainly learned a lot building it.

przeor commented 8 years ago

@joshdmiller thank you for all your help. I am closing the issue as I believe all the help required has been provided. Thank you all again!