diff --git a/SUMMARY.md b/SUMMARY.md index 198ed249..0b21fb78 100755 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -48,6 +48,7 @@ * [Recipe: Custom JWT Payload](guides/auth/recipe.customize-jwt-payload.md) * [Recipe: Mixed Auth Endpoints](guides/auth/recipe.mixed-auth.md) * [Recipe: Basic OAuth](guides/auth/recipe.oauth-basic.md) + * [Recipe: Custom Auth Strategies](guides/auth/recipe.custom-auth-strategy.md) * [Offline first](guides/offline-first/readme.md) * [Strategies](guides/offline-first/strategies.md) * [Snapshot](guides/offline-first/snapshot.md) diff --git a/guides/auth/readme.md b/guides/auth/readme.md index e010afbc..7ec4c634 100644 --- a/guides/auth/readme.md +++ b/guides/auth/readme.md @@ -23,3 +23,6 @@ Learn how to setup an endpoint so that it handles unauthenticated and authentica [**Auth Recipe: Basic OAuth**](./recipe.oauth-basic.md)
Learn how OAuth (Facebook, Google, GitHub) login works, and how you can use it in your application. + +[**Auth Recipe: Custom Auth Strategy**](./recipe.custom-auth-strategy.md)
+Learn how to setup a completely custom passport based auth stratgies \ No newline at end of file diff --git a/guides/auth/recipe.custom-auth-strategy.md b/guides/auth/recipe.custom-auth-strategy.md new file mode 100644 index 00000000..f91bec36 --- /dev/null +++ b/guides/auth/recipe.custom-auth-strategy.md @@ -0,0 +1,290 @@ +# FeathersJS Auth Recipe: Custom Auth Strategy + +The Auk release of FeathersJS includes a powerful new [authentication suite](../../api/authentication/server.md) built on top of [PassportJS](http://www.passportjs.org/). The new plugins are very flexible, allowing you to customize nearly everything. We can leverage this to create completely custom authentication strategies using [Passport Custom](https://www.npmjs.com/package/passport-custom). Let's take a look at two such examples in this guide. + +## Setting up the basic app +Let's first start by creating a basic server setup. + +```js +const feathers = require('feathers'); +const bodyParser = require('body-parser'); +const hooks = require('feathers-hooks'); +const rest = require('feathers-rest'); +const auth = require('feathers-authentication'); +const jwt = require('feathers-authentication-jwt'); +const memory = require('feathers-memory'); + +const app = feathers(); + +app.configure(hooks()); +app.configure(rest()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.configure(auth({ secret: 'secret' })); +app.configure(jwt()); +app.use('/users', memory()); + +app.hooks({ + before: { + all: [auth.hooks.authenticate('jwt')] + } +}); + +app.listen(8080); +``` + +## Creating a Custom API Key Auth Strategy +The first custom strategy example we can look at is an API Key Strategy. Within it, we'll check if there is a specific header in the request containing a specific API key. If true, we'll successfully authorize the request. + + +First let's make the strategy using [`passport-custom`](https://www.npmjs.com/package/passport-custom) npm package. +```js +const Strategy = require('passport-custom'); + +module.exports = opts => { + return function() { + const verifier = (req, done) => { + + // get the key from the request header supplied in opts + const key = req.params.headers[opts.header]; + + // check if the key is in the allowed keys supplied in opts + const match = opts.allowedKeys.includes(key); + + // user will default to false if no key is present + // and the authorization will fail + const user = match ? 'api' : match; + return done(null, user); + }; + + // register the strategy in the app.passport instance + this.passport.use('apiKey', new Strategy(verifier)); + }; +}; +``` + +Next let's add this to our server setup +```js +const apiKey = require('./apiKey'); + +app.configure( + apiKey({ + // which header to look at + header: 'x-api-key', + // which keys are allowed + allowedKeys: ['opensesame'] + }) +); +``` + +Next let's create a custom authentication hook that conditionally applies auth for all external requests. + +```js +const commonHooks = require('feathers-hooks-common'); + +const authenticate = () => + commonHooks.iff( + // if and only if the request is external + commonHooks.every(commonHooks.isProvider('external')), + commonHooks.iffElse( + // if the specific header is included + ctx => ctx.params.headers['x-api-key'], + // authentication with this strategy + auth.hooks.authenticate('apiKey'), + // else fallback on the jwt strategy + auth.hooks.authenticate(['jwt']) + ) + ); + +app.hooks({ + before: { + all: [authenticate()] + } +}); +``` + +Finally our `server.js` looks like this: +```js +const feathers = require('feathers'); +const bodyParser = require('body-parser'); + +const hooks = require('feathers-hooks'); +const rest = require('feathers-rest'); +const auth = require('feathers-authentication'); +const jwt = require('feathers-authentication-jwt'); +const memory = require('feathers-memory'); +const commonHooks = require('feathers-hooks-common'); + +const apiKey = require('./apiKey'); + +const app = feathers(); +app.configure(hooks()); +app.configure(rest()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.configure(auth({ secret: 'secret' })); +app.configure(jwt()); +app.configure( + apiKey({ + header: 'x-api-key', + allowedKeys: ['opensesame'] + }) +); + +app.use('/users', memory()); + +const authenticate = () => + commonHooks.iff( + commonHooks.every(commonHooks.isProvider('external')), + commonHooks.iffElse( + ctx => ctx.params.headers['x-api-key'], + auth.hooks.authenticate('apiKey'), + auth.hooks.authenticate(['jwt']) + ) + ); + +app.hooks({ + before: { + all: [authenticate()] + } +}); + +app.listen(8080); +``` +Now any request with a header `x-api-key` and the value `opensesame` will be authenticated by the server. + +## Creating an Anonymous User Strategy +The second strategy we'll look at is for an anonymous user. For this specific flow we'll expect the client to call the `/authentication` endpoint letting us know that it wants to authenticate anonymously. The server will then create a new user and return a new JWT token that the client will have to use from that point onwards. + +First let's create the strategy using `passport-custom` +```js +const Strategy = require('passport-custom'); + +module.exports = opts => { + return function() { + const verifier = async (req, done) => { + // create a new user in the user service + // mark this user with a specific anonymous=true attribute + const user = await this.service(opts.userService).create({ + anonymous: true + }); + + // authenticate the request with this user + return done(null, user, { + userId: user.id + }); + }; + + // register the strategy in the app.passport instance + this.passport.use('anonymous', new Strategy(verifier)); + }; +}; +``` + +Next let's update our `server.js` to use this strategy. +```js +const anonymous = require('./anonymous'); + +app.configure( + anonymous({ + // the user service + userService: 'users' + }) +); + +const authenticate = () => + commonHooks.iff( + commonHooks.every(commonHooks.isProvider('external')), + commonHooks.iffElse( + ctx => ctx.params.headers['x-api-key'], + auth.hooks.authenticate('apiKey'), + // add the additional anonymous strategy + auth.hooks.authenticate(['jwt', 'anonymous']) + ) + ); +``` + +Finally our `server.js` looks like this: +```js +const feathers = require('feathers'); +const bodyParser = require('body-parser'); + +const hooks = require('feathers-hooks'); +const rest = require('feathers-rest'); +const auth = require('feathers-authentication'); +const jwt = require('feathers-authentication-jwt'); +const memory = require('feathers-memory'); +const commonHooks = require('feathers-hooks-common'); + +const apiKey = require('./apiKey'); +const anonymous = require('./anonymous'); + +const app = feathers(); +app.configure(hooks()); +app.configure(rest()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.configure(auth({ secret: 'secret' })); +app.configure(jwt()); +app.configure( + apiKey({ + header: 'x-api-key', + allowedKeys: ['opensesame'] + }) +); +app.configure( + anonymous({ + userService: 'users' + }) +); + +app.use('/users', memory()); + +const authenticate = () => + commonHooks.iff( + commonHooks.every(commonHooks.isProvider('external')), + commonHooks.iffElse( + ctx => ctx.params.headers['x-api-key'], + auth.hooks.authenticate('apiKey'), + auth.hooks.authenticate(['jwt', 'anonymous']) + ) + ); + +app.hooks({ + before: { + all: [authenticate()] + } +}); + +app.listen(8080); +``` +Now any such request will return a valid JWT token: +```js +POST /authentication + +{ + strategy: 'anonymous' +} +``` +Note that this looks very similar to a request body for `local` strategy: +```js +POST /authentication + +{ + strategy: 'local', + username: 'admin', + password: 'password' +} +``` + +So for any new strategy we register, we can call the `/authentication` endpoint with a specific body and expect a valid JWT in return, which we can use from thereon. + + +--- + +As we can see it's very easy to create a completely custom auth strategy in a standard passport way using `passport-custom`. + +Happy Hacking!!