rethinkdb / horizon

Horizon is a realtime, open-source backend for JavaScript apps.
MIT License
6.78k stars 351 forks source link

Spec out and build an authentication API #2

Closed coffeemug closed 8 years ago

coffeemug commented 8 years ago

TBD

deontologician commented 8 years ago

Here's a brain dump of stuff I've been looking at and thinking about wrt authentication. Authentication is pretty useless with out authorization, and there are couple places where I have to mention authorization stubs since there's some chicken-and-egg problems. For instance, a notion of an admin user makes no sense without specifying what authorization that entails, but it's pretty obvious we need to talk about the admin user here

Creating the authentication machinery

This is something the user should eventually be able to do through a web ui. It's fairly simple, but managing keys etc is tedious and not really a programmatic task. That being said, there still needs to be an api for it since the webui should build on top of fusion.

The pattern with the administrative commands is they're less recombinable, and more individual commands (even if there is some similar code/protocol underneath). So lots of different methods that do one specific thing, but don't need to do .value or .subscribe, they just return the promise directly.

Creating an admin connection

Administration connections should probably be on an entirely different port, with a different connection. I suggest 8182.

const Fusion = require('fusion')
let admin = Fusion.adminConnection('localhost:8182')

The admin connection would only connect without credentials if there is not already a users table. On subsequent connections it would need to authenticate as the admin user, or with a jwt that allows for administrative actions to be performed (this is getting into the authorization side).

Creating the admin user

admin.initializeUsersTable({
    'admin_password': 'plaintext password',
    'auth_type': 'password',
    'scrypt_cost': <optional cost specified by the user>,
})

This will succeed if there is no users table and admin user, and fail otherwise.

For auth_type: password: The server should hash the password with scrypt with the given cost parameters if given and store it in the database.

For auth_type: OAuth, I mention below what the user needs to provide to create the admin user.

Authentication methods

Password

This is the equivalent of basic auth in http, and firebase's password based auth. It needs to be over a secure connection

Handshake:

Notes: the hash needs to be done in the browser using something like scrypt-async to avoid easily DOSing the server. This should happen in a web worker to avoid freezing the UI thread.

Also scrypt requires a salt. Since storing the salt on the server and sending it to the user requires another round-trip, and salts are basically irrelevant with an algorithm like scrypt, I suggest we just sha1 hash the username, or whatever's easiest and doesn't require talking to the server to determine the salt.

OAuth 2.0

I don't think we should bother implementing an OAuth provider ourselves, just clients for other providers (google, facebook, twitter etc). The consensus seems to be that OAuth 2 is inferior security-wise to OAuth 1.0a, but all the major players have moved towards it and Google has deprecated their 1.0a identity api, so there's not a huge number of options unless we want to support SAML (which we totally shouldn't).

I don't think we should allow mixing auth types (password and non-password), but we should probably allow mixing auth providers for OAuth.

admin.initializeUserTable({
  oauth2: {
    google: { api_key: 'XXX', api_secret: 'XXX'},
    twitter: { api_key: 'XXX', 'api_secret: 'YYY'},
  },
  admin_credentials: {
    ... // whatever's needed by oauth client library to authenticate
  }
})

Handshake:

This is a part I haven't fleshed out yet. It should be possible to pass a jwt in the handshake to get access if you've already authenticated recently. Alternately, you can trigger a fresh authentication with the provider. We should probably return a function that does the OAuth redirect if necessary, so it's dead simple to do, but doesn't fire by itself. We may also need an oauth landing page for the redirect (i.e. a non-websocket endpoint that can receive data from the OAuth provider). There might also be ways around that

New user registration

This doesn't need to be on the admin connection. We should allow registration to occur in an anonymous handshake on a normal fusion connection.

Other uses for the admin connection

I think the admin connection will get more use with authorization, where the admin user can set up permission types, etc and set default permissions for new users.

danielmewes commented 8 years ago

The password-based auth should probably use a challenge-response protocol. @VeXocide would you mind taking a quick look at this?

Regarding the admin connection: Is it necessary to have a separate port, and to allow admin configuration from the browser? I haven't thought about this too much, but maybe we could just initialize some fusion configuration tables, which can be easily modified through ReQL? We could provide additional bootstrapping scripts for the backend so users wouldn't need to learn ReQL for a basic setup. They could just run the script configure-auth on the server.

A problem with writing directly to a RethinkDB table through ReQL could be that the passwords need to be hashed ans salted. That makes it a bit harder to do. Our script could take care of that though.

deontologician commented 8 years ago

So the main reason I was thinking a separate port was to make it a very clear line between the connection types, and make it easier to have different rules for exposing the port externally. Alternately, we could have a different kind of handshake for admin connections.

The goal is to make them feel very different because the admin connection is for configuring stuff, which should take place once (or very seldomly and deliberately through the life of an app). It should be hard to not be sure if you're doing an admin operation. In fact, I'm now thinking it would be better to call it a "configuration connection". In practice, they would only use this connection through the webui, and we could optionally provide a commandline interface that exposes the same operations.

danielmewes commented 8 years ago

Yeah I'm on board with keeping that sort of configuration separate.

I'd consider having it not even be part of the browser-side Fusion library though. It could be a separate library that connects directly to the server's ReQL port and/or HTTP-based ReQL port (web UI port). It would just run normal ReQL queries directly to achieve the configuration, rather than going through a special Fusion protocol.

This also somewhat depends on how we want to integrate Fusion with the web UI. Apart from adding modules to the current web UI to configure Fusion, it might also be feasible to have a completely separate Fusion web UI, that is provided by the Fusion backend server.

Maybe we should start out with just having a configure-fusion script that you have to run on the RethinkDB / Fusion server machine, and that performs the configuration through ReQL? We'll probably want to integrate that a bit better before we ship, but I think it's a bit early to plan that in detail since we don't even know yet how users will configure permissions, schemas, or the production mode in general, do we?

deontologician commented 8 years ago

Oh yeah, I should call it something else, maybe the config ui. This would just be a separate app entirely. I spoke briefly with @coffeemug and he indicated it might be nice to have that ui just be a normal fusion app, which could show off its capabilities etc. That would be the only reason to have it use the fusion interface.

If we don't build the ui with fusion, it's less important that the config/admin operations be in the client library.

Maybe we should start out with just having a configure-fusion script that you have to run on the RethinkDB / Fusion server machine

I think this is good to have in addition, since ops people will eventually want to automate this stuff, but I think the ui for configuration is necessary. It's much more user friendly, and the target audience are people who want to avoid backend/ops work and just do frontend coding. In fact, I'd suggest having a ui for config a blocking issue for the first release.

marshall007 commented 8 years ago

I'd consider having it not even be part of the browser-side Fusion library though.

I completely agree, I don't see a use-case for having any of that stuff in the client API and I almost feel like it would be confusing if it were.

Maybe we should start out with just having a configure-fusion script that you have to run on the RethinkDB / Fusion server machine, and that performs the configuration through ReQL?

I can appreciate why you guys want to ship a standalone fusion server binary that you can run directly and that probably makes a lot of since for on-boarding new users. That being said, aside from the initial playing around phase, I think the preferred way to run/configure Fusion in production would be by running your own Node server (i.e. one of the options from #24).

I think the authentication and web UI stuff are perfect examples of why the middleware-based approach makes sense for customizing server behavior. This seems a lot easier to reason about and understand than trying to configure an already running Fusion server instance. In addition to being more powerful and extremely customizable for people that want that.

var fusion = require('fusion-server')();

fusion.use(require('fusion-web-ui')(/* options */));
fusion.use(require('fusion-oauth')({
  google: { api_key: '...', secret: '...'},
  twitter: { api_key: '...', secret: '...'},
}));

// ...

fusion.listen(<port>);
deontologician commented 8 years ago

I guess a big question is "How much do we want to provide the seamless firebase experience?"

The approaches mentioned so far:

  1. Config is a part of the api, a'la firebase. This is the frontend-only route
  2. Config is how you set up your middleware. We generate a small, reasonable default file to get started quickly, but basically every user is going to need to do at least some backend config.

Option 1 maybe doesn't make as much sense for us given that the user always has control over the server, whereas in firebase the actual backend stack is opaque, so they need to build these abstractions over config just to allow certain things to be possible. In our case, we can make configuration more convenient, but not having a nice interface isn't a showstopper since they control the backend as well and can hack together whatever they want.

Option 2 is similar to boilerplate generators like Yeoman. They give you a skeleton, but you are always in control. A con for this approach is that people may not feel comfortable modifying files they didn't create. In practice "theoretically you can do whatever you want on the backend, we don't have an interface for you" may lead some people to just use firebase instead.

danielmewes commented 8 years ago

For the record: I don't think we should not provide a nice graphical configuration interface. In fact I agree that we should make getting started with the configuration extremely easy and intuitive to developers before we ship.

However I think the process for how to do that needs some extra thought once we know how all the other configuration (e.g. configuring schemas etc.) is going to work. I think it's better to get things working without worrying about the configuration first, and then designing something well integrated for our users separately.

Does that sound reasonable?

deontologician commented 8 years ago

I think the process for how to do that needs some extra thought once we know how all the other configuration (e.g. configuring schemas etc.) is going to work.

I agree. I'm not tied to the proposal above, I'm just looking for something concrete to propose. There are a lot of circular design dependencies between the different parts of fusion, and one of them had to be created ex nihilo.

So you're suggesting moving mention of the config end of things out of this issue?

danielmewes commented 8 years ago

So you're suggesting moving mention of the config end of things out of this issue?

That would be my proposal. But I might easily be off about this, since I haven't been following Fusion development super closely. This could also well be the right time to get something concrete going.

nstadigs commented 8 years ago

What about integrating Passport? https://www.npmjs.com/package/passport. It's already widely used in the node community and there are tons (300+) of authentication strategies for already built for it: http://passportjs.org/

deontologician commented 8 years ago

@nbostrom we discussed passport in #64 The long and the short of it is that it ended up being too hard to integrate into horizon given that we don't want to lock the user into express or hapi etc.

nstadigs commented 8 years ago

@deontologician Ah, I see. Sounds like a sane decision. Great to hear that you are considering all options!

deontologician commented 8 years ago

I think this is actually done now. There are some pieces still in flux, but the main component is done