Currently, attaching third-party authentication providers requires a fairly deep understanding of the server internals. The goals of this proposal are as follows:
const strategy = horizon.auth.strategy(<provider>) // -> Strategy
horizon.auth.use(strategy(<options>))
provider: name of the already registered provider (ex. twitter) or an object containing the following properties:name: name of custom provider.protocol: oauth | oauth2auth: authorization endpoint URI.token: access token endpoint URI.temporary: temporary credentials endpoint URI (oauth only).scope: an array of scope strings (oauth2 only).headers: an object containing any additional headers required by the provider.profile: an optional function used to deserialize/normalize user profile information.function(credentials, params, get)credentials: OAuth credentials objectparams: the parsed information received from the provider.get: a helper function to make authenticated requests using the credentials.function(uri, params)uri: requested resource URI.params: any query parameters that should be attached to the request.options: an object containing the following properties (may vary depending on provider.protocol):consumer_key: OAuth consumer key.consumer_secret: OAuth consumer secret.With this API in mind, implementing the Twitter OAuth provider from scratch becomes as simple as:
// create new strategy
const twitter = horizon.auth.strategy({
name: 'twitter',
protocol: 'oauth',
temporary: 'https://api.twitter.com/oauth/request_token',
auth: 'https://api.twitter.com/oauth/authenticate',
token: 'https://api.twitter.com/oauth/access_token',
profile: (credentials, params, get) =>
get('https://api.twitter.com/1.1/account/verify_credentials.json').then(res => res.id)
});
// use this strategy
horizon.auth.use(twitter({
consumer_key: '...',
consumer_secret: '...'
}))
This looks great for oauth/oauth2, but what about custom authentication? From what I can tell, it looks as though currently each auth provider module really boils down to generating a user + redirecting with a JWT.
This could be be more or less enforced through documentation / interfaces, and with the addition of a way to register custom auth modules, we could effectively implement any kind of authentication.
Some pseudo code to illustrate:
// custom auth module
function customAuth(horizon, raw_options) {
// do custom auth
// ...
// now generate token
horizon._auth.generate(provider, user_id)
// return jwt
}
module.exports = customAuth
// horizon server setup
const customAuth = require('../auth/custom');
const horizon_server = horizon(http_server, options);
horizon_server.registerAuthModule(customAuth);
Im with @ericb
I tried this out a little today and got pretty far by adding a new authType called 'custom' in the client and in the handshake on the server, note that i only use custom endpoints.
The above example looks like a good way of solving it.
Is there any other plans for this yet?
If not i can give it a try and you can see if there is something you want to keep from that.
I will need some kind of custom auth anyway.
Ping @deontologician
This is medium term, so not necessarily the next release. Before taking a crack at it @stellanhaglund, we might want to get a comment from @Tryneus on the api design. My understanding of the auth classes right now is fuzzy, so I can't provide too much useful commentary.
@marshall007 Is this in addition to having a supported set of OAuth providers? I like this API but I think it's too much boilerplate for developers who want to just add Google, Github, Twitter, etc to their application.
@dalanmiller yea so the idea is that we would use this API internally to pre-configure the default set of supported auth strategies. You would only have to use the horizon.auth.strategy() API if you needed support for something else.
Server would initialize built-in strategies automatically:
horizon.auth.twitter = horizon.auth.strategy(...)
horizon.auth.github = horizon.auth.strategy(...)
// ...
Which get consumed by the user in one of the following ways:
## 1 - config
[auth.github]
id = "your_client_id"
secret = "your_client_secret"
// 2 - programmatic - built-in
horizon.auth.use(horizon.auth.twitter({ ... }))
// 3 - programmatic - custom
const SlackStrategy = horizon.auth.strategy({ name: 'slack', ... })
horizon.auth.use(SlackStrategy({ ... }))
Also note that while I only illustrated options specifically related to OAuth strategies, the idea is that protocol could be a variety of things (anonymous, unauthenticated, http-basic, etc) and hopefully this API would allow you to configure arbitrary authentication workflows. I just haven't had time to think through all that yet.
here is a pr for @ericb's suggestion.
Right now it goes completely outside the default auth, allowing you to do whatever you want starting from the handshake this is beacause I want to use other than the internal tables for my users, but you could use those if you wanted to.
I think #64 is going to make this easier.
Most helpful comment
@dalanmiller yea so the idea is that we would use this API internally to pre-configure the default set of supported auth strategies. You would only have to use the
horizon.auth.strategy()API if you needed support for something else.Server would initialize built-in strategies automatically:
Which get consumed by the user in one of the following ways: