├── .editorconfig ├── .eslintrc.yml ├── .github ├── contributing.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── migrating.md └── new-1.0-features.md ├── lib ├── express │ ├── authenticate.js │ ├── emit-events.js │ ├── expose-cookies.js │ ├── expose-headers.js │ ├── failure-redirect.js │ ├── index.js │ ├── set-cookie.js │ └── success-redirect.js ├── hooks │ ├── authenticate.js │ └── index.js ├── index.js ├── options.js ├── passport │ ├── authenticate.js │ ├── index.js │ └── initialize.js ├── service.js ├── socket │ ├── handler.js │ ├── index.js │ └── update-entity.js └── utils.js ├── mocha.opts ├── package-lock.json ├── package.json └── test ├── express ├── authenticate.test.js ├── emit-events.test.js ├── expose-cookies.test.js ├── expose-headers.test.js ├── failure-redirect.test.js ├── index.test.js ├── set-cookie.test.js └── success-redirect.test.js ├── fixtures ├── server.js └── strategy.js ├── hooks ├── authenticate.test.js └── index.test.js ├── index.test.js ├── integration ├── primus.test.js ├── rest.test.js └── socketio.test.js ├── options.test.js ├── passport ├── authenticate.test.js ├── index.test.js └── initialize.test.js ├── service.test.js ├── socket ├── index.test.js └── update-entity.test.js └── utils.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: eslint-config-semistandard -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 62 | 63 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 64 | 65 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 66 | 67 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 72 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | # THIS MODULE HAS BEEN MOVED TO https://github.com/feathersjs/feathers 2 | 3 | Please open any issues there. This repository will be archived once all open issues have been addressed. 4 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # THIS MODULE HAS BEEN MOVED TO https://github.com/feathersjs/feathers 2 | 3 | Please open any issues there. This repository will be archived once all open issues have been addressed. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | dist/ 33 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: ./lib/ 4 | include-all-sources: true 5 | reporting: 6 | print: summary 7 | reports: 8 | - html 9 | - text 10 | - lcov 11 | watermarks: 12 | statements: [50, 80] 13 | lines: [50, 80] 14 | functions: [50, 80] 15 | branches: [50, 80] 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | .vscode/ 8 | test/ 9 | coverage/ 10 | .github/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - node 5 | - '6' 6 | install: npm install 7 | before_script: 8 | - npm install -g codeclimate-test-reporter 9 | after_script: 10 | - codeclimate-test-reporter < coverage/lcov.info 11 | addons: 12 | code_climate: 13 | repo_token: bfe7eff41c202d315744b1c2309e34018a57329de556b6503f12b91bb8f12fc8 14 | notifications: 15 | email: false 16 | slack: 17 | rooms: 18 | secure: Rhu2a5JGw02NVtLbMC5NdlWk5MQd3pznCuZof9JJ8bOmyfiIvYdruhfnX0sHPwXE3LyLjRFgNCewmxd0NRsMrPV+lfgWXbHlA/gDn18S3GYWpJJhh6zRYk3PVqb4P09UrWC6kQ5tTRAtmerDKYPkDRyI1+IQDOwxOee8CbDN8se4HVaXmCXiq6m52s4uUBJY1s1ZQEOnW+TqHcjXyo3RTtRi7DDw33GO9KBdOsKpyQR2TCaCBaSmXml3BnDcgKuVJFwgb20WOvqpqQRfynYcdHXpo9ta2HRy4dZFMM8MXXGnosg9k4c4RTonuNq4s3/T5doVh79Y9iLjlD/nWMdul3NMzUv7s8s/5D6cPVEvqgox/pZQbI3ZSaa/MS09M3uJ3I8Bj+08XW5HJJ/yWXZyZhcxVcD8MUpee5rC3U1sbPDy9+esgXurLS1GidRkBHlEBIF1qqKRvslrsL/3a6GrPyHPki7gg/N7w4Yej+RawYsSBTbTi+Rurph6hbuMNQUm7/52gRQofsOnf+S4YCXbwbhxH0Ld3BNfMpmOIqMnVtgeBLSmk2XJMDmKGs2/wUUzSK6OZTBu+0SJ+ok9FKEFAoIc4GAGfJcYT3lfdtcaxnBDY4ESP/lYmhxAJJywa0e6Z3ogDV5IHjYArOhF/6ea6slqNiXLERBWopjhFX0WY90= 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @feathersjs/authentication 2 | 3 | > __Important:__ The code for this module has been moved into the main Feathers repository at [feathersjs/feathers](https://github.com/feathersjs/feathers) ([package direct link](https://github.com/feathersjs/feathers/tree/master/packages/authentication)). Please open issues and pull requests there. 4 | 5 | [![Build Status](https://travis-ci.org/feathersjs/authentication.png?branch=master)](https://travis-ci.org/feathersjs/authentication) 6 | 7 | > Add Authentication to your FeathersJS app. 8 | 9 | `@feathersjs/authentication` adds shared [PassportJS](http://passportjs.org/) authentication for Feathers HTTP REST and WebSocket transports using [JSON Web Tokens](http://jwt.io/). 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install @feathersjs/authentication --save 15 | ``` 16 | 17 | ## Quick example 18 | 19 | ```js 20 | const feathers = require('@feathersjs/feathers'); 21 | const express = require('@feathersjs/express'); 22 | const socketio = require('@feathersjs/socketio'); 23 | const auth = require('@feathersjs/authentication'); 24 | const local = require('@feathersjs/authentication-local'); 25 | const jwt = require('@feathersjs/authentication-jwt'); 26 | const memory = require('feathers-memory'); 27 | 28 | const app = express(feathers()); 29 | app.configure(express.rest()) 30 | .configure(socketio()) 31 | .use(express.json()) 32 | .use(express.urlencoded({ extended: true })) 33 | .configure(auth({ secret: 'supersecret' })) 34 | .configure(local()) 35 | .configure(jwt()) 36 | .use('/users', memory()) 37 | .use('/', express.static(__dirname + '/public')) 38 | .use(express.errorHandler()); 39 | 40 | app.service('users').hooks({ 41 | // Make sure `password` never gets sent to the client 42 | after: local.hooks.protect('password') 43 | }); 44 | 45 | app.service('authentication').hooks({ 46 | before: { 47 | create: [ 48 | // You can chain multiple strategies 49 | auth.hooks.authenticate(['jwt', 'local']) 50 | ], 51 | remove: [ 52 | auth.hooks.authenticate('jwt') 53 | ] 54 | } 55 | }); 56 | 57 | // Add a hook to the user service that automatically replaces 58 | // the password with a hash of the password before saving it. 59 | app.service('users').hooks({ 60 | before: { 61 | find: [ 62 | auth.hooks.authenticate('jwt') 63 | ], 64 | create: [ 65 | local.hooks.hashPassword({ passwordField: 'password' }) 66 | ] 67 | } 68 | }); 69 | 70 | const port = 3030; 71 | let server = app.listen(port); 72 | server.on('listening', function() { 73 | console.log(`Feathers application started on localhost:${port}`); 74 | }); 75 | ``` 76 | 77 | ## Documentation 78 | 79 | Please refer to the [@feathersjs/authentication API documentation](https://docs.feathersjs.com/api/authentication/server.html) for more details. 80 | 81 | ## License 82 | 83 | Copyright (c) 2018 84 | 85 | Licensed under the [MIT license](LICENSE). 86 | -------------------------------------------------------------------------------- /docs/migrating.md: -------------------------------------------------------------------------------- 1 | # Migrating to 1.0 2 | 3 | Feathers authentication has had a major overhaul in order to bring some much needed functionality, customization, and scalability going forward while also making it less complex. It is now simply an adapter over top of [Passport](http://passportjs.org/). 4 | 5 | After usage by ourselves and others we realized that there were some limitations in the previous architecture. These new changes allow for some pretty awesome functionality and flexibility that are outlined in [New 1.0 Features](./new-1.0-features.md). 6 | 7 | We've also decoupled the authentication strategies and permissions from the core authentication. While many apps need these, not **every** app does. This has also allowed us to better test each piece in isolation. 8 | 9 | They are now located here: 10 | 11 | - [feathers-authentication-client](https://github.com/feathersjs/authentication-client) 12 | - [feathers-authentication-local](https://github.com/feathersjs/authentication-local) 13 | - [feathers-authentication-jwt](https://github.com/feathersjs/authentication-jwt) 14 | - [feathers-authentication-oauth1](https://github.com/feathersjs/authentication-oauth1) 15 | - [feathers-authentication-oauth2](https://github.com/feathersjs/authentication-oauth2) 16 | - [feathers-authentication-hooks](https://github.com/feathersjs/authentication-hooks) 17 | - [feathers-permissions](https://github.com/feathersjs/feathers-permissions) **(experimental)** 18 | 19 | For most of you, migrating your app should be fairly straight forward as there are only a couple breaking changes to the public interface. 20 | 21 | --- 22 | 23 | # Breaking Changes 24 | 25 | ## Setting up authentication on the server 26 | 27 | **The Old Way (< v0.8.0)** 28 | 29 | ```js 30 | // @feathersjs/authentication < v0.8.0 31 | 32 | // In your config files 33 | { 34 | "auth": { 35 | "token": { 36 | "secret": "xxxx" 37 | }, 38 | "local": {}, 39 | "facebook": { 40 | "clientID": "", 41 | "clientSecret": "", 42 | "permissions": { 43 | "scope": ["public_profile","email"] 44 | } 45 | } 46 | } 47 | } 48 | 49 | // In your authentication service 50 | const authentication = require('@feathersjs/authentication'); 51 | const FacebookStrategy = require('passport-facebook').Strategy; 52 | 53 | let config = app.get('authentication'); 54 | config.facebook.strategy = FacebookStrategy; 55 | app.configure(authentication(config)) 56 | .use('/users', memory()) // this use to be okay to be anywhere 57 | ``` 58 | 59 | **The New Way** 60 | 61 | ```js 62 | // @feathersjs/authentication >= v1.0.0 63 | 64 | // In your config files 65 | { 66 | "auth": { 67 | "secret": "xxxx" 68 | "facebook": { 69 | "clientID": "", 70 | "clientSecret": "", 71 | "scope": ["public_profile","email"] 72 | } 73 | } 74 | } 75 | 76 | // In your app or authentication service, wherever you would like 77 | const auth = require('@feathersjs/authentication'); 78 | const local = require('feathers-authentication-local'); 79 | const jwt = require('feathers-authentication-jwt'); 80 | const oauth1 = require('feathers-authentication-oauth1'); 81 | const oauth2 = require('feathers-authentication-oauth2'); 82 | const FacebookStrategy = require('passport-facebook').Strategy; 83 | 84 | // The services you are setting the `entity` param for need to be registered before authentication 85 | app.configure(auth(app.get('authentication'))) 86 | .configure(jwt()) 87 | .configure(local()) 88 | .configure(oauth1()) 89 | .configure(oauth2({ 90 | name: 'facebook', // if the name differs from your config key you need to pass your config options explicitly 91 | Strategy: FacebookStrategy 92 | })) 93 | .use('/users', memory()); 94 | 95 | // Authenticate the user using the a JWT or 96 | // email/password strategy and if successful 97 | // return a new JWT access token. 98 | app.service('authentication').hooks({ 99 | before: { 100 | create: [ 101 | auth.hooks.authenticate(['jwt', 'local']) 102 | ] 103 | } 104 | }); 105 | ``` 106 | 107 | ### Config Options 108 | 109 | There are a number of breaking changes since the services have been removed: 110 | 111 | - Change `auth.token` -> `auth.jwt` in your config 112 | - Move `auth.token.secret` -> `auth.secret` 113 | - `auth.token.payload` option has been removed. See [customizing JWT payload](#customizing-jwt-payload) for how to do this. 114 | - `auth.idField` has been removed. It is now included in all services so we can pull it internally without you needing to specify it. 115 | - `auth.shouldSetupSuccessRoute` has been removed. Success redirect middleware is registered automatically but only triggers if you explicitly set a redirect. [See redirecting]() for more details. 116 | - `auth.shouldSetupFailureRoute` has been removed. Failure redirect middleware is registered automatically but only triggers if you explicitly set a redirect. [See redirecting]() for more details. 117 | - `auth.tokenEndpoint` has been removed. There isn't a token service anymore. 118 | - `auth.localEndpoint` has been removed. There isn't a local service anymore. It is a passport plugin and has turned into `feathers-authentication-local`. 119 | - `auth.userEndpoint` has been removed. It is now part of `feathers-authentication-local` and is `auth.local.service`. 120 | - Cookies are now disabled by default. If you need cookie support (ie. OAuth, Server Side Rendering, Redirection) then you need to explicitly enable it by setting `auth.cookie.enabled = true`. 121 | - When setting up an OAuth strategy it used to be `strategy: FacebookStrategy` and is now capitalized `Strategy: FacebookStrategy`. 122 | - Any passport strategy options are flattened. So previously you would have had this in your config: 123 | 124 | ```json 125 | { 126 | "auth": { 127 | "facebook": { 128 | "clientID": "", 129 | "clientSecret": "", 130 | "permissions": { 131 | "scope": ["public_profile","email"] 132 | } 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | and now you have: 139 | 140 | ```json 141 | { 142 | "auth": { 143 | "facebook": { 144 | "clientID": "", 145 | "clientSecret": "", 146 | "scope": ["public_profile","email"] 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | ## Setting up authentication on the client 153 | 154 | Authenticating through the Feathers client is almost exactly the same with just a few keys changes: 155 | 156 | - `type` is now `strategy` when calling `authenticate()` and must be an exact name match of one of your strategies registered server side. 157 | - You must fetch your user explicitly (typically after authentication succeeds) 158 | - You require `feathers-authentication-client` instead of `@feathersjs/authentication/client` 159 | 160 | You can use `feathers-authentication-compatibility` on the server to keep the old client functional, this helps to migrate large scale deployments where you can not update all clients/api consumers before migrating to `>=1.0.0` Check https://www.npmjs.com/package/feathers-authentication-compatibility for more information. 161 | 162 | **The Old Way (< v0.8.0)** 163 | 164 | ```js 165 | // @feathersjs/authentication < v0.8.0 166 | const auth = require('@feathersjs/authentication/client'); 167 | app.configure(auth()); 168 | 169 | app.authenticate({ 170 | type: 'local', 171 | email: 'admin@feathersjs.com', 172 | password: 'admin' 173 | }).then(function(result){ 174 | console.log('Authenticated!', result); 175 | }).catch(function(error){ 176 | console.error('Error authenticating!', error); 177 | }); 178 | ``` 179 | 180 | **The New Way (with `feathers-authentication-client`)** 181 | 182 | ```js 183 | // feathers-authentication-client >= v1.0.0 184 | const auth = require('feathers-authentication-client'); 185 | app.configure(auth(config)); 186 | 187 | app.authenticate({ 188 | strategy: 'local', 189 | email: 'admin@feathersjs.com', 190 | password: 'admin' 191 | }) 192 | .then(response => { 193 | console.log('Authenticated!', response); 194 | // By this point your accessToken has been stored in 195 | // localstorage 196 | return app.passport.verifyJWT(response.accessToken); 197 | }) 198 | .then(payload => { 199 | console.log('JWT Payload', payload); 200 | return app.service('users').get(payload.userId); 201 | }) 202 | .then(user => { 203 | app.set('user', user); 204 | console.log('User', app.get('user')); 205 | // Do whatever you want now 206 | }) 207 | .catch(function(error){ 208 | console.error('Error authenticating!', error); 209 | }); 210 | ``` 211 | 212 | ### Config Options 213 | 214 | - `localEndpoint` has been removed. There is just one endpoint called `service` which defaults to `/authentication`. 215 | - `tokenEndpoint` has been removed. There is just one endpoint called `service` which defaults to `/authentication`. 216 | - `tokenKey` -> `accessTokenKey` 217 | 218 | 219 | ## Response to `app.authenticate()` does not return `user` 220 | 221 | We previously made the poor assumption that you are always authenticating a user. This is not always the case, or your app may not care about the current user as you already have their id in the accessToken payload or can encode some additional details in the JWT accessToken. Therefore, if you need to get the current user you need to request it explicitly after authentication or populate it yourself in an after hook server side. See the new usage above for how to fetch your user. 222 | 223 | ## Customizing JWT Payload 224 | 225 | By default the payload for your JWT is simply your entity id (ie. `{ userId }`). However, you can customize your JWT payloads however you wish by adding a `before` hook to the authentication service. For example: 226 | 227 | ```js 228 | // This hook customizes your payload. 229 | function customizeJWTPayload() { 230 | return function(hook) { 231 | console.log('Customizing JWT Payload'); 232 | hook.params.payload = { 233 | // You need to make sure you have the right id. 234 | // You can put whatever you want to be encoded in 235 | // the JWT access token. 236 | customId: hook.params.user.id 237 | }; 238 | 239 | return Promise.resolve(hook); 240 | }; 241 | } 242 | 243 | // Authenticate the user using the a JWT or 244 | // email/password strategy and if successful 245 | // return a new JWT access token. 246 | app.service('authentication').hooks({ 247 | before: { 248 | create: [ 249 | auth.hooks.authenticate(['jwt', 'local']), 250 | customizeJWTPayload() 251 | ] 252 | } 253 | }); 254 | ``` 255 | 256 | ## JWT Parsing 257 | 258 | The JWT is only parsed from the header and body by default now. It is no longer pulled from the query string unless you explicitly tell `feathers-authentication-jwt` to do so. 259 | 260 | You can customize the header and body keys like so: 261 | 262 | ```js 263 | app.configure(authentication({ 264 | header: 'custom', 265 | bodyKey: 'custom' 266 | })); 267 | ``` 268 | 269 | If you want to customize things further you can refer to the [`feathers-authentication-jwt`](https://github.com/feathersjs/authentication-jwt) module or implement your own custom passport JWT strategy. 270 | 271 | ## Hook Changes 272 | 273 | ### Hooks always return promises 274 | 275 | This shouldn't really affect you unless you are testing, modifying or wrapping existing hooks but they **always** return promises now. This makes the interface more consistent, making it easier to test and reason as to what a hook does. 276 | 277 | ### Added Hooks 278 | 279 | `auth.hooks.authenticate([...strategies])`: This hook takes the place of the `verifyToken` and `populateUser` hooks. 280 | 281 | For the JWT strategy, this hook has different behavior from the old hooks: it will return a `401 Unauthorized` error if no JWT is present in the request. Include an anonymous JWT (a JWT with no associated user) to prevent a `401` response. Anonymous JWT's can created through by externally calling the `create` endpoint of the authentication service. Internally, they can be created through `app.service('authentication').create({})`. 282 | 283 | ### Removed Hooks 284 | 285 | We have removed all of the old authentication hooks. If you still need these they have been moved to the [feathers-authentication-hooks](https://github.com/feathersjs/authentication-hooks) repo and some of them have been deprecated. 286 | 287 | The following hooks have been removed: 288 | 289 | - `verifyOrRestrict` -> use new `feathers-permissions` plugin 290 | - `populateOrRestrict` -> use new `feathers-permissions` plugin 291 | - `hashPassword` -> has been moved to `feathers-authentication-local` **This is important.** 292 | - `populateUser` -> use new `populate` hook in `feathers-hooks-common` 293 | - `verifyToken` -> use new `feathers-authentication-jwt` plugin to easily validate a JWT access token. You can also now call `app.passport.verifyJWT` anywhere in your app to do it explicitly. 294 | 295 | 296 | **The Old Way (< v0.8.0)** 297 | 298 | Typically you saw a lot of this in your hook definitions for a service: 299 | 300 | ```js 301 | // @feathersjs/authentication < v0.8.0 302 | // Users service 303 | const auth = require('@feathersjs/authentication').hooks; 304 | exports.before = { 305 | all: [], 306 | find: [ 307 | auth.verifyToken(), 308 | auth.populateUser(), 309 | auth.restrictToAuthenticated(), 310 | auth.queryWithCurrentUser() 311 | ], 312 | get: [ 313 | auth.verifyToken(), 314 | auth.populateUser(), 315 | auth.restrictToAuthenticated(), 316 | auth.restrictToOwner({ ownerField: '_id' }) 317 | ], 318 | create: [ 319 | auth.hashPassword() 320 | ], 321 | update: [ 322 | auth.verifyToken(), 323 | auth.populateUser(), 324 | auth.restrictToAuthenticated(), 325 | auth.hashPassword() 326 | ], 327 | patch: [ 328 | auth.verifyToken(), 329 | auth.populateUser(), 330 | auth.restrictToAuthenticated(), 331 | auth.hashPassword() 332 | ], 333 | } 334 | ``` 335 | 336 | **The New Way** 337 | 338 | ```js 339 | // @feathersjs/authentication >= v1.0.0 340 | const auth = require('@feathersjs/authentication'); 341 | const local = require('feathers-authentication-local'); 342 | const { 343 | queryWithCurrentUser, 344 | restrictToOwner 345 | } = require('feathers-authentication-hooks'); 346 | 347 | exports.before = { 348 | all: [], 349 | find: [ 350 | auth.hooks.authenticate('jwt'), 351 | queryWithCurrentUser() 352 | ], 353 | get: [ 354 | auth.hooks.authenticate('jwt'), 355 | restrictToOwner({ ownerField: '_id' }) 356 | ], 357 | create: [ 358 | local.hooks.hashPassword() 359 | ], 360 | update: [ 361 | auth.hooks.authenticate('jwt'), 362 | restrictToOwner({ ownerField: '_id' }), 363 | local.hooks.hashPassword() 364 | ], 365 | patch: [ 366 | auth.hooks.authenticate('jwt'), 367 | restrictToOwner({ ownerField: '_id' }), 368 | local.hooks.hashPassword() 369 | ], 370 | } 371 | ``` 372 | -------------------------------------------------------------------------------- /docs/new-1.0-features.md: -------------------------------------------------------------------------------- 1 | # New 1.0 Features 2 | 3 | ## New Config Options 4 | 5 | - `entity` - the global key name to assign data returned from a strategy to. Defaults to `user`. Which becomes `req.user` in middleware, `hook.params.user` in hooks and `socket.user` for websockets. 6 | 7 | You can override this property for each passport authentication strategy like so: 8 | 9 | ```js 10 | // when calling from hooks 11 | auth.hooks.authenticate('local', { assignProperty: 'custom' }) 12 | // when calling from middleware 13 | auth.express.authenticate('local', { assignProperty: 'custom' }) 14 | // For sockets and all other methods you can have it in your main 15 | // config or pass explicitly when initializing auth. 16 | app.configure(authentication({ 17 | local: { 18 | assignProperty: 'custom' 19 | } 20 | })) 21 | ``` 22 | 23 | ## More warnings and debugging 24 | 25 | We've added more helpful warning messages and added debug logs for every hook, service, and middleware. We use the [debug]() module so usage is the same. 26 | 27 | #### Turning on all auth logs 28 | You can turn on all auth debug logs by running your app with `DEBUG=@feathersjs/authentication* npm start`. 29 | 30 | #### Turning on logs for a specific type 31 | If you want to only turn on logs for a `hooks`, `express`, `passport` or `service` you can do `DEBUG=@feathersjs/authentication:* npm start`. For example, 32 | 33 | ``` 34 | `DEBUG=@feathersjs/authentication:hooks* npm start` 35 | ``` 36 | 37 | #### Turning on logs for a specific entity 38 | If you want to only turn on logs for a specific hook, middleware or service you can do `DEBUG=@feathersjs/authentication:: npm start`. For example, 39 | 40 | ``` 41 | `DEBUG=@feathersjs/authentication:hooks:authenticate npm start` 42 | ``` 43 | 44 | ## More Flexible Tokens 45 | 46 | Previously JWT's were only used as an access token and could only be created as part of the authentication flow. This is still the default, however it's now possible to create JWTs with different options for all sorts of things like password reset, email verification, magic links, etc. 47 | 48 | On the server side all you need to do is call `app.createJWT(payload, [options])`. By default it will pull from your global auth config but you can pass custom options in order to customize the JWT behaviour. 49 | 50 | Here is an example of how you might generate a temporary password reset JWT: 51 | 52 | ```js 53 | const payload = { 54 | id: user.id 55 | // whatever else you want to put in the token 56 | }; 57 | 58 | const options = { 59 | jwt: { 60 | audience: 'user', 61 | subject: 'password-reset', 62 | expiresIn: '5m' 63 | } 64 | }; 65 | 66 | app.passport.createJWT(payload, options).then(token => { 67 | // Do your thing 68 | }) 69 | .catch(error => { 70 | // Handle errors 71 | }); 72 | ``` 73 | 74 | You can also verify a JWT at any point in your app. You are no longer restricted to using a hook: 75 | 76 | ```js 77 | const options = { 78 | jwt: { 79 | audience: 'user', 80 | subject: 'password-reset', 81 | expiresIn: '5m' 82 | } 83 | }; 84 | 85 | app.passport.verifyJWT(payload, options).then(payload => { 86 | // Do your thing 87 | }) 88 | .catch(error => { 89 | // Handle errors 90 | }); 91 | ``` 92 | 93 | ## Server Side Rendering 94 | 95 | You can now create "Universal" apps or the more old school server side templated apps **with** stateless JWT authentication. In order to support server side rendering the client will now automatically attempt to authenticate if a token is present without you needing to call `app.authenticate` explicitly each time. 96 | 97 | For servers that are using a template engine to render their views server side (ie. Jade, Handlebars, etc) you may not be using client side JS for your authentication. So we now support using your JWT more like a traditional session. It's still stateless but the JWT access token is stored in a cookie that by default expires at the same time as the JWT access token. 98 | 99 | ## Logged In/Logged Out State 100 | It wasn't possible to know accurately when a user was logged in or out on the server side. We've fixed that! 101 | 102 | You can now access the user at any point in your application . In addition, **you no longer need to add the `verifyToken` and `populateUser` hooks to your services** because your entity is already loaded earlier on in the data flow. 103 | 104 | Whenever you successfully authenticate with an authentication strategy the data that gets returned from the authentication strategy is now accessible throughout your entire app. 105 | 106 | ### Using Sockets 107 | 108 | Using sockets you can now listen for the server side `login` and `logout` events across your entire back-end. This is accomplished by doing: 109 | 110 | ```js 111 | app.on('login', function(entity, info) { 112 | console.log('User logged in', entity); 113 | }); 114 | 115 | app.on('logout', function(tokenPayload, info) { 116 | console.log('User logged out', tokenPayload); 117 | }); 118 | 119 | // or on a specific socket 120 | 121 | socket.on('login', function(entity, info) { 122 | console.log('User logged in', entity); 123 | }); 124 | 125 | socket.on('logout', function(tokenPayload, info) { 126 | console.log('User logged out', tokenPayload); 127 | }); 128 | 129 | ``` 130 | 131 | ### In Express Middleware 132 | If you need this information in a custom route or other middleware you can access the currently authenticated entity (ie. user) and whether the request is authenticated by inspecting `req.` (ie. `req.user`) and `req.authenticated`. 133 | 134 | ### In Hooks/Services 135 | If you need this information in a hook or service you can access the current currently authenticated entity (ie. user) and whether they are authenticated by inspecting `hook.params.` (ie. `hook.params.user`) and `hooks.params.authenticated` respectively. 136 | -------------------------------------------------------------------------------- /lib/express/authenticate.js: -------------------------------------------------------------------------------- 1 | const errors = require('@feathersjs/errors'); 2 | const Debug = require('debug'); 3 | const debug = Debug('@feathersjs/authentication:express:authenticate'); 4 | 5 | module.exports = function authenticate (strategy, options = {}) { 6 | // TODO (EK): Support arrays of strategies 7 | 8 | if (!strategy) { 9 | throw new Error(`The 'authenticate' hook requires one of your registered passport strategies.`); 10 | } 11 | 12 | return function (req, res, next) { 13 | // If we are already authenticated skip 14 | if (req.authenticated) { 15 | return next(); 16 | } 17 | 18 | // if (!req.app.passport._strategy(strategy)) { 19 | // return next(new Error(`Your '${strategy}' authentication strategy is not registered with passport.`)); 20 | // } 21 | // TODO (EK): Can we do something in here to get away 22 | // from express-session for OAuth1? 23 | // TODO (EK): Handle chaining multiple strategies 24 | req.app.authenticate(strategy, options)(req).then((result = {}) => { 25 | // TODO (EK): Support passport failureFlash 26 | // TODO (EK): Support passport successFlash 27 | if (result.success) { 28 | Object.assign(req, { authenticated: true }, result.data); 29 | Object.assign(req.feathers, { authenticated: true }, result.data); 30 | 31 | if (options.successRedirect && !options.__oauth) { 32 | debug(`Redirecting to ${options.successRedirect}`); 33 | res.status(302); 34 | return res.redirect(options.successRedirect); 35 | } 36 | 37 | return next(); 38 | } 39 | 40 | if (result.fail) { 41 | if (options.failureRedirect && !options.__oauth) { 42 | debug(`Redirecting to ${options.failureRedirect}`); 43 | res.status(302); 44 | return res.redirect(options.failureRedirect); 45 | } 46 | 47 | const { challenge, status = 401 } = result; 48 | let message = challenge && challenge.message ? challenge.message : challenge; 49 | 50 | if (options.failureMessage) { 51 | message = options.failureMessage; 52 | } 53 | 54 | res.status(status); 55 | return Promise.reject(new errors[status](message, challenge)); 56 | } 57 | 58 | if (result.redirect) { 59 | debug(`Redirecting to ${result.url}`); 60 | res.status(result.status); 61 | return res.redirect(result.url); 62 | } 63 | 64 | // Only gets here if pass() is called by the strategy 65 | next(); 66 | }).catch(next); 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /lib/express/emit-events.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | 3 | const debug = Debug('feathers-authentication:express:emit-events'); 4 | 5 | module.exports = function emitEvents () { 6 | return function (req, res, next) { 7 | const method = res.hook && res.hook.method; 8 | 9 | let event = null; 10 | 11 | if (method === 'remove') { 12 | event = 'logout'; 13 | } else if (method === 'create') { 14 | event = 'login'; 15 | } 16 | 17 | if (res.data && res.data.accessToken && event) { 18 | const { app } = req; 19 | 20 | debug(`Sending '${event}' event for REST provider. Token is`, res.data.accessToken); 21 | 22 | app.emit(event, res.data, { 23 | provider: 'rest', 24 | req, 25 | res 26 | }); 27 | } 28 | 29 | next(); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/express/expose-cookies.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const debug = Debug('feathers-authentication:express:expose-cookies'); 3 | 4 | module.exports = function () { 5 | debug('Registering exposeCookies middleware'); 6 | 7 | return function exposeCookies (req, res, next) { 8 | debug('Exposing Express cookies to hooks and services', req.cookies); 9 | req.feathers.cookies = req.cookies; 10 | next(); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/express/expose-headers.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const debug = Debug('feathers-authentication:express:expose-headers'); 3 | 4 | module.exports = function () { 5 | debug('Registering exposeHeaders middleware'); 6 | 7 | return function exposeHeaders (req, res, next) { 8 | debug('Exposing Express headers to hooks and services'); 9 | req.feathers.headers = req.headers; 10 | next(); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/express/failure-redirect.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const debug = Debug('feathers-authentication:middleware:failure-redirect'); 3 | 4 | module.exports = function failureRedirect (options = {}) { 5 | debug('Registering failureRedirect middleware'); 6 | 7 | return function (error, req, res, next) { 8 | if (options.cookie && options.cookie.enabled) { 9 | debug(`Clearing old '${options.cookie.name}' cookie`); 10 | res.clearCookie(options.cookie.name); 11 | } 12 | 13 | if (res.hook && res.hook.data && res.hook.data.__redirect) { 14 | const { url, status } = res.hook.data.__redirect; 15 | debug(`Redirecting to ${url} after failed authentication.`); 16 | 17 | res.status(status || 302); 18 | return res.redirect(url); 19 | } 20 | 21 | next(error); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/express/index.js: -------------------------------------------------------------------------------- 1 | const setCookie = require('./set-cookie'); 2 | const successRedirect = require('./success-redirect'); 3 | const failureRedirect = require('./failure-redirect'); 4 | const authenticate = require('./authenticate'); 5 | const exposeHeaders = require('./expose-headers'); 6 | const exposeCookies = require('./expose-cookies'); 7 | const emitEvents = require('./emit-events'); 8 | 9 | module.exports = { 10 | exposeHeaders, 11 | exposeCookies, 12 | authenticate, 13 | setCookie, 14 | successRedirect, 15 | failureRedirect, 16 | emitEvents 17 | }; 18 | -------------------------------------------------------------------------------- /lib/express/set-cookie.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const omit = require('lodash.omit'); 3 | const ms = require('ms'); 4 | 5 | const debug = Debug('feathers-authentication:middleware:set-cookie'); 6 | 7 | module.exports = function setCookie (authOptions = {}) { 8 | debug('Registering setCookie middleware'); 9 | 10 | function makeExpiry (timeframe) { 11 | return new Date(Date.now() + ms(timeframe)); 12 | } 13 | 14 | return function (req, res, next) { 15 | const app = req.app; 16 | // Prevent mutating authOptions object 17 | const options = Object.assign({}, authOptions.cookie); 18 | 19 | debug('Running setCookie middleware with options:', options); 20 | 21 | // If cookies are enabled then set it with its options. 22 | if (options.enabled && options.name) { 23 | const cookie = options.name; 24 | 25 | // Don't set the cookie if this was called after removing the token. 26 | if (res.hook && res.hook.method === 'remove') { 27 | return next(); 28 | } 29 | // Only set the cookie if we have a JWT access token. 30 | if (res.data && res.data.accessToken) { 31 | // Clear out any old cookie since we are creating a new one 32 | debug(`Clearing old '${cookie}' cookie`); 33 | res.clearCookie(cookie); 34 | 35 | // Check HTTPS and cookie status in production. 36 | if (!req.secure && app.get('env') === 'production' && options.secure) { 37 | console.warn('WARN: Request isn\'t served through HTTPS: JWT in the cookie is exposed.'); 38 | console.info('If you are behind a proxy (e.g. NGINX) you can:'); 39 | console.info('- trust it: http://expressjs.com/en/guide/behind-proxies.html'); 40 | console.info(`- set cookie['${cookie}'].secure false`); 41 | } 42 | 43 | // If a custom expiry wasn't passed then set the expiration 44 | // to be that of the JWT expiration or the maxAge option if provided. 45 | if (options.expires === undefined) { 46 | if (options.maxAge) { 47 | options.expires = makeExpiry(options.maxAge); 48 | } else if (authOptions.jwt.expiresIn) { 49 | options.expires = makeExpiry(authOptions.jwt.expiresIn); 50 | } 51 | } 52 | 53 | // Ensure that if a custom expiration was passed it is a valid date 54 | if (options.expires && !(options.expires instanceof Date)) { 55 | return next(new Error('cookie.expires must be a valid Date object')); 56 | } 57 | 58 | // remove some of our options that don't apply to express cookie creation 59 | // as well as the maxAge because we have set an explicit expiry. 60 | const cookieOptions = omit(options, 'name', 'enabled', 'maxAge'); 61 | 62 | debug(`Setting '${cookie}' cookie with options`, cookieOptions); 63 | res.cookie(cookie, res.data.accessToken, cookieOptions); 64 | } 65 | } 66 | 67 | next(); 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /lib/express/success-redirect.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const debug = Debug('feathers-authentication:middleware:success-redirect'); 3 | 4 | module.exports = function successRedirect () { 5 | debug('Registering successRedirect middleware'); 6 | 7 | return function (req, res, next) { 8 | if (res.hook && res.hook.data && res.hook.data.__redirect) { 9 | const { url, status } = res.hook.data.__redirect; 10 | debug(`Redirecting to ${url} after successful authentication.`); 11 | 12 | res.status(status || 302); 13 | return res.redirect(url); 14 | } 15 | 16 | next(); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/hooks/authenticate.js: -------------------------------------------------------------------------------- 1 | const errors = require('@feathersjs/errors'); 2 | const Debug = require('debug'); 3 | const merge = require('lodash.merge'); 4 | const { NotAuthenticated } = require('@feathersjs/errors'); 5 | const debug = Debug('@feathersjs/authentication:hooks:authenticate'); 6 | 7 | module.exports = function authenticate (_strategies, options = {}) { 8 | if (!_strategies) { 9 | throw new Error(`The 'authenticate' hook requires one of your registered passport strategies.`); 10 | } 11 | 12 | const strategies = Array.isArray(_strategies) ? _strategies : [ _strategies ]; 13 | 14 | return function (hook) { 15 | const app = hook.app; 16 | 17 | // If called internally or we are already authenticated skip 18 | if (!hook.params.provider || hook.params.authenticated) { 19 | return Promise.resolve(hook); 20 | } 21 | 22 | if (hook.type !== 'before') { 23 | return Promise.reject(new Error(`The 'authenticate' hook should only be used as a 'before' hook.`)); 24 | } 25 | 26 | hook.data = hook.data || {}; 27 | 28 | const strategy = hook.data.strategy || strategies[0]; 29 | 30 | if (strategies.indexOf(strategy) === -1) { 31 | return Promise.reject(new NotAuthenticated(`Strategy ${strategy} is not permitted`)); 32 | } 33 | 34 | // Handle the case where authenticate hook was registered without a passport strategy specified 35 | if (!strategy) { 36 | return Promise.reject(new errors.GeneralError(`You must provide an authentication 'strategy'`)); 37 | } 38 | 39 | // The client must send a `strategy` name. 40 | if (!app.passport._strategy(strategy)) { 41 | return Promise.reject(new errors.BadRequest(`Authentication strategy '${strategy}' is not registered.`)); 42 | } 43 | 44 | // NOTE (EK): Passport expects an express/connect 45 | // like request object. So we need to create one. 46 | let request = { 47 | query: hook.data, 48 | body: hook.data, 49 | params: hook.params, 50 | headers: hook.params.headers || {}, 51 | cookies: hook.params.cookies || {}, 52 | session: {} 53 | }; 54 | 55 | const strategyOptions = merge({}, app.passport.options(strategy), options); 56 | 57 | debug(`Attempting to authenticate using ${strategy} strategy with options`, strategyOptions); 58 | 59 | return app.authenticate(strategy, strategyOptions)(request).then((result = {}) => { 60 | if (result.fail && options.allowUnauthenticated !== true) { 61 | // TODO (EK): Reject with something... 62 | // You get back result.challenge and result.status 63 | if (strategyOptions.failureRedirect) { 64 | // TODO (EK): Bypass the service? 65 | // hook.result = true 66 | Object.defineProperty(hook.data, '__redirect', { value: { status: 302, url: strategyOptions.failureRedirect } }); 67 | } 68 | 69 | const { challenge, status = 401 } = result; 70 | let message = challenge && challenge.message ? challenge.message : challenge; 71 | 72 | if (strategyOptions.failureMessage) { 73 | message = strategyOptions.failureMessage; 74 | } 75 | 76 | return Promise.reject(new errors[status](message, challenge)); 77 | } 78 | 79 | if (result.success || options.allowUnauthenticated === true) { 80 | hook.params = Object.assign({ authenticated: result.success }, hook.params, result.data); 81 | 82 | // Add the user to the original request object so it's available in the socket handler 83 | Object.assign(request.params, hook.params); 84 | 85 | if (strategyOptions.successRedirect) { 86 | // TODO (EK): Bypass the service? 87 | // hook.result = true 88 | Object.defineProperty(hook.data, '__redirect', { value: { status: 302, url: strategyOptions.successRedirect } }); 89 | } 90 | } else if (result.redirect) { 91 | // TODO (EK): Bypass the service? 92 | // hook.result = true 93 | Object.defineProperty(hook.data, '__redirect', { value: { status: result.status, url: result.url } }); 94 | } 95 | 96 | return Promise.resolve(hook); 97 | }); 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /lib/hooks/index.js: -------------------------------------------------------------------------------- 1 | const authenticate = require('./authenticate'); 2 | 3 | module.exports = { 4 | authenticate 5 | }; 6 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const hooks = require('./hooks'); 3 | const express = require('./express'); 4 | const passport = require('passport'); 5 | const adapter = require('./passport'); 6 | const getOptions = require('./options'); 7 | const service = require('./service'); 8 | const socket = require('./socket'); 9 | 10 | const debug = Debug('@feathersjs/authentication:index'); 11 | 12 | function init (config = {}) { 13 | return function authentication () { 14 | const app = this; 15 | const _super = app.setup; 16 | // Merge and flatten options 17 | const options = getOptions(config); 18 | 19 | if (app.passport) { 20 | throw new Error(`You have already registered authentication on this app. You only need to do it once.`); 21 | } 22 | 23 | if (!options.secret) { 24 | throw new Error(`You must provide a 'secret' in your authentication configuration`); 25 | } 26 | 27 | // Make sure cookies don't have to be sent over HTTPS 28 | // when in development or test mode. 29 | if (app.get('env') === 'development' || app.get('env') === 'test') { 30 | options.cookie.secure = false; 31 | } 32 | 33 | app.set('authentication', options); 34 | app.set('auth', options); 35 | 36 | debug('Setting up Passport'); 37 | // Set up our framework adapter 38 | passport.framework(adapter.call(app, options)); 39 | // Expose passport on the app object 40 | app.passport = passport; 41 | // Alias to passport for less keystrokes 42 | app.authenticate = passport.authenticate.bind(passport); 43 | // Expose express request headers to Feathers services and hooks. 44 | app.use(express.exposeHeaders()); 45 | 46 | if (options.cookie.enabled) { 47 | // Expose express cookies to Feathers services and hooks. 48 | debug('Setting up Express exposeCookie middleware'); 49 | app.use(express.exposeCookies()); 50 | } 51 | 52 | // TODO (EK): Support passing your own service or force 53 | // developer to register it themselves. 54 | app.configure(service(options)); 55 | app.passport.initialize(); 56 | 57 | app.setup = function () { 58 | let result = _super.apply(this, arguments); 59 | 60 | // Socket.io middleware 61 | if (app.io) { 62 | debug('registering Socket.io authentication middleware'); 63 | app.io.on('connection', socket.socketio(app, options)); 64 | } 65 | 66 | // Primus middleware 67 | if (app.primus) { 68 | debug('registering Primus authentication middleware'); 69 | app.primus.on('connection', socket.primus(app, options)); 70 | } 71 | 72 | return result; 73 | }; 74 | }; 75 | } 76 | 77 | module.exports = init; 78 | 79 | // Exposed Modules 80 | Object.assign(module.exports, { 81 | default: init, 82 | hooks, 83 | express, 84 | service 85 | }); 86 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash.merge'); 2 | 3 | const defaults = { 4 | path: '/authentication', 5 | header: 'Authorization', 6 | entity: 'user', 7 | service: 'users', 8 | passReqToCallback: true, 9 | session: false, 10 | cookie: { 11 | enabled: false, 12 | name: 'feathers-jwt', 13 | httpOnly: false, 14 | secure: true 15 | }, 16 | jwt: { 17 | header: { typ: 'access' }, // by default is an access token but can be any type 18 | audience: 'https://yourdomain.com', // The resource server where the token is processed 19 | subject: 'anonymous', // Typically the entity id associated with the JWT 20 | issuer: 'feathers', // The issuing server, application or resource 21 | algorithm: 'HS256', 22 | expiresIn: '1d' 23 | } 24 | }; 25 | 26 | module.exports = function (...otherOptions) { 27 | return merge({}, defaults, ...otherOptions); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/passport/authenticate.js: -------------------------------------------------------------------------------- 1 | const makeDebug = require('debug'); 2 | 3 | const debug = makeDebug('@feathersjs/authentication:passport:authenticate'); 4 | 5 | module.exports = function authenticate (options = {}) { 6 | debug('Initializing custom passport authenticate', options); 7 | 8 | // This function is bound by passport and called by passport.authenticate() 9 | return function (passport, strategies, strategyOptions = {}, callback = () => {}) { 10 | // This is called by the feathers middleware, hook or socket. The request object 11 | // is a mock request derived from an http request, socket object, or hook. 12 | return function (request = {}) { 13 | return new Promise((resolve, reject) => { 14 | // TODO (EK): Support transformAuthInfo 15 | 16 | // Allow you to set a location for the success payload. 17 | // Default is hook.params.user, req.user and socket.user. 18 | const entity = strategyOptions.entity || strategyOptions.assignProperty || options.entity; 19 | request.body = request.body || {}; 20 | let strategyName = request.body.strategy; 21 | 22 | if (!strategyName) { 23 | if (Array.isArray(strategies)) { 24 | strategyName = strategies[0]; 25 | } else { 26 | strategyName = strategies; 27 | } 28 | } 29 | 30 | if (!strategyName) { 31 | return reject(new Error(`You must provide an authentication 'strategy'`)); 32 | } 33 | 34 | // Make sure `strategies` is an array, allowing authentication to pass through a chain of 35 | // strategies. The first name to succeed, redirect, or error will halt 36 | // the chain. Authentication failures will proceed through each strategy in 37 | // series, ultimately failing if all strategies fail. 38 | // 39 | // This is typically used on API endpoints to allow clients to authenticate 40 | // using their preferred choice of Basic, Digest, token-based schemes, etc. 41 | // It is not feasible to construct a chain of multiple strategies that involve 42 | // redirection (for example both Facebook and Twitter), since the first one to 43 | // redirect will halt the chain. 44 | if (!Array.isArray(strategies)) { 45 | strategies = [strategies]; 46 | } 47 | 48 | // Return an error if the client is trying to authenticate with a strategy 49 | // that the server hasn't allowed for this authenticate call. This is important 50 | // because it prevents the user from authenticating with a registered strategy 51 | // that is not being allowed for this authenticate call. 52 | if (!strategies.includes(strategyName)) { 53 | return reject(new Error(`Invalid authentication strategy '${strategyName}'`)); 54 | } 55 | 56 | // Get the strategy, which will be used as prototype from which to create 57 | // a new instance. Action functions will then be bound to the strategy 58 | // within the context of the HTTP request/response pair. 59 | let prototype = passport._strategy(strategyName); 60 | 61 | if (!prototype) { 62 | return reject(new Error(`Unknown authentication strategy '${strategyName}'`)); 63 | } 64 | 65 | // Implement required passport methods that 66 | // can be called by a passport strategy. 67 | let strategy = Object.create(prototype); 68 | 69 | strategy.redirect = (url, status = 302) => { 70 | debug(`'${strategyName}' authentication redirecting to`, url, status); 71 | resolve({ redirect: true, url, status }); 72 | }; 73 | 74 | strategy.fail = (challenge, status) => { 75 | debug(`Authentication strategy '${strategyName}' failed`, challenge, status); 76 | resolve({ 77 | fail: true, 78 | challenge, 79 | status 80 | }); 81 | }; 82 | 83 | strategy.error = error => { 84 | debug(`Error in '${strategyName}' authentication strategy`, error); 85 | reject(error); 86 | }; 87 | 88 | strategy.success = (data, payload) => { 89 | debug(`'${strategyName}' authentication strategy succeeded`, data, payload); 90 | resolve({ 91 | success: true, 92 | data: { 93 | [entity]: data, 94 | payload 95 | } 96 | }); 97 | }; 98 | 99 | strategy.pass = () => { 100 | debug(`Passing on '${strategyName}' authentication strategy`); 101 | resolve(); 102 | }; 103 | 104 | debug('Passport request object', request); 105 | strategy.authenticate(request, strategyOptions); 106 | }); 107 | }; 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /lib/passport/index.js: -------------------------------------------------------------------------------- 1 | const initialize = require('./initialize'); 2 | const authenticate = require('./authenticate'); 3 | const makeDebug = require('debug'); 4 | 5 | const debug = makeDebug('@feathersjs/authentication:passport'); 6 | 7 | module.exports = function feathersPassport (options) { 8 | const app = this; 9 | 10 | debug('Initializing Feathers passport adapter'); 11 | 12 | return { 13 | initialize: initialize.call(app, options), 14 | authenticate: authenticate.call(app, options) 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/passport/initialize.js: -------------------------------------------------------------------------------- 1 | const makeDebug = require('debug'); 2 | 3 | const { 4 | createJWT, 5 | verifyJWT 6 | } = require('../utils'); 7 | 8 | const debug = makeDebug('@feathersjs/authentication:passport:initialize'); 9 | 10 | module.exports = function initialize (options = {}) { 11 | // const app = this; 12 | 13 | debug('Initializing custom passport initialize', options); 14 | 15 | // Do any special feathers passport initialization here. We may need this 16 | // to support different engines. 17 | return function (passport) { 18 | // NOTE (EK): This is called by passport.initialize() when calling 19 | // app.configure(authentication()). 20 | 21 | // Expose our JWT util functions globally 22 | passport._feathers = {}; 23 | passport.createJWT = createJWT; 24 | passport.verifyJWT = verifyJWT; 25 | passport.options = function (name, strategyOptions) { 26 | if (!name) { 27 | return passport._feathers; 28 | } 29 | 30 | if (typeof name === 'string' && !strategyOptions) { 31 | return passport._feathers[name]; 32 | } 33 | 34 | if (typeof name === 'string' && strategyOptions) { 35 | debug(`Setting ${name} strategy options`, strategyOptions); 36 | passport._feathers[name] = Object.assign({}, strategyOptions); 37 | } 38 | }; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const merge = require('lodash.merge'); 3 | const express = require('./express'); 4 | 5 | const debug = Debug('@feathersjs/authentication:authentication:service'); 6 | 7 | class Service { 8 | constructor (app) { 9 | this.app = app; 10 | this.passport = app.passport; 11 | } 12 | 13 | create (data = {}, params = {}) { 14 | const defaults = this.app.get('authentication') || this.app.get('auth'); 15 | const payload = params.payload; 16 | 17 | // create accessToken 18 | // TODO (EK): Support refresh tokens 19 | // TODO (EK): This should likely be a hook 20 | // TODO (EK): This service can be datastore backed to support blacklists :) 21 | return this.passport 22 | .createJWT(payload, merge({}, defaults, params)) 23 | .then(accessToken => { 24 | return { accessToken }; 25 | }); 26 | } 27 | 28 | remove (id, params) { 29 | const defaults = this.app.get('authentication') || this.app.get('auth'); 30 | const authHeader = params.headers && params.headers[defaults.header.toLowerCase()]; 31 | const authParams = authHeader && authHeader.match(/(\S+)\s+(\S+)/); 32 | const accessToken = id !== null ? id : (authParams && authParams[2]) || authHeader; 33 | 34 | // TODO (EK): return error if token is missing? 35 | return this.passport 36 | .verifyJWT(accessToken, merge(defaults, params)) 37 | .then(payload => { 38 | return { accessToken }; 39 | }); 40 | } 41 | } 42 | 43 | module.exports = function init (options) { 44 | return function () { 45 | const app = this; 46 | const path = options.path; 47 | const { 48 | successRedirect, 49 | failureRedirect, 50 | setCookie, 51 | emitEvents 52 | } = express; 53 | 54 | if (typeof path !== 'string') { 55 | throw new Error(`You must provide a 'path' in your authentication configuration or pass one explicitly.`); 56 | } 57 | 58 | debug('Configuring authentication service at path', path); 59 | 60 | app.use( 61 | path, 62 | new Service(app, options), 63 | emitEvents(options), 64 | setCookie(options), 65 | successRedirect(), 66 | failureRedirect(options) 67 | ); 68 | 69 | const service = app.service(path); 70 | 71 | if (typeof service.publish === 'function') { 72 | service.publish(() => false); 73 | } 74 | }; 75 | }; 76 | 77 | module.exports.Service = Service; 78 | -------------------------------------------------------------------------------- /lib/socket/handler.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const ms = require('ms'); 3 | 4 | const merge = require('lodash.merge'); 5 | 6 | const { 7 | normalizeError 8 | } = require('@feathersjs/socket-commons/lib/utils'); 9 | 10 | const lt = require('long-timeout'); 11 | const updateEntity = require('./update-entity'); 12 | 13 | const debug = Debug('@feathersjs/authentication:sockets:handler'); 14 | 15 | function handleSocketCallback (promise, callback) { 16 | if (typeof callback === 'function') { 17 | promise.then(data => callback(null, data)) 18 | .catch(error => { 19 | debug(`Socket authentication error`, error); 20 | callback(normalizeError(error)); 21 | }); 22 | } 23 | 24 | return promise; 25 | } 26 | 27 | module.exports = function setupSocketHandler (app, options, { feathersParams, provider, emit, disconnect }) { 28 | const authSettings = app.get('authentication') || app.get('auth'); 29 | const service = app.service(authSettings.path); 30 | const entityService = app.service(authSettings.service); 31 | let isUpdateEntitySetup = false; 32 | 33 | return function (socket) { 34 | let logoutTimer; 35 | 36 | const logout = function (callback = () => {}) { 37 | const connection = feathersParams(socket); 38 | const { accessToken } = connection; 39 | 40 | if (accessToken) { 41 | debug('Logging out socket with accessToken', accessToken); 42 | 43 | delete connection.accessToken; 44 | delete connection.authenticated; 45 | connection.headers = {}; 46 | socket._feathers.body = {}; 47 | if (socket.feathers) { 48 | socket.feathers.payload = null; 49 | socket.feathers[authSettings.entity] = null; 50 | } 51 | 52 | const promise = service.remove(accessToken, { authenticated: true }).then(tokens => { 53 | debug(`Successfully logged out socket with accessToken`, accessToken); 54 | 55 | app.emit('logout', tokens, { 56 | provider, 57 | socket, 58 | connection 59 | }); 60 | 61 | return tokens; 62 | }); 63 | 64 | // Clear logoutTimer. 65 | lt.clearTimeout(logoutTimer); 66 | 67 | handleSocketCallback(promise, callback); 68 | } else if (typeof callback === 'function') { 69 | return callback(null, {}); 70 | } 71 | }; 72 | 73 | const authenticate = function (data = {}, callback = () => {}) { 74 | if (typeof data === 'function') { 75 | callback = data; 76 | } 77 | 78 | if (typeof data === 'function' || typeof data !== 'object' || data === null) { 79 | data = {}; 80 | } 81 | 82 | const { strategy } = data; 83 | socket._feathers = Object.assign({ 84 | query: {}, 85 | provider: 'socketio', 86 | headers: {}, 87 | session: {}, 88 | cookies: {} 89 | }, feathersParams(socket)); 90 | 91 | const strategyOptions = merge({}, options, app.passport.options(strategy)); 92 | 93 | const promise = service.create(data, socket._feathers) 94 | .then(tokens => { 95 | if (socket._feathers.authenticated) { 96 | // Add the auth strategy response data and tokens to the socket connection 97 | // so that they can be referenced in the future. (ie. attach the user) 98 | let connection = feathersParams(socket); 99 | const headers = { 100 | [authSettings.header]: tokens.accessToken 101 | }; 102 | let result = { 103 | payload: socket._feathers.payload, 104 | [strategyOptions.entity]: socket._feathers[strategyOptions.entity] 105 | }; 106 | 107 | connection = Object.assign(connection, result, tokens, { headers, authenticated: true }); 108 | 109 | app.emit('login', tokens, { 110 | provider, 111 | socket, 112 | connection 113 | }); 114 | } 115 | 116 | // Clear any previous timeout if we have logged in again. 117 | if (logoutTimer) { 118 | debug(`Clearing old timeout.`); 119 | lt.clearTimeout(logoutTimer); 120 | } 121 | 122 | logoutTimer = lt.setTimeout(() => { 123 | debug(`Token expired. Logging out.`); 124 | logout(); 125 | }, ms(authSettings.jwt.expiresIn)); 126 | 127 | // TODO (EK): Setup and tear down socket listeners to keep the entity 128 | // up to date that should be attached to the socket. Need to get the 129 | // entity or assignProperty 130 | // 131 | // Remove old listeners to prevent leaks 132 | // socket.off('users updated'); 133 | // socket.off('users patched'); 134 | // socket.off('users removed'); 135 | 136 | // Register new event listeners 137 | // socket.on('users updated', data => { 138 | // if (data.id === id) { 139 | // let connection = feathersParams(socket); 140 | // connection.user = data; 141 | // } 142 | // }); 143 | 144 | // socket.on('users patched', data => { 145 | // if (data.id === id) { 146 | // let connection = feathersParams(socket); 147 | // connection.user = data; 148 | // } 149 | // }); 150 | 151 | // socket.on('users removed', data => { 152 | // if (data.id === id) { 153 | // logout(); 154 | // } 155 | // }); 156 | 157 | return Promise.resolve(tokens); 158 | }); 159 | 160 | handleSocketCallback(promise, callback); 161 | }; 162 | 163 | socket.on('authenticate', authenticate); 164 | socket.on(disconnect, logout); 165 | socket.on('logout', logout); 166 | 167 | // Only bind the handlers on receiving the first socket connection. 168 | if (!isUpdateEntitySetup) { 169 | isUpdateEntitySetup = true; 170 | entityService.on('updated', updateEntity(app)); 171 | entityService.on('patched', updateEntity(app)); 172 | } 173 | }; 174 | }; 175 | -------------------------------------------------------------------------------- /lib/socket/index.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const setupSocketHandler = require('./handler'); 3 | 4 | const debug = Debug('@feathersjs/authentication:sockets'); 5 | 6 | const socketio = function socketio (app, options = {}) { 7 | debug('Setting up Socket.io authentication middleware with options:', options); 8 | 9 | const providerSettings = { 10 | provider: 'socketio', 11 | emit: 'emit', 12 | disconnect: 'disconnect', 13 | feathersParams (socket) { 14 | return socket.feathers; 15 | } 16 | }; 17 | 18 | return setupSocketHandler(app, options, providerSettings); 19 | }; 20 | 21 | const primus = function primus (app, options = {}) { 22 | debug('Setting up Primus authentication middleware with options:', options); 23 | 24 | const providerSettings = { 25 | provider: 'primus', 26 | emit: 'send', 27 | disconnect: 'end', 28 | feathersParams (socket) { 29 | return socket.request.feathers; 30 | } 31 | }; 32 | 33 | return setupSocketHandler(app, options, providerSettings); 34 | }; 35 | 36 | module.exports = { 37 | socketio, 38 | primus 39 | }; 40 | -------------------------------------------------------------------------------- /lib/socket/update-entity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (app) => (entity) => { 4 | const authConfig = app.get('auth'); 5 | let idField = app.service(authConfig.service).id; 6 | 7 | if (!idField) { 8 | console.error(`The adapter for the ${authConfig.service} service does not add an \`id\` property to the service. It needs to be updated to do so.`); 9 | idField = entity.hasOwnProperty('id') ? 'id' : '_id'; 10 | } 11 | 12 | const entityId = entity[idField]; 13 | let socketMap; 14 | 15 | if (app.io) { 16 | socketMap = app.io.sockets.sockets; 17 | } 18 | if (app.primus) { 19 | socketMap = app.primus.connections; 20 | } 21 | 22 | Object.keys(socketMap).forEach(socketId => { 23 | const socket = socketMap[socketId]; 24 | const feathers = socket.feathers || socket.request.feathers; 25 | const socketEntity = feathers && feathers[authConfig.entity]; 26 | 27 | if (socketEntity) { 28 | const socketEntityId = socketEntity[idField]; 29 | 30 | if (`${entityId}` === `${socketEntityId}`) { 31 | // Need to assign because of external references 32 | Object.assign(socketEntity, entity); 33 | 34 | // Delete any removed entity properties 35 | const entityProps = new Set(Object.keys(entity)); 36 | Object.keys(socketEntity) 37 | .filter(prop => !entityProps.has(prop)) 38 | .forEach(prop => delete socketEntity[prop]); 39 | } 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const uuidv4 = require('uuid/v4'); 3 | const merge = require('lodash.merge'); 4 | const pick = require('lodash.pick'); 5 | const omit = require('lodash.omit'); 6 | const jwt = require('jsonwebtoken'); 7 | 8 | const debug = Debug('@feathersjs/authentication:authentication:utils'); 9 | 10 | exports.createJWT = function createJWT (payload = {}, options = {}) { 11 | const VALID_KEYS = [ 12 | 'algorithm', 13 | 'expiresIn', 14 | 'notBefore', 15 | 'audience', 16 | 'issuer', 17 | 'jwtid', 18 | 'subject', 19 | 'noTimestamp', 20 | 'header', 21 | 'exp', 22 | 'nbf', 23 | 'aud', 24 | 'sub', 25 | 'iss' 26 | ]; 27 | const settings = merge({}, options.jwt); 28 | const { secret } = options; 29 | 30 | if (!(payload.jti || settings.jwtid)) { 31 | settings.jwtid = uuidv4(); 32 | } 33 | 34 | return new Promise((resolve, reject) => { 35 | debug('Creating JWT using options', settings); 36 | 37 | if (!secret) { 38 | return reject(new Error(`secret must provided`)); 39 | } 40 | 41 | // TODO (EK): Support jwtids. Maybe auto-generate a uuid 42 | jwt.sign(omit(payload, VALID_KEYS), secret, pick(settings, VALID_KEYS), function (error, token) { 43 | if (error) { 44 | debug('Error signing JWT', error); 45 | return reject(error); 46 | } 47 | 48 | debug('New JWT issued with payload', payload); 49 | return resolve(token); 50 | }); 51 | }); 52 | }; 53 | 54 | exports.verifyJWT = function verifyJWT (token, options = {}) { 55 | const VALID_KEYS = [ 56 | 'algorithms', 57 | 'audience', 58 | 'issuer', 59 | 'ignoreExpiration', 60 | 'ignoreNotBefore', 61 | 'clockTolerance' 62 | ]; 63 | const settings = merge({}, options.jwt); 64 | const { secret } = options; 65 | 66 | // normalize algorithm to array 67 | if (settings.algorithm) { 68 | settings.algorithms = Array.isArray(settings.algorithm) ? settings.algorithm : [settings.algorithm]; 69 | delete settings.algorithm; 70 | } 71 | 72 | return new Promise((resolve, reject) => { 73 | if (!token) { 74 | return reject(new Error(`token must provided`)); 75 | } 76 | 77 | if (!secret) { 78 | return reject(new Error(`secret must provided`)); 79 | } 80 | 81 | debug('Verifying token', token); 82 | jwt.verify(token, secret, pick(settings, VALID_KEYS), (error, payload) => { 83 | if (error) { 84 | debug('Error verifying token', error); 85 | return reject(error); 86 | } 87 | 88 | debug('Verified token with payload', payload); 89 | resolve(payload); 90 | }); 91 | }); 92 | }; 93 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive test/ 2 | --timeout 20000 3 | --exit 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feathersjs/authentication", 3 | "description": "Add Authentication to your FeathersJS app.", 4 | "version": "2.1.7", 5 | "homepage": "https://github.com/feathersjs/authentication", 6 | "main": "lib/", 7 | "keywords": [ 8 | "feathers", 9 | "feathers-plugin" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/feathersjs/authentication.git" 15 | }, 16 | "author": { 17 | "name": "Feathers contributors", 18 | "email": "hello@feathersjs.com", 19 | "url": "https://feathersjs.com" 20 | }, 21 | "contributors": [], 22 | "bugs": { 23 | "url": "https://github.com/feathersjs/authentication/issues" 24 | }, 25 | "engines": { 26 | "node": ">= 6" 27 | }, 28 | "scripts": { 29 | "publish": "git push origin --tags && npm run changelog && git push origin", 30 | "release:patch": "npm version patch && npm publish --access public", 31 | "release:minor": "npm version minor && npm publish --access public", 32 | "release:major": "npm version major && npm publish --access public", 33 | "release:pre": "npm version prerelease && npm publish --tag pre --access public", 34 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 35 | "lint": "semistandard --fix", 36 | "mocha": "mocha --opts mocha.opts", 37 | "test": "npm run lint && npm run coverage", 38 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --opts mocha.opts" 39 | }, 40 | "semistandard": { 41 | "env": [ 42 | "mocha" 43 | ] 44 | }, 45 | "directories": { 46 | "lib": "lib" 47 | }, 48 | "dependencies": { 49 | "@feathersjs/commons": "^2.0.0", 50 | "@feathersjs/errors": "^3.0.0", 51 | "@feathersjs/socket-commons": "^3.1.2", 52 | "debug": "^3.1.0", 53 | "jsonwebtoken": "^8.0.0", 54 | "lodash.clone": "^4.5.0", 55 | "lodash.merge": "^4.6.0", 56 | "lodash.omit": "^4.5.0", 57 | "lodash.pick": "^4.4.0", 58 | "long-timeout": "^0.1.1", 59 | "ms": "^2.0.0", 60 | "passport": "^0.4.0", 61 | "uuid": "^3.1.0" 62 | }, 63 | "devDependencies": { 64 | "@feathersjs/authentication-jwt": "^2.0.0", 65 | "@feathersjs/authentication-local": "^1.0.0", 66 | "@feathersjs/configuration": "^2.0.0", 67 | "@feathersjs/express": "^1.0.0", 68 | "@feathersjs/feathers": "^3.0.5", 69 | "@feathersjs/primus": "^3.0.3", 70 | "@feathersjs/socketio": "^3.0.0", 71 | "body-parser": "^1.15.2", 72 | "chai": "^4.1.0", 73 | "chai-uuid": "^1.0.6", 74 | "feathers-memory": "^2.0.0", 75 | "istanbul": "^1.1.0-alpha.1", 76 | "jshint": "^2.9.3", 77 | "localstorage-memory": "^1.0.2", 78 | "mocha": "^5.0.0", 79 | "mongodb": "^3.0.0", 80 | "passport-strategy": "^1.0.0", 81 | "primus": "^7.0.0", 82 | "semistandard": "^13.0.1", 83 | "sinon": "^7.3.1", 84 | "sinon-chai": "^3.0.0", 85 | "socket.io-client": "^2.0.0", 86 | "superagent": "^3.0.0", 87 | "ws": "^6.0.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/express/authenticate.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const passport = require('passport'); 3 | const MockStrategy = require('../fixtures/strategy'); 4 | const chai = require('chai'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require('sinon-chai'); 7 | const { expect } = chai; 8 | const { authenticate } = require('../../lib/express'); 9 | 10 | chai.use(sinonChai); 11 | 12 | describe('express:authenticate', () => { 13 | let req; 14 | let res; 15 | let next; 16 | 17 | beforeEach(() => { 18 | passport.use(new MockStrategy({}, () => {})); 19 | req = { 20 | feathers: {}, 21 | app: { 22 | passport, 23 | authenticate: () => { 24 | return () => Promise.resolve(); 25 | } 26 | } 27 | }; 28 | res = { 29 | status: sinon.spy() 30 | }; 31 | next = sinon.spy(); 32 | }); 33 | 34 | afterEach(() => { 35 | next.resetHistory(); 36 | res.status.resetHistory(); 37 | }); 38 | 39 | describe('when strategy name is missing', () => { 40 | it('throws an error', () => { 41 | expect(() => { 42 | authenticate()(req, res, next); 43 | }).to.throw; 44 | }); 45 | }); 46 | 47 | describe('when already authenticated', () => { 48 | it('calls next', next => { 49 | req.authenticated = true; 50 | authenticate('missing')(req, res, next); 51 | }); 52 | }); 53 | 54 | describe('when strategy has not been registered with passport', () => { 55 | it('returns an error', done => { 56 | authenticate('missing')(req, res, error => { 57 | expect(error).to.not.equal(undefined); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('when authentication succeeds', () => { 64 | let response; 65 | 66 | beforeEach(() => { 67 | response = { 68 | success: true, 69 | data: { 70 | user: { name: 'bob' }, 71 | info: { platform: 'feathers' } 72 | } 73 | }; 74 | req.app.authenticate = () => { 75 | return () => Promise.resolve(response); 76 | }; 77 | }); 78 | 79 | it('calls next', next => { 80 | authenticate('mock')(req, res, next); 81 | }); 82 | 83 | it('exposes result to express request object', done => { 84 | authenticate('mock')(req, res, () => { 85 | expect(req.user).to.deep.equal(response.data.user); 86 | expect(req.info).to.deep.equal(response.data.info); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('sets request.authenticated', done => { 92 | authenticate('mock')(req, res, () => { 93 | expect(req.authenticated).to.equal(true); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('exposes result to feathers', done => { 99 | authenticate('mock')(req, res, () => { 100 | expect(req.feathers.user).to.deep.equal(response.data.user); 101 | expect(req.feathers.info).to.deep.equal(response.data.info); 102 | done(); 103 | }); 104 | }); 105 | 106 | it('sets request.feathers.authenticated', done => { 107 | authenticate('mock')(req, res, () => { 108 | expect(req.feathers.authenticated).to.equal(true); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('supports redirecting', done => { 114 | const successRedirect = '/app'; 115 | 116 | res.redirect = url => { 117 | expect(res.status).to.have.been.calledWith(302); 118 | expect(next).to.not.be.called; 119 | expect(url).to.equal(successRedirect); 120 | done(); 121 | }; 122 | 123 | authenticate('mock', { successRedirect })(req, res, next); 124 | }); 125 | }); 126 | 127 | describe('when authentication fails', () => { 128 | let response; 129 | 130 | beforeEach(() => { 131 | response = { 132 | fail: true, 133 | challenge: 'missing credentials' 134 | }; 135 | req.app.authenticate = () => { 136 | return () => Promise.resolve(response); 137 | }; 138 | }); 139 | 140 | it('returns an Unauthorized error', done => { 141 | authenticate('mock')(req, res, error => { 142 | expect(error.code).to.equal(401); 143 | done(); 144 | }); 145 | }); 146 | 147 | it('returns an error with challenge as the message', done => { 148 | authenticate('mock')(req, res, error => { 149 | expect(error.message).to.equal(response.challenge); 150 | done(); 151 | }); 152 | }); 153 | 154 | it('returns an error with the challenge message', done => { 155 | response.challenge = { message: 'missing credentials' }; 156 | req.app.authenticate = () => { 157 | return () => Promise.resolve(response); 158 | }; 159 | authenticate('mock')(req, res, error => { 160 | expect(error.code).to.equal(401); 161 | expect(error.message).to.equal(response.challenge.message); 162 | done(); 163 | }); 164 | }); 165 | 166 | it('returns an error from a custom status code', done => { 167 | response.status = 400; 168 | req.app.authenticate = () => { 169 | return () => Promise.resolve(response); 170 | }; 171 | authenticate('mock')(req, res, error => { 172 | expect(error.code).to.equal(400); 173 | done(); 174 | }); 175 | }); 176 | 177 | it('supports custom error messages', done => { 178 | const failureMessage = 'Custom Error'; 179 | authenticate('mock', { failureMessage })(req, res, error => { 180 | expect(error.message).to.equal(failureMessage); 181 | done(); 182 | }); 183 | }); 184 | 185 | it('supports redirecting', done => { 186 | const failureRedirect = '/login'; 187 | 188 | res.redirect = (url) => { 189 | expect(next).to.not.be.called; 190 | expect(res.status).to.have.been.calledWith(302); 191 | expect(url).to.equal(failureRedirect); 192 | done(); 193 | }; 194 | 195 | authenticate('mock', { failureRedirect })(req, res, next); 196 | }); 197 | }); 198 | 199 | describe('when authentication errors', () => { 200 | beforeEach(() => { 201 | req.app.authenticate = () => { 202 | return () => Promise.reject(new Error('Authentication Error')); 203 | }; 204 | }); 205 | 206 | it('returns an error', done => { 207 | authenticate('mock')(req, res, error => { 208 | expect(error).to.not.equal(undefined); 209 | done(); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('when authentication redirects', () => { 215 | let response; 216 | 217 | beforeEach(() => { 218 | response = { 219 | redirect: true, 220 | status: 302, 221 | url: '/app' 222 | }; 223 | req.app.authenticate = () => { 224 | return () => Promise.resolve(response); 225 | }; 226 | }); 227 | 228 | it('redirects', done => { 229 | res.redirect = url => { 230 | expect(next).to.not.be.called; 231 | expect(res.status).to.have.been.calledWith(response.status); 232 | expect(url).to.equal(response.url); 233 | done(); 234 | }; 235 | 236 | authenticate('mock')(req, res, next); 237 | }); 238 | }); 239 | 240 | describe('when authentication passes', () => { 241 | it('calls next', next => { 242 | authenticate('mock')(req, res, next); 243 | }); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /test/express/emit-events.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const chai = require('chai'); 3 | const sinon = require('sinon'); 4 | const sinonChai = require('sinon-chai'); 5 | const { emitEvents } = require('../../lib/express'); 6 | const { expect } = chai; 7 | 8 | chai.use(sinonChai); 9 | 10 | describe('express:emitEvents', () => { 11 | let req; 12 | let res; 13 | 14 | beforeEach(() => { 15 | req = { 16 | app: { 17 | emit: sinon.spy() 18 | } 19 | }; 20 | res = { 21 | hook: { 22 | method: 'create' 23 | }, 24 | data: { 25 | accessToken: 'token' 26 | } 27 | }; 28 | }); 29 | 30 | afterEach(() => { 31 | req.app.emit.resetHistory(); 32 | }); 33 | 34 | it('calls next', next => { 35 | emitEvents()(req, res, next); 36 | }); 37 | 38 | describe('when res.data is missing', () => { 39 | it('does not call app.emit', done => { 40 | delete res.data; 41 | emitEvents()(req, res, () => { 42 | expect(req.app.emit).to.not.have.been.called; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('when res.data.accessToken is missing', () => { 49 | it('does not call app.emit', done => { 50 | delete res.data.accessToken; 51 | emitEvents()(req, res, () => { 52 | expect(req.app.emit).to.not.have.been.called; 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('when create method was called', () => { 59 | it('emits login event', done => { 60 | emitEvents()(req, res, () => { 61 | expect(req.app.emit).to.have.been.calledOnce; 62 | expect(req.app.emit).to.have.been.calledWith('login', res.data, { provider: 'rest', req, res }); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('when remove method was called', () => { 69 | it('emits logout event', done => { 70 | res.hook.method = 'remove'; 71 | emitEvents()(req, res, () => { 72 | expect(req.app.emit).to.have.been.calledOnce; 73 | expect(req.app.emit).to.have.been.calledWith('logout', res.data, { provider: 'rest', req, res }); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/express/expose-cookies.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { exposeCookies } = require('../../lib/express'); 3 | 4 | const cookies = { 5 | 'feathers-jwt': 'cookie cookie cookie' 6 | }; 7 | 8 | describe('express:exposeCookies', () => { 9 | let req; 10 | let res; 11 | 12 | beforeEach(() => { 13 | req = { 14 | feathers: {}, 15 | cookies 16 | }; 17 | res = {}; 18 | }); 19 | 20 | it('adds the cookies object to req.feathers', done => { 21 | exposeCookies()(req, res, () => { 22 | expect(req.feathers.cookies).to.deep.equal(cookies); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('calls next', next => { 28 | exposeCookies()(req, res, next); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/express/expose-headers.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { exposeHeaders } = require('../../lib/express'); 3 | 4 | const headers = { 5 | 'authorization': 'JWT:my token' 6 | }; 7 | 8 | describe('express:exposeHeaders', () => { 9 | let req; 10 | let res; 11 | 12 | beforeEach(() => { 13 | req = { 14 | feathers: {}, 15 | headers 16 | }; 17 | res = {}; 18 | }); 19 | 20 | it('adds the headers object to req.feathers', done => { 21 | exposeHeaders()(req, res, () => { 22 | expect(req.feathers.headers).to.deep.equal(headers); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('calls next', next => { 28 | exposeHeaders()(req, res, next); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/express/failure-redirect.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const chai = require('chai'); 3 | const sinon = require('sinon'); 4 | const sinonChai = require('sinon-chai'); 5 | const { failureRedirect } = require('../../lib/express'); 6 | const { expect } = chai; 7 | 8 | chai.use(sinonChai); 9 | 10 | describe('express:failureRedirect', () => { 11 | let req; 12 | let res; 13 | let error; 14 | 15 | beforeEach(() => { 16 | req = {}; 17 | res = { 18 | hook: { 19 | data: { 20 | __redirect: { 21 | url: '/app' 22 | } 23 | } 24 | }, 25 | clearCookie: sinon.spy(), 26 | redirect: sinon.spy(), 27 | status: sinon.spy() 28 | }; 29 | error = new Error('Authentication Error'); 30 | }); 31 | 32 | afterEach(() => { 33 | res.clearCookie.resetHistory(); 34 | res.redirect.resetHistory(); 35 | res.status.resetHistory(); 36 | }); 37 | 38 | describe('when redirect is set on the hook', () => { 39 | it('redirects to configured endpoint with default status code', () => { 40 | failureRedirect()(error, req, res); 41 | expect(res.status).to.have.been.calledOnce; 42 | expect(res.status).to.have.been.calledWith(302); 43 | expect(res.redirect).to.have.been.calledOnce; 44 | expect(res.redirect).to.have.been.calledWith('/app'); 45 | }); 46 | 47 | it('supports a custom status code', () => { 48 | res.hook.data.__redirect.status = 400; 49 | failureRedirect()(error, req, res); 50 | expect(res.status).to.have.been.calledOnce; 51 | expect(res.status).to.have.been.calledWith(400); 52 | expect(res.redirect).to.have.been.calledOnce; 53 | expect(res.redirect).to.have.been.calledWith('/app'); 54 | }); 55 | }); 56 | 57 | describe('when cookie is enabled', () => { 58 | it('clears cookie', () => { 59 | const options = { 60 | cookie: { 61 | enabled: true, 62 | name: 'feathers-jwt' 63 | } 64 | }; 65 | 66 | failureRedirect(options)(error, req, res); 67 | expect(res.clearCookie).to.have.been.calledOnce; 68 | expect(res.clearCookie).to.have.been.calledWith(options.cookie.name); 69 | }); 70 | }); 71 | 72 | describe('when res.hook is not defined', done => { 73 | it('calls next with error', done => { 74 | delete res.hook; 75 | failureRedirect()(error, req, res, e => { 76 | expect(e).to.equal(error); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('when res.hook.data.redirect is not defined', done => { 83 | it('calls next with error', done => { 84 | delete res.hook.data.__redirect; 85 | failureRedirect()(error, req, res, e => { 86 | expect(e).to.equal(error); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/express/index.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | const express = require('../../lib/express'); 4 | 5 | describe('express middleware', () => { 6 | it('is CommonJS compatible', () => { 7 | expect(typeof require('../../lib/express')).to.equal('object'); 8 | }); 9 | 10 | it('is ES6 compatible', () => { 11 | expect(typeof express).to.equal('object'); 12 | }); 13 | 14 | it('exposes authenticate middleware', () => { 15 | expect(typeof express.authenticate).to.equal('function'); 16 | }); 17 | 18 | it('exposes exposeHeaders middleware', () => { 19 | expect(typeof express.exposeHeaders).to.equal('function'); 20 | }); 21 | 22 | it('exposes exposeCookies middleware', () => { 23 | expect(typeof express.exposeCookies).to.equal('function'); 24 | }); 25 | 26 | it('exposes failureRedirect middleware', () => { 27 | expect(typeof express.failureRedirect).to.equal('function'); 28 | }); 29 | 30 | it('exposes successRedirect middleware', () => { 31 | expect(typeof express.successRedirect).to.equal('function'); 32 | }); 33 | 34 | it('exposes setCookie middleware', () => { 35 | expect(typeof express.setCookie).to.equal('function'); 36 | }); 37 | 38 | it('exposes emitEvents middleware', () => { 39 | expect(typeof express.emitEvents).to.equal('function'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/express/set-cookie.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const chai = require('chai'); 3 | const sinon = require('sinon'); 4 | const sinonChai = require('sinon-chai'); 5 | const ms = require('ms'); 6 | const getOptions = require('../../lib/options'); 7 | const { setCookie } = require('../../lib/express'); 8 | 9 | const { expect } = chai; 10 | 11 | chai.use(sinonChai); 12 | 13 | describe('express:setCookie', () => { 14 | let req; 15 | let res; 16 | let options; 17 | 18 | beforeEach(() => { 19 | options = getOptions(); 20 | req = { 21 | app: { 22 | get: () => {} 23 | }, 24 | feathers: {} 25 | }; 26 | res = { 27 | cookie: sinon.spy(), 28 | clearCookie: sinon.spy(), 29 | hook: { method: 'create' }, 30 | data: { 31 | accessToken: 'token' 32 | } 33 | }; 34 | }); 35 | 36 | afterEach(() => { 37 | res.cookie.resetHistory(); 38 | res.clearCookie.resetHistory(); 39 | }); 40 | 41 | describe('when cookies are not enabled', () => { 42 | it('calls next', next => { 43 | setCookie(options)(req, res, next); 44 | }); 45 | 46 | it('does not clear cookie', done => { 47 | setCookie(options)(req, res, () => { 48 | expect(res.clearCookie).to.not.have.been.called; 49 | done(); 50 | }); 51 | }); 52 | 53 | it('does not set cookie', done => { 54 | setCookie(options)(req, res, () => { 55 | expect(res.cookie).to.not.have.been.called; 56 | done(); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('when cookies are enabled', () => { 62 | beforeEach(() => { 63 | options.cookie.enabled = true; 64 | }); 65 | 66 | describe('when cookie name is missing', () => { 67 | beforeEach(() => { 68 | delete options.cookie.name; 69 | }); 70 | 71 | it('does not clear the cookie', done => { 72 | setCookie(options)(req, res, () => { 73 | expect(res.clearCookie).to.not.have.been.called; 74 | done(); 75 | }); 76 | }); 77 | 78 | it('does not set the cookie', done => { 79 | setCookie(options)(req, res, () => { 80 | expect(res.cookie).to.not.have.been.called; 81 | done(); 82 | }); 83 | }); 84 | 85 | it('calls next', next => { 86 | setCookie(options)(req, res, next); 87 | }); 88 | }); 89 | 90 | it('clears cookie', done => { 91 | setCookie(options)(req, res, () => { 92 | expect(res.clearCookie).to.have.been.calledWith(options.cookie.name); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('sets cookie with default expiration of the configured jwt expiration', done => { 98 | const expiry = new Date(Date.now() + ms(options.jwt.expiresIn)); 99 | setCookie(options)(req, res, () => { 100 | expect(res.cookie).to.have.been.calledWith('feathers-jwt', 'token'); 101 | expect(res.cookie.getCall(0).args[2].httpOnly).to.equal(false); 102 | expect(res.cookie.getCall(0).args[2].secure).to.equal(true); 103 | expect(res.cookie.getCall(0).args[2].expires.toString()).to.equal(expiry.toString()); 104 | done(); 105 | }); 106 | }); 107 | 108 | it('sets cookie with expiration using maxAge', done => { 109 | const expiry = new Date(Date.now() + ms('1d')); 110 | options.cookie.maxAge = '1d'; 111 | setCookie(options)(req, res, () => { 112 | expect(res.cookie).to.have.been.calledWith('feathers-jwt', 'token'); 113 | expect(res.cookie.getCall(0).args[2].httpOnly).to.equal(false); 114 | expect(res.cookie.getCall(0).args[2].secure).to.equal(true); 115 | expect(res.cookie.getCall(0).args[2].expires.toString()).to.equal(expiry.toString()); 116 | done(); 117 | }); 118 | }); 119 | 120 | it('sets cookie with custom expiration', done => { 121 | const expiry = new Date(Date.now() + ms('1d')); 122 | const expectedOptions = { 123 | httpOnly: false, 124 | secure: true, 125 | expires: expiry 126 | }; 127 | options.cookie.expires = expiry; 128 | 129 | setCookie(options)(req, res, () => { 130 | expect(res.cookie).to.have.been.calledWithExactly('feathers-jwt', 'token', expectedOptions); 131 | done(); 132 | }); 133 | }); 134 | 135 | it('returns an error when expiration is not a date', done => { 136 | options.cookie.expires = true; 137 | setCookie(options)(req, res, error => { 138 | expect(error).to.not.equal(undefined); 139 | done(); 140 | }); 141 | }); 142 | 143 | it('does not mutate given option object', done => { 144 | setCookie(options)(req, res, () => { 145 | expect(res.cookie.getCall(0).args[2].expires).to.be.ok; 146 | expect(options.expires).to.be.undefined; 147 | done(); 148 | }); 149 | }); 150 | 151 | it('calls next', next => { 152 | setCookie(options)(req, res, next); 153 | }); 154 | 155 | describe('when hook method is remove', () => { 156 | beforeEach(() => { 157 | res.hook.method = 'remove'; 158 | }); 159 | 160 | it('does not set the cookie', done => { 161 | setCookie(options)(req, res, () => { 162 | expect(res.cookie).to.not.have.been.called; 163 | done(); 164 | }); 165 | }); 166 | 167 | it('calls next', next => { 168 | setCookie(options)(req, res, next); 169 | }); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /test/express/success-redirect.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const chai = require('chai'); 3 | const sinon = require('sinon'); 4 | const sinonChai = require('sinon-chai'); 5 | const { successRedirect } = require('../../lib/express'); 6 | const { expect } = chai; 7 | 8 | chai.use(sinonChai); 9 | 10 | describe('express:successRedirect', () => { 11 | let req; 12 | let res; 13 | 14 | beforeEach(() => { 15 | req = {}; 16 | res = { 17 | hook: { 18 | data: { 19 | __redirect: { 20 | url: '/app' 21 | } 22 | } 23 | }, 24 | redirect: sinon.spy(), 25 | status: sinon.spy() 26 | }; 27 | }); 28 | 29 | afterEach(() => { 30 | res.redirect.resetHistory(); 31 | res.status.resetHistory(); 32 | }); 33 | 34 | describe('when redirect is set on the hook', () => { 35 | it('redirects to configured endpoint with default status code', () => { 36 | successRedirect()(req, res); 37 | expect(res.status).to.have.been.calledOnce; 38 | expect(res.status).to.have.been.calledWith(302); 39 | expect(res.redirect).to.have.been.calledOnce; 40 | expect(res.redirect).to.have.been.calledWith('/app'); 41 | }); 42 | 43 | it('supports a custom status code', () => { 44 | res.hook.data.__redirect.status = 400; 45 | successRedirect()(req, res); 46 | expect(res.status).to.have.been.calledOnce; 47 | expect(res.status).to.have.been.calledWith(400); 48 | expect(res.redirect).to.have.been.calledOnce; 49 | expect(res.redirect).to.have.been.calledWith('/app'); 50 | }); 51 | }); 52 | 53 | describe('when res.hook is not defined', () => { 54 | it('calls next', next => { 55 | delete res.hook; 56 | successRedirect()(req, res, next); 57 | }); 58 | }); 59 | 60 | describe('when res.hook.data.__redirect is not defined', () => { 61 | it('calls next', next => { 62 | delete res.hook.data.__redirect; 63 | successRedirect()(req, res, next); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/fixtures/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const express = require('@feathersjs/express'); 4 | const rest = require('@feathersjs/express/rest'); 5 | const socketio = require('@feathersjs/socketio'); 6 | const primus = require('@feathersjs/primus'); 7 | const memory = require('feathers-memory'); 8 | const bodyParser = require('body-parser'); 9 | const errorHandler = require('@feathersjs/errors/handler'); 10 | const local = require('@feathersjs/authentication-local'); 11 | const jwt = require('@feathersjs/authentication-jwt'); 12 | const auth = require('../../lib/index'); 13 | 14 | const User = { 15 | email: 'admin@feathersjs.com', 16 | password: 'admin', 17 | permissions: ['*'] 18 | }; 19 | 20 | process.on('unhandledRejection', (reason, p) => { 21 | console.log('Unhandled Rejection at: Promise ', p, ' reason: ', reason); 22 | }); 23 | 24 | module.exports = function (settings, socketProvider) { 25 | const app = express(feathers()); 26 | 27 | let _provider; 28 | if (socketProvider === 'socketio') { 29 | _provider = socketio((io) => { 30 | io.use((socket, next) => { 31 | socket.feathers.data = 'Hello world'; 32 | next(); 33 | }); 34 | }); 35 | } else { 36 | _provider = primus({ 37 | transformer: 'websockets' 38 | }, function (primus) { 39 | // Set up Primus authorization here 40 | primus.authorize(function (req, done) { 41 | req.feathers.data = 'Hello world'; 42 | 43 | done(); 44 | }); 45 | }); 46 | } 47 | 48 | app.configure(rest()) 49 | .configure(_provider) 50 | .use(bodyParser.json()) 51 | .use(bodyParser.urlencoded({ extended: true })) 52 | .configure(auth(settings)) 53 | .configure(local()) 54 | .configure(local({ 55 | name: 'org-local', 56 | entity: 'org' 57 | })) 58 | .configure(jwt()) 59 | .use('/users', memory()) 60 | .use('/', express.static(path.resolve(__dirname, '/public'))); 61 | 62 | app.service('authentication').hooks({ 63 | before: { 64 | create: [ 65 | auth.hooks.authenticate(['jwt', 'local', 'org-local']) 66 | ], 67 | remove: [ 68 | auth.hooks.authenticate('jwt') 69 | ] 70 | } 71 | }); 72 | 73 | // Add a hook to the user service that automatically replaces 74 | // the password with a hash of the password before saving it. 75 | app.service('users').hooks({ 76 | before: { 77 | find: [ 78 | auth.hooks.authenticate('jwt') 79 | ], 80 | create: [ 81 | local.hooks.hashPassword({ passwordField: 'password' }) 82 | ] 83 | } 84 | }); 85 | 86 | // Create a user that we can use to log in 87 | app.service('users').create(User).catch(console.error); 88 | 89 | // Custom Express routes 90 | app.get('/protected', auth.express.authenticate('jwt'), (req, res, next) => { 91 | res.json({ success: true }); 92 | }); 93 | 94 | app.get('/unprotected', (req, res, next) => { 95 | res.json({ success: true }); 96 | }); 97 | 98 | // Custom route with custom redirects 99 | app.post('/login', auth.express.authenticate('local', { successRedirect: '/app', failureRedirect: '/login' })); 100 | 101 | app.get('/app', (req, res, next) => { 102 | res.json({ success: true }); 103 | }); 104 | 105 | app.get('/login', (req, res, next) => { 106 | res.json({ success: false }); 107 | }); 108 | 109 | app.use(errorHandler()); 110 | 111 | app.on('connection', connection => app.channel('everybody').join(connection)); 112 | app.publish(() => app.channel('everybody')); 113 | 114 | return app; 115 | }; 116 | -------------------------------------------------------------------------------- /test/fixtures/strategy.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport-strategy'); 2 | const util = require('util'); 3 | 4 | const Strategy = function (options, verify) { 5 | passport.Strategy.call(this); 6 | this.name = 'mock'; 7 | this._options = options; 8 | this._verify = verify; 9 | }; 10 | 11 | util.inherits(Strategy, passport.Strategy); 12 | 13 | Strategy.prototype.authenticate = function (req, options) { 14 | const callback = function (error, user, info) { 15 | if (error) { 16 | return this.error(error); 17 | } 18 | 19 | if (info && info.pass) { 20 | return this.pass(); 21 | } 22 | 23 | if (info && info.url) { 24 | return this.redirect(info.url, info.status); 25 | } 26 | 27 | if (!user) { 28 | return this.fail(info.challenge, info.status); 29 | } 30 | 31 | return this.success(user, info); 32 | }.bind(this); 33 | 34 | this._verify(callback); 35 | }; 36 | 37 | module.exports = Strategy; 38 | -------------------------------------------------------------------------------- /test/hooks/authenticate.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const passport = require('passport'); 3 | const MockStrategy = require('../fixtures/strategy'); 4 | const chai = require('chai'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require('sinon-chai'); 7 | const { authenticate } = require('../../lib/hooks'); 8 | const { expect } = chai; 9 | 10 | chai.use(sinonChai); 11 | 12 | describe('hooks:authenticate', () => { 13 | let hook; 14 | let authenticator; 15 | 16 | beforeEach(() => { 17 | passport.use(new MockStrategy({}, () => {})); 18 | authenticator = sinon.stub().returns(Promise.resolve()); 19 | hook = { 20 | type: 'before', 21 | app: { 22 | passport, 23 | authenticate: () => { 24 | return authenticator; 25 | } 26 | }, 27 | data: { name: 'Bob' }, 28 | params: { 29 | provider: 'rest', 30 | headers: { 31 | authorization: 'JWT' 32 | }, 33 | cookies: { 34 | 'feathers-jwt': 'token' 35 | } 36 | } 37 | }; 38 | }); 39 | 40 | afterEach(() => { 41 | authenticator.resetHistory(); 42 | }); 43 | 44 | describe('when strategy name is missing', () => { 45 | it('throws an error', () => { 46 | expect(() => { 47 | authenticate()(hook); 48 | }).to.throw; 49 | }); 50 | }); 51 | 52 | describe('when provider is missing', () => { 53 | it('does nothing', () => { 54 | delete hook.params.provider; 55 | return authenticate('mock')(hook).then(returnedHook => { 56 | expect(returnedHook).to.deep.equal(hook); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('when hook is already authenticated', () => { 62 | it('does nothing', () => { 63 | hook.params.authenticated = true; 64 | return authenticate('mock')(hook).then(returnedHook => { 65 | expect(returnedHook).to.deep.equal(hook); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('when not called as a before hook', () => { 71 | it('returns an error', () => { 72 | hook.type = 'after'; 73 | return authenticate('mock')(hook).catch(error => { 74 | expect(error).to.not.equal(undefined); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('when strategy has not been registered with passport', () => { 80 | it('returns an error', () => { 81 | return authenticate('missing')(hook).catch(error => { 82 | expect(error).to.not.equal(undefined); 83 | }); 84 | }); 85 | }); 86 | 87 | it('normalizes request object for passport', () => { 88 | return authenticate('mock')(hook).then(() => { 89 | expect(authenticator).to.have.been.called; 90 | expect(authenticator).to.have.been.calledWith({ 91 | query: hook.data, 92 | body: hook.data, 93 | params: hook.params, 94 | headers: hook.params.headers, 95 | cookies: hook.params.cookies, 96 | session: {} 97 | }); 98 | }); 99 | }); 100 | 101 | it('throws error when strategy is not allowed', () => { 102 | hook.data.strategy = 'something'; 103 | 104 | return authenticate('mock')(hook).then(() => { 105 | throw new Error('Should never get here'); 106 | }).catch(error => { 107 | expect(error.message).to.equal('Strategy something is not permitted'); 108 | }); 109 | }); 110 | 111 | describe('when authentication succeeds', () => { 112 | let response; 113 | 114 | beforeEach(() => { 115 | response = { 116 | success: true, 117 | data: { 118 | user: { name: 'bob' }, 119 | info: { platform: 'feathers' } 120 | } 121 | }; 122 | hook.app.authenticate = () => { 123 | return () => Promise.resolve(response); 124 | }; 125 | }); 126 | 127 | it('exposes result to hook.params', () => { 128 | return authenticate('mock')(hook).then(hook => { 129 | expect(hook.params.user).to.deep.equal(response.data.user); 130 | expect(hook.params.info).to.deep.equal(response.data.info); 131 | }); 132 | }); 133 | 134 | it('sets hook.params.authenticated', () => { 135 | return authenticate('mock')(hook).then(hook => { 136 | expect(hook.params.authenticated).to.equal(true); 137 | }); 138 | }); 139 | 140 | it('supports redirecting', () => { 141 | const successRedirect = '/app'; 142 | return authenticate('mock', { successRedirect })(hook).then(hook => { 143 | expect(hook.data.__redirect.status).to.equal(302); 144 | expect(hook.data.__redirect.url).to.equal(successRedirect); 145 | }); 146 | }); 147 | }); 148 | 149 | describe('when authentication fails', () => { 150 | let response; 151 | 152 | beforeEach(() => { 153 | response = { 154 | fail: true, 155 | challenge: 'missing credentials' 156 | }; 157 | hook.app.authenticate = () => { 158 | return () => Promise.resolve(response); 159 | }; 160 | }); 161 | 162 | it('returns an Unauthorized error', () => { 163 | return authenticate('mock')(hook).catch(error => { 164 | expect(error.code).to.equal(401); 165 | }); 166 | }); 167 | 168 | it('does not set hook.params.authenticated', () => { 169 | return authenticate('mock')(hook).catch(() => { 170 | expect(hook.params.authenticated).to.equal(undefined); 171 | }); 172 | }); 173 | 174 | it('returns an error with challenge as the message', () => { 175 | return authenticate('mock')(hook).catch(error => { 176 | expect(error.message).to.equal(response.challenge); 177 | }); 178 | }); 179 | 180 | it('returns an error with the challenge message', () => { 181 | response.challenge = { message: 'missing credentials' }; 182 | hook.app.authenticate = () => { 183 | return () => Promise.resolve(response); 184 | }; 185 | authenticate('mock')(hook).catch(error => { 186 | expect(error.code).to.equal(401); 187 | expect(error.message).to.equal(response.challenge.message); 188 | }); 189 | }); 190 | 191 | it('returns an error from a custom status code', () => { 192 | response.status = 400; 193 | hook.app.authenticate = () => { 194 | return () => Promise.resolve(response); 195 | }; 196 | authenticate('mock')(hook).catch(error => { 197 | expect(error.code).to.equal(400); 198 | }); 199 | }); 200 | 201 | it('supports custom error messages', () => { 202 | const failureMessage = 'Custom Error'; 203 | authenticate('mock', { failureMessage })(hook).catch(error => { 204 | expect(error.message).to.equal(failureMessage); 205 | }); 206 | }); 207 | 208 | it('supports redirecting', () => { 209 | const failureRedirect = '/login'; 210 | return authenticate('mock', { failureRedirect })(hook).catch(() => { 211 | expect(hook.data.__redirect.status).to.equal(302); 212 | expect(hook.data.__redirect.url).to.equal(failureRedirect); 213 | }); 214 | }); 215 | }); 216 | 217 | describe('when authentication errors', () => { 218 | beforeEach(() => { 219 | hook.app.authenticate = () => { 220 | return () => Promise.reject(new Error('Authentication Error')); 221 | }; 222 | }); 223 | 224 | it('returns an error', () => { 225 | return authenticate('mock')(hook).catch(error => { 226 | expect(error).to.not.equal(undefined); 227 | }); 228 | }); 229 | 230 | it('does not set hook.params.authenticated', () => { 231 | return authenticate('mock')(hook).catch(() => { 232 | expect(hook.params.authenticated).to.equal(undefined); 233 | }); 234 | }); 235 | }); 236 | 237 | describe('when authentication redirects', () => { 238 | let response; 239 | 240 | beforeEach(() => { 241 | response = { 242 | redirect: true, 243 | status: 302, 244 | url: '/app' 245 | }; 246 | hook.app.authenticate = () => { 247 | return () => Promise.resolve(response); 248 | }; 249 | }); 250 | 251 | it('sets hook.data.__redirect', () => { 252 | return authenticate('mock')(hook).then(hook => { 253 | expect(hook.data.__redirect.status).to.equal(response.status); 254 | expect(hook.data.__redirect.url).to.equal(response.url); 255 | }); 256 | }); 257 | }); 258 | 259 | describe('when authentication passes', () => { 260 | it('does nothing', () => { 261 | return authenticate('mock')(hook).then(returnedHook => { 262 | expect(returnedHook).to.deep.equal(hook); 263 | }); 264 | }); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /test/hooks/index.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | const hooks = require('../../lib/hooks'); 4 | 5 | describe('hooks', () => { 6 | it('is CommonJS compatible', () => { 7 | expect(typeof require('../../lib/hooks')).to.equal('object'); 8 | }); 9 | 10 | it('is ES6 compatible', () => { 11 | expect(typeof hooks).to.equal('object'); 12 | }); 13 | 14 | it('exposes authenticate hook', () => { 15 | expect(typeof hooks.authenticate).to.equal('function'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const feathers = require('@feathersjs/feathers'); 3 | const expressify = require('@feathersjs/express'); 4 | const passport = require('passport'); 5 | const socketio = require('@feathersjs/socketio'); 6 | const primus = require('@feathersjs/primus'); 7 | const authentication = require('../lib'); 8 | const socket = require('../lib/socket'); 9 | const chai = require('chai'); 10 | const sinon = require('sinon'); 11 | const sinonChai = require('sinon-chai'); 12 | const { expect } = chai; 13 | const { express } = authentication; 14 | 15 | chai.use(sinonChai); 16 | 17 | describe('Feathers Authentication', () => { 18 | let app; 19 | let config; 20 | 21 | beforeEach(() => { 22 | app = expressify(feathers()); 23 | config = { secret: 'supersecret' }; 24 | }); 25 | 26 | it('is CommonJS compatible', () => { 27 | expect(typeof require('../lib')).to.equal('function'); 28 | }); 29 | 30 | it('is ES6 compatible', () => { 31 | expect(typeof authentication).to.equal('function'); 32 | }); 33 | 34 | it('exposes default', () => { 35 | expect(typeof authentication.default).to.equal('function'); 36 | }); 37 | 38 | it('exposes hooks', () => { 39 | expect(typeof authentication.hooks).to.equal('object'); 40 | }); 41 | 42 | it('exposes express middleware', () => { 43 | expect(typeof authentication.express).to.equal('object'); 44 | }); 45 | 46 | it('exposes the auth service', () => { 47 | expect(typeof authentication.service).to.equal('function'); 48 | }); 49 | 50 | describe('when secret is missing', () => { 51 | it('throws an error', () => { 52 | expect(() => { 53 | app.configure(authentication()); 54 | }).to.throw; 55 | }); 56 | }); 57 | 58 | describe('authentication has already been registered', () => { 59 | it('throws an error', () => { 60 | expect(() => { 61 | app.configure(authentication(config)); 62 | app.configure(authentication(config)); 63 | }).to.throw; 64 | }); 65 | }); 66 | 67 | describe('when in development mode', () => { 68 | it('sets cookie to be insecure', () => { 69 | app.set('env', 'development'); 70 | app.configure(authentication(config)); 71 | expect(app.get('authentication').cookie.secure).to.equal(false); 72 | }); 73 | }); 74 | 75 | describe('when in test mode', () => { 76 | it('sets cookie to be insecure', () => { 77 | app.set('env', 'test'); 78 | app.configure(authentication(config)); 79 | expect(app.get('authentication').cookie.secure).to.equal(false); 80 | }); 81 | }); 82 | 83 | describe('when in production mode', () => { 84 | it('sets cookie to be secure', () => { 85 | app.set('env', 'production'); 86 | app.configure(authentication(config)); 87 | expect(app.get('authentication').cookie.secure).to.equal(true); 88 | }); 89 | }); 90 | 91 | it('sets custom config options', () => { 92 | config.custom = 'custom'; 93 | app.configure(authentication(config)); 94 | expect(app.get('authentication').custom).to.equal('custom'); 95 | }); 96 | 97 | it('sets up feathers passport adapter', () => { 98 | app.configure(authentication(config)); 99 | expect(typeof app.passport).to.equal('object'); 100 | }); 101 | 102 | it('sets up passport', () => { 103 | sinon.spy(passport, 'framework'); 104 | app.configure(authentication(config)); 105 | expect(passport.framework).to.have.been.calledOnce; 106 | passport.framework.restore(); 107 | }); 108 | 109 | it('aliases passport.authenticate to app.authenticate', () => { 110 | app.configure(authentication(config)); 111 | expect(typeof app.authenticate).to.equal('function'); 112 | }); 113 | 114 | it('registers the exposeHeaders express middleware', () => { 115 | sinon.spy(express, 'exposeHeaders'); 116 | app.configure(authentication(config)); 117 | expect(express.exposeHeaders).to.have.been.calledOnce; 118 | express.exposeHeaders.restore(); 119 | }); 120 | 121 | it('initializes passport', () => { 122 | sinon.spy(passport, 'initialize'); 123 | app.configure(authentication(config)); 124 | expect(passport.initialize).to.have.been.calledOnce; 125 | passport.initialize.restore(); 126 | }); 127 | 128 | it('registers the authentication service', () => { 129 | app.configure(authentication(config)); 130 | expect(app.service('authentication')).to.not.equal(undefined); 131 | }); 132 | 133 | describe('when cookies are enabled', () => { 134 | it('registers the express exposeCookies middleware', () => { 135 | config = Object.assign(config, { cookie: { enabled: true } }); 136 | sinon.spy(express, 'exposeCookies'); 137 | app.configure(authentication(config)); 138 | expect(express.exposeCookies).to.have.been.calledOnce; 139 | express.exposeCookies.restore(); 140 | }); 141 | }); 142 | 143 | describe('when socketio is configured', () => { 144 | beforeEach(() => { 145 | sinon.spy(socket, 'socketio'); 146 | app.configure(socketio()) 147 | .configure(authentication(config)) 148 | .listen(); 149 | }); 150 | 151 | afterEach(() => { 152 | socket.socketio.restore(); 153 | }); 154 | 155 | it('registers socketio middleware', () => { 156 | expect(socket.socketio).to.have.been.calledOnce; 157 | }); 158 | }); 159 | 160 | describe('when primus is configured', () => { 161 | beforeEach(() => { 162 | sinon.spy(socket, 'primus'); 163 | app.configure(primus()) 164 | .configure(authentication(config)) 165 | .listen(); 166 | }); 167 | 168 | afterEach(() => { 169 | socket.primus.restore(); 170 | }); 171 | 172 | it('registers primus middleware', () => { 173 | expect(socket.primus).to.have.been.calledOnce; 174 | }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /test/integration/primus.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const merge = require('lodash.merge'); 3 | const clone = require('lodash.clone'); 4 | const createApplication = require('../fixtures/server'); 5 | const chai = require('chai'); 6 | const sinon = require('sinon'); 7 | const sinonChai = require('sinon-chai'); 8 | const { expect } = chai; 9 | 10 | chai.use(sinonChai); 11 | 12 | describe('Primus authentication', function () { 13 | const port = 8998; 14 | const baseURL = `http://localhost:${port}`; 15 | const app = createApplication({ secret: 'supersecret' }, 'primus'); 16 | const expiringApp = createApplication({ 17 | secret: 'supersecret', 18 | jwt: { expiresIn: '500ms' } 19 | }, 'primus'); 20 | const hook = sinon.spy(function (hook) {}); 21 | app.service('authentication').hooks({ 22 | before: { 23 | create: [ hook ] 24 | } 25 | }); 26 | 27 | let server; 28 | let socket; 29 | let Socket; 30 | let serverSocket; 31 | let ExpiringSocket; 32 | let expiringServer; 33 | let expiringSocket; 34 | let expiredToken; 35 | let accessToken; 36 | 37 | before(done => { 38 | const options = merge({}, app.get('authentication'), { jwt: { expiresIn: '1ms' } }); 39 | app.passport.createJWT({}, options) 40 | .then(token => { 41 | expiredToken = token; 42 | return app.passport.createJWT({ userId: 0 }, app.get('authentication')); 43 | }) 44 | .then(token => { 45 | accessToken = token; 46 | expiringServer = expiringApp.listen(1337); 47 | expiringServer.once('listening', () => { 48 | ExpiringSocket = expiringApp.primus.Socket; 49 | server = app.listen(port); 50 | server.once('listening', () => { 51 | Socket = app.primus.Socket; 52 | app.primus.on('connection', s => { serverSocket = s; }); 53 | done(); 54 | }); 55 | }); 56 | }); 57 | }); 58 | 59 | beforeEach(done => { 60 | expiringSocket = new ExpiringSocket('http://localhost:1337'); 61 | expiringSocket.on('open', () => { 62 | socket = new Socket(baseURL); 63 | socket.on('open', () => done()); 64 | }); 65 | }); 66 | 67 | afterEach(() => { 68 | hook.resetHistory(); 69 | }); 70 | 71 | after(() => { 72 | expiringServer.close(); 73 | server.close(); 74 | }); 75 | 76 | describe('Authenticating against auth service', () => { 77 | describe('Using local strategy', () => { 78 | let data; 79 | 80 | beforeEach(() => { 81 | data = { 82 | strategy: 'local', 83 | email: 'admin@feathersjs.com', 84 | password: 'admin' 85 | }; 86 | }); 87 | 88 | describe('when using valid credentials', () => { 89 | it('returns a valid access token, does not send real-time event', done => { 90 | socket.once('authentication created', () => 91 | done(new Error('real-time events for authentication should not be emitted')) 92 | ); 93 | 94 | socket.send('authenticate', data, (error, response) => { 95 | expect(error).to.not.be.ok; 96 | expect(response.accessToken).to.exist; 97 | app.passport.verifyJWT(response.accessToken, app.get('authentication')).then(payload => { 98 | expect(payload).to.exist; 99 | expect(payload.iss).to.equal('feathers'); 100 | expect(payload.userId).to.equal(0); 101 | expect(hook).to.be.calledWith(sinon.match({ params: { data: 'Hello world' } })); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | 107 | it('sets the user on the socket', done => { 108 | socket.send('authenticate', data, (error, response) => { 109 | expect(error).to.not.be.ok; 110 | expect(response.accessToken).to.exist; 111 | expect(serverSocket.request.feathers.user).to.not.equal(undefined); 112 | done(); 113 | }); 114 | }); 115 | 116 | it('updates the user on the socket', done => { 117 | socket.send('authenticate', data, (error, response) => { 118 | expect(error).to.not.be.ok; 119 | // Clone the socket user and replace it with the clone so that feathers-memory 120 | // doesn't have a reference to the same object. 121 | const socketUser = clone(serverSocket.request.feathers.user); 122 | serverSocket.request.feathers.user = socketUser; 123 | 124 | const email = 'test@feathersjs.com'; 125 | const oldEmail = socketUser.email; 126 | 127 | app.service('users').patch(socketUser.id, { email }) 128 | .then(user => { 129 | expect(socketUser.email).to.equal(email); 130 | return app.service('users').patch(socketUser.id, { email: oldEmail }); 131 | }) 132 | .then(user => { 133 | expect(socketUser.email).to.equal(oldEmail); 134 | done(); 135 | }); 136 | }); 137 | }); 138 | 139 | it('sets entity specified in strategy', done => { 140 | data.strategy = 'org-local'; 141 | socket.send('authenticate', data, (error, response) => { 142 | expect(error).to.not.be.ok; 143 | expect(response.accessToken).to.exist; 144 | expect(serverSocket.request.feathers.org).to.not.equal(undefined); 145 | done(); 146 | }); 147 | }); 148 | }); 149 | 150 | describe('when using invalid credentials', () => { 151 | it('returns NotAuthenticated error', done => { 152 | data.password = 'invalid'; 153 | socket.send('authenticate', data, error => { 154 | expect(error.code).to.equal(401); 155 | done(); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('when missing credentials', () => { 161 | it('returns BadRequest error', done => { 162 | socket.send('authenticate', { strategy: 'local' }, error => { 163 | expect(error.code).to.equal(400); 164 | done(); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('when missing strategy and server strategy does not match', () => { 170 | it('returns NotAuthenticated error', done => { 171 | delete data.strategy; 172 | socket.send('authenticate', data, error => { 173 | expect(error.code).to.equal(401); 174 | done(); 175 | }); 176 | }); 177 | }); 178 | }); 179 | 180 | describe('Using JWT strategy', () => { 181 | let data; 182 | 183 | beforeEach(() => { 184 | data = { 185 | strategy: 'jwt', 186 | accessToken 187 | }; 188 | }); 189 | 190 | describe('when using a valid access token', () => { 191 | it('returns a valid access token', done => { 192 | socket.send('authenticate', data, (error, response) => { 193 | expect(error).to.not.be.ok; 194 | expect(response.accessToken).to.exist; 195 | app.passport.verifyJWT(response.accessToken, app.get('authentication')).then(payload => { 196 | expect(payload).to.exist; 197 | expect(payload.iss).to.equal('feathers'); 198 | expect(payload.userId).to.equal(0); 199 | done(); 200 | }); 201 | }); 202 | }); 203 | }); 204 | 205 | describe.skip('when using a valid refresh token', () => { 206 | it('returns a valid access token', done => { 207 | delete data.accessToken; 208 | data.refreshToken = 'refresh'; 209 | socket.send('authenticate', data, (error, response) => { 210 | expect(error).to.not.be.ok; 211 | expect(response.accessToken).to.exist; 212 | app.passport.verifyJWT(response.accessToken, app.get('authentication')).then(payload => { 213 | expect(payload).to.exist; 214 | expect(payload.iss).to.equal('feathers'); 215 | expect(payload.userId).to.equal(0); 216 | done(); 217 | }); 218 | }); 219 | }); 220 | }); 221 | 222 | describe('when access token is invalid', () => { 223 | it('returns NotAuthenticated error', done => { 224 | data.accessToken = 'invalid'; 225 | socket.send('authenticate', data, error => { 226 | expect(error.code).to.equal(401); 227 | done(); 228 | }); 229 | }); 230 | }); 231 | 232 | describe('when access token is missing', () => { 233 | it('returns NotAuthenticated error', done => { 234 | delete data.accessToken; 235 | socket.send('authenticate', data, error => { 236 | expect(error.code).to.equal(401); 237 | done(); 238 | }); 239 | }); 240 | }); 241 | 242 | describe('when access token is expired', () => { 243 | it('returns NotAuthenticated error', done => { 244 | data.accessToken = expiredToken; 245 | socket.send('authenticate', data, error => { 246 | expect(error.code).to.equal(401); 247 | done(); 248 | }); 249 | }); 250 | }); 251 | 252 | describe('when missing strategy it uses the auth strategy specified on the server', () => { 253 | it('returns an accessToken', done => { 254 | delete data.strategy; 255 | socket.send('authenticate', data, (error, response) => { 256 | expect(error).to.equal(null); 257 | expect(response.accessToken).to.not.equal(undefined); 258 | done(); 259 | }); 260 | }); 261 | }); 262 | }); 263 | }); 264 | 265 | describe('when calling a protected service method', () => { 266 | describe('when not authenticated', () => { 267 | it('returns NotAuthenticated error', done => { 268 | socket.send('users::find', {}, error => { 269 | expect(error.code).to.equal(401); 270 | done(); 271 | }); 272 | }); 273 | }); 274 | 275 | describe('when access token is expired', () => { 276 | it('returns NotAuthenticated error', done => { 277 | const data = { 278 | strategy: 'local', 279 | email: 'admin@feathersjs.com', 280 | password: 'admin' 281 | }; 282 | 283 | expiringSocket.send('authenticate', data, (error, response) => { 284 | expect(error).to.not.be.ok; 285 | expect(response).to.be.ok; 286 | // Wait for the accessToken to expire 287 | setTimeout(function () { 288 | expiringSocket.send('users::find', {}, (error, response) => { 289 | expect(error.code).to.equal(401); 290 | done(); 291 | }); 292 | }, 1000); 293 | }); 294 | }); 295 | }); 296 | 297 | describe('when authenticated', () => { 298 | it('returns data', done => { 299 | const data = { 300 | strategy: 'jwt', 301 | accessToken 302 | }; 303 | 304 | socket.send('authenticate', data, (error, response) => { 305 | expect(error).to.not.be.ok; 306 | expect(response).to.be.ok; 307 | socket.send('users::find', {}, (error, response) => { 308 | expect(error).to.not.be.ok; 309 | expect(response.length).to.equal(1); 310 | expect(response[0].id).to.equal(0); 311 | done(); 312 | }); 313 | }); 314 | }); 315 | }); 316 | }); 317 | 318 | describe('when calling an un-protected service method', () => { 319 | describe('when not authenticated', () => { 320 | it('returns data', done => { 321 | socket.send('users::get', 0, (error, response) => { 322 | expect(error).to.not.be.ok; 323 | expect(response.id).to.equal(0); 324 | done(); 325 | }); 326 | }); 327 | }); 328 | 329 | describe('when access token is expired', () => { 330 | it('returns data', done => { 331 | const data = { 332 | strategy: 'local', 333 | email: 'admin@feathersjs.com', 334 | password: 'admin' 335 | }; 336 | 337 | expiringSocket.send('authenticate', data, (error, response) => { 338 | expect(error).to.not.be.ok; 339 | expect(response).to.be.ok; 340 | // Wait for the accessToken to expire 341 | setTimeout(function () { 342 | socket.send('users::get', 0, (error, response) => { 343 | expect(error).to.not.be.ok; 344 | expect(response.id).to.equal(0); 345 | done(); 346 | }); 347 | }, 1000); 348 | }); 349 | }); 350 | }); 351 | 352 | describe('when authenticated', () => { 353 | it('returns data', done => { 354 | const data = { 355 | strategy: 'jwt', 356 | accessToken 357 | }; 358 | 359 | socket.send('authenticate', data, (error, response) => { 360 | expect(error).to.not.be.ok; 361 | expect(response).to.be.ok; 362 | socket.send('users::get', 0, (error, response) => { 363 | expect(error).to.not.be.ok; 364 | expect(response.id).to.equal(0); 365 | done(); 366 | }); 367 | }); 368 | }); 369 | }); 370 | }); 371 | 372 | describe.skip('when redirects are enabled', () => { 373 | let data; 374 | 375 | beforeEach(() => { 376 | data = { 377 | strategy: 'local', 378 | email: 'admin@feathersjs.com', 379 | password: 'admin' 380 | }; 381 | }); 382 | 383 | describe('authentication succeeds', () => { 384 | it('redirects', done => { 385 | socket.send('authenticate', data, (error, response) => { 386 | expect(error).to.not.be.ok; 387 | expect(response.redirect).to.equal(true); 388 | expect(response.url).to.be.ok; 389 | done(); 390 | }); 391 | }); 392 | }); 393 | 394 | describe('authentication fails', () => { 395 | it('redirects', done => { 396 | delete data.password; 397 | socket.send('authenticate', data, (error, response) => { 398 | expect(error).to.not.be.ok; 399 | expect(response.redirect).to.equal(true); 400 | expect(response.url).to.be.ok; 401 | done(); 402 | }); 403 | }); 404 | }); 405 | }); 406 | 407 | describe('events', () => { 408 | let data; 409 | 410 | beforeEach(() => { 411 | data = { 412 | strategy: 'local', 413 | email: 'admin@feathersjs.com', 414 | password: 'admin' 415 | }; 416 | }); 417 | 418 | describe('when authentication succeeds', () => { 419 | it('emits login event', done => { 420 | app.once('login', function (auth, info) { 421 | expect(info.provider).to.equal('primus'); 422 | expect(info.socket).to.exist; 423 | expect(info.connection).to.exist; 424 | done(); 425 | }); 426 | 427 | socket.send('authenticate', data); 428 | }); 429 | }); 430 | 431 | describe('authentication fails', () => { 432 | it('does not emit login event', done => { 433 | data.password = 'invalid'; 434 | const handler = sinon.spy(); 435 | app.once('login', handler); 436 | 437 | socket.send('authenticate', data, error => { 438 | expect(error.code).to.equal(401); 439 | 440 | setTimeout(function () { 441 | expect(handler).to.not.have.been.called; 442 | done(); 443 | }, 100); 444 | }); 445 | }); 446 | }); 447 | 448 | describe('when logout succeeds', () => { 449 | it('emits logout event', done => { 450 | app.once('logout', function (auth, info) { 451 | expect(info.provider).to.equal('primus'); 452 | expect(info.socket).to.exist; 453 | expect(info.connection).to.exist; 454 | done(); 455 | }); 456 | 457 | socket.send('authenticate', data, (error, response) => { 458 | expect(error).to.not.be.ok; 459 | expect(response).to.be.ok; 460 | socket.send('logout', data); 461 | }); 462 | }); 463 | }); 464 | }); 465 | }); 466 | -------------------------------------------------------------------------------- /test/integration/rest.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const merge = require('lodash.merge'); 3 | const request = require('superagent'); 4 | const createApplication = require('../fixtures/server'); 5 | const chai = require('chai'); 6 | const sinon = require('sinon'); 7 | const sinonChai = require('sinon-chai'); 8 | const { expect } = chai; 9 | 10 | chai.use(sinonChai); 11 | 12 | describe('REST authentication', function () { 13 | const port = 8996; 14 | const baseURL = `http://localhost:${port}`; 15 | const app = createApplication({ secret: 'supersecret' }); 16 | let server; 17 | let expiredToken; 18 | let accessToken; 19 | 20 | before(done => { 21 | const options = merge({}, app.get('authentication'), { jwt: { expiresIn: '1ms' } }); 22 | app.passport.createJWT({}, options) 23 | .then(token => { 24 | expiredToken = token; 25 | return app.passport.createJWT({ userId: 0 }, app.get('authentication')); 26 | }) 27 | .then(token => { 28 | accessToken = token; 29 | server = app.listen(port); 30 | server.once('listening', () => done()); 31 | }); 32 | }); 33 | 34 | after(() => server.close()); 35 | 36 | describe('Authenticating against auth service', () => { 37 | describe('Using local strategy', () => { 38 | let data; 39 | 40 | beforeEach(() => { 41 | data = { 42 | strategy: 'local', 43 | email: 'admin@feathersjs.com', 44 | password: 'admin' 45 | }; 46 | }); 47 | 48 | describe('when using valid credentials', () => { 49 | it('returns a valid access token', () => { 50 | return request 51 | .post(`${baseURL}/authentication`) 52 | .send(data) 53 | .then(response => { 54 | expect(response.body.accessToken).to.exist; 55 | return app.passport.verifyJWT(response.body.accessToken, app.get('authentication')); 56 | }).then(payload => { 57 | expect(payload).to.exist; 58 | expect(payload.iss).to.equal('feathers'); 59 | expect(payload.userId).to.equal(0); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('when using invalid credentials', () => { 65 | it('returns NotAuthenticated error', () => { 66 | data.password = 'invalid'; 67 | return request 68 | .post(`${baseURL}/authentication`) 69 | .send(data) 70 | .then(response => { 71 | expect(response).to.not.be.ok; // should not get here 72 | }) 73 | .catch(error => { 74 | expect(error.status).to.equal(401); 75 | expect(error.response.body.name).to.equal('NotAuthenticated'); 76 | }); 77 | }); 78 | }); 79 | 80 | describe('when missing credentials', () => { 81 | it('returns NotAuthenticated error', () => { 82 | return request 83 | .post(`${baseURL}/authentication`) 84 | .send({}) 85 | .then(response => { 86 | expect(response).to.not.be.ok; // should not get here 87 | }) 88 | .catch(error => { 89 | expect(error.status).to.equal(401); 90 | expect(error.response.body.name).to.equal('NotAuthenticated'); 91 | }); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('Using JWT strategy via body', () => { 97 | let data; 98 | 99 | beforeEach(() => { 100 | data = { accessToken }; 101 | }); 102 | 103 | describe('when using a valid access token', () => { 104 | it('returns a valid access token', () => { 105 | return request 106 | .post(`${baseURL}/authentication`) 107 | .send(data) 108 | .then(response => { 109 | expect(response.body.accessToken).to.exist; 110 | return app.passport.verifyJWT(response.body.accessToken, app.get('authentication')); 111 | }).then(payload => { 112 | expect(payload).to.exist; 113 | expect(payload.iss).to.equal('feathers'); 114 | expect(payload.userId).to.equal(0); 115 | }); 116 | }); 117 | }); 118 | 119 | describe.skip('when using a valid refresh token', () => { 120 | it('returns a valid access token', () => { 121 | return request 122 | .post(`${baseURL}/authentication`) 123 | .send(data) 124 | .then(response => { 125 | expect(response.body.accessToken).to.exist; 126 | return app.passport.verifyJWT(response.body.accessToken, app.get('authentication')); 127 | }).then(payload => { 128 | expect(payload).to.exist; 129 | expect(payload.iss).to.equal('feathers'); 130 | expect(payload.userId).to.equal(0); 131 | }); 132 | }); 133 | }); 134 | 135 | describe('when access token is invalid', () => { 136 | it('returns not authenticated error', () => { 137 | data.accessToken = 'invalid'; 138 | return request 139 | .post(`${baseURL}/authentication`) 140 | .send(data) 141 | .then(response => { 142 | expect(response).to.not.be.ok; // should not get here 143 | }) 144 | .catch(error => { 145 | expect(error.status).to.equal(401); 146 | expect(error.response.body.name).to.equal('NotAuthenticated'); 147 | }); 148 | }); 149 | }); 150 | 151 | describe('when access token is expired', () => { 152 | it('returns not authenticated error', () => { 153 | data.accessToken = expiredToken; 154 | return request 155 | .post(`${baseURL}/authentication`) 156 | .send(data) 157 | .then(response => { 158 | expect(response).to.not.be.ok; // should not get here 159 | }) 160 | .catch(error => { 161 | expect(error.status).to.equal(401); 162 | expect(error.response.body.name).to.equal('NotAuthenticated'); 163 | }); 164 | }); 165 | }); 166 | 167 | describe('when access token is missing', () => { 168 | it('returns not authenticated error', () => { 169 | delete data.accessToken; 170 | return request 171 | .post(`${baseURL}/authentication`) 172 | .send(data) 173 | .then(response => { 174 | expect(response).to.not.be.ok; // should not get here 175 | }) 176 | .catch(error => { 177 | expect(error.status).to.equal(401); 178 | expect(error.response.body.name).to.equal('NotAuthenticated'); 179 | }); 180 | }); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('when calling a protected service method', () => { 186 | describe('when header is invalid', () => { 187 | it('returns not authenticated error', () => { 188 | return request 189 | .get(`${baseURL}/users`) 190 | .set('X-Authorization', accessToken) 191 | .then(response => { 192 | expect(response).to.not.be.ok; // should not get here 193 | }) 194 | .catch(error => { 195 | expect(error.status).to.equal(401); 196 | expect(error.response.body.name).to.equal('NotAuthenticated'); 197 | }); 198 | }); 199 | }); 200 | 201 | describe('when token is invalid', () => { 202 | it('returns not authenticated error', () => { 203 | return request 204 | .get(`${baseURL}/users`) 205 | .set('Authorization', 'invalid') 206 | .then(response => { 207 | expect(response).to.not.be.ok; // should not get here 208 | }) 209 | .catch(error => { 210 | expect(error.status).to.equal(401); 211 | expect(error.response.body.name).to.equal('NotAuthenticated'); 212 | }); 213 | }); 214 | }); 215 | 216 | describe('when token is expired', () => { 217 | it('returns not authenticated error', () => { 218 | return request 219 | .get(`${baseURL}/users`) 220 | .set('Authorization', expiredToken) 221 | .then(response => { 222 | expect(response).to.not.be.ok; // should not get here 223 | }) 224 | .catch(error => { 225 | expect(error.status).to.equal(401); 226 | expect(error.response.body.name).to.equal('NotAuthenticated'); 227 | }); 228 | }); 229 | }); 230 | 231 | describe('when token is valid', () => { 232 | it('returns data', () => { 233 | return request 234 | .get(`${baseURL}/users`) 235 | .set('Authorization', accessToken) 236 | .then(response => { 237 | expect(response.body.length).to.equal(1); 238 | expect(response.body[0].id).to.equal(0); 239 | }); 240 | }); 241 | }); 242 | }); 243 | 244 | describe('when calling an un-protected service method', () => { 245 | describe('when header is invalid', () => { 246 | it('returns data', () => { 247 | return request 248 | .get(`${baseURL}/users/0`) 249 | .set('X-Authorization', accessToken) 250 | .then(response => { 251 | expect(response.body.id).to.equal(0); 252 | }); 253 | }); 254 | }); 255 | describe('when token is invalid', () => { 256 | it('returns data', () => { 257 | return request 258 | .get(`${baseURL}/users/0`) 259 | .set('Authorization', 'invalid') 260 | .then(response => { 261 | expect(response.body.id).to.equal(0); 262 | }); 263 | }); 264 | }); 265 | 266 | describe('when token is expired', () => { 267 | it('returns data', () => { 268 | return request 269 | .get(`${baseURL}/users/0`) 270 | .set('Authorization', expiredToken) 271 | .then(response => { 272 | expect(response.body.id).to.equal(0); 273 | }); 274 | }); 275 | }); 276 | 277 | describe('when token is valid', () => { 278 | it('returns data', () => { 279 | return request 280 | .get(`${baseURL}/users/0`) 281 | .set('Authorization', accessToken) 282 | .then(response => { 283 | expect(response.body.id).to.equal(0); 284 | }); 285 | }); 286 | }); 287 | }); 288 | 289 | describe('when calling a protected custom route', () => { 290 | describe('when header is invalid', () => { 291 | it('returns not authenticated error', () => { 292 | return request 293 | .get(`${baseURL}/protected`) 294 | .set('Content-Type', 'application/json') 295 | .set('X-Authorization', accessToken) 296 | .then(response => { 297 | expect(response).to.not.be.ok; // should not get here 298 | }) 299 | .catch(error => { 300 | expect(error.status).to.equal(401); 301 | expect(error.response.body.name).to.equal('NotAuthenticated'); 302 | }); 303 | }); 304 | }); 305 | 306 | describe('when token is invalid', () => { 307 | it('returns not authenticated error', () => { 308 | return request 309 | .get(`${baseURL}/protected`) 310 | .set('Content-Type', 'application/json') 311 | .set('Authorization', 'invalid') 312 | .then(response => { 313 | expect(response).to.not.be.ok; // should not get here 314 | }) 315 | .catch(error => { 316 | expect(error.status).to.equal(401); 317 | expect(error.response.body.name).to.equal('NotAuthenticated'); 318 | }); 319 | }); 320 | }); 321 | 322 | describe('when token is expired', () => { 323 | it('returns not authenticated error', () => { 324 | return request 325 | .get(`${baseURL}/protected`) 326 | .set('Content-Type', 'application/json') 327 | .set('Authorization', expiredToken) 328 | .then(response => { 329 | expect(response).to.not.be.ok; // should not get here 330 | }) 331 | .catch(error => { 332 | expect(error.status).to.equal(401); 333 | expect(error.response.body.name).to.equal('NotAuthenticated'); 334 | }); 335 | }); 336 | }); 337 | 338 | describe('when token is valid', () => { 339 | it('returns data', () => { 340 | return request 341 | .get(`${baseURL}/protected`) 342 | .set('Content-Type', 'application/json') 343 | .set('Authorization', accessToken) 344 | .then(response => { 345 | expect(response.body.success).to.equal(true); 346 | }); 347 | }); 348 | }); 349 | }); 350 | 351 | describe('when calling an un-protected custom route', () => { 352 | describe('when header is invalid', () => { 353 | it('returns data', () => { 354 | return request 355 | .get(`${baseURL}/unprotected`) 356 | .set('Content-Type', 'application/json') 357 | .set('X-Authorization', accessToken) 358 | .then(response => { 359 | expect(response.body.success).to.equal(true); 360 | }); 361 | }); 362 | }); 363 | describe('when token is invalid', () => { 364 | it('returns data', () => { 365 | return request 366 | .get(`${baseURL}/unprotected`) 367 | .set('Content-Type', 'application/json') 368 | .set('Authorization', 'invalid') 369 | .then(response => { 370 | expect(response.body.success).to.equal(true); 371 | }); 372 | }); 373 | }); 374 | 375 | describe('when token is expired', () => { 376 | it('returns data', () => { 377 | return request 378 | .get(`${baseURL}/unprotected`) 379 | .set('Content-Type', 'application/json') 380 | .set('Authorization', expiredToken) 381 | .then(response => { 382 | expect(response.body.success).to.equal(true); 383 | }); 384 | }); 385 | }); 386 | 387 | describe('when token is valid', () => { 388 | it('returns data', () => { 389 | return request 390 | .get(`${baseURL}/unprotected`) 391 | .set('Content-Type', 'application/json') 392 | .set('Authorization', accessToken) 393 | .then(response => { 394 | expect(response.body.success).to.equal(true); 395 | }); 396 | }); 397 | }); 398 | }); 399 | 400 | describe('when redirects are enabled', () => { 401 | let data; 402 | 403 | beforeEach(() => { 404 | data = { 405 | email: 'admin@feathersjs.com', 406 | password: 'admin' 407 | }; 408 | }); 409 | 410 | describe('authentication succeeds', () => { 411 | it('redirects', () => { 412 | return request 413 | .post(`${baseURL}/login`) 414 | .send(data) 415 | .then(response => { 416 | expect(response.status).to.equal(200); 417 | expect(response.body).to.deep.equal({ success: true }); 418 | }); 419 | }); 420 | }); 421 | 422 | describe('authentication fails', () => { 423 | it('redirects', () => { 424 | data.password = 'invalid'; 425 | return request 426 | .post(`${baseURL}/login`) 427 | .send(data) 428 | .then(response => { 429 | expect(response.body.success).to.equal(false); 430 | }); 431 | }); 432 | }); 433 | }); 434 | 435 | describe('events', () => { 436 | let data; 437 | 438 | beforeEach(() => { 439 | data = { 440 | strategy: 'local', 441 | email: 'admin@feathersjs.com', 442 | password: 'admin' 443 | }; 444 | }); 445 | 446 | describe('when authentication succeeds', () => { 447 | it('emits login event', done => { 448 | app.once('login', function (auth, info) { 449 | expect(info.provider).to.equal('rest'); 450 | expect(info.req).to.exist; 451 | expect(info.res).to.exist; 452 | done(); 453 | }); 454 | 455 | request.post(`${baseURL}/authentication`).send(data).end(); 456 | }); 457 | }); 458 | 459 | describe('authentication fails', () => { 460 | it('does not emit login event', done => { 461 | data.password = 'invalid'; 462 | const handler = sinon.spy(); 463 | app.once('login', handler); 464 | 465 | request.post(`${baseURL}/authentication`) 466 | .send(data) 467 | .then(response => { 468 | expect(response).to.not.be.ok; // should not get here 469 | }) 470 | .catch(error => { 471 | expect(error.status).to.equal(401); 472 | 473 | setTimeout(function () { 474 | expect(handler).to.not.have.been.called; 475 | done(); 476 | }, 100); 477 | }); 478 | }); 479 | }); 480 | 481 | describe('when logout succeeds', () => { 482 | it('emits logout event', done => { 483 | app.once('logout', function (auth, info) { 484 | expect(info.provider).to.equal('rest'); 485 | expect(info.req).to.exist; 486 | expect(info.res).to.exist; 487 | done(); 488 | }); 489 | 490 | request.post(`${baseURL}/authentication`) 491 | .send(data) 492 | .then(response => { 493 | return request 494 | .del(`${baseURL}/authentication`) 495 | .set('Content-Type', 'application/json') 496 | .set('Authorization', response.body.accessToken); 497 | }) 498 | .then(response => { 499 | expect(response.status).to.equal(200); 500 | }); 501 | }); 502 | }); 503 | }); 504 | }); 505 | -------------------------------------------------------------------------------- /test/integration/socketio.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const merge = require('lodash.merge'); 3 | const io = require('socket.io-client'); 4 | const createApplication = require('../fixtures/server'); 5 | const chai = require('chai'); 6 | const sinon = require('sinon'); 7 | const sinonChai = require('sinon-chai'); 8 | const clone = require('lodash.clone'); 9 | const { expect } = chai; 10 | 11 | chai.use(sinonChai); 12 | 13 | describe('Socket.io authentication', function () { 14 | const port = 8997; 15 | const baseURL = `http://localhost:${port}`; 16 | const app = createApplication({ secret: 'supersecret' }, 'socketio'); 17 | const expiringApp = createApplication({ 18 | secret: 'supersecret', 19 | jwt: { expiresIn: '500ms' } 20 | }, 'socketio'); 21 | const hook = sinon.spy(function (hook) {}); 22 | app.service('authentication').hooks({ 23 | before: { 24 | create: [ hook ] 25 | } 26 | }); 27 | 28 | let server; 29 | let socket; 30 | let serverSocket; 31 | let expiringServer; 32 | let expiringSocket; 33 | let expiredToken; 34 | let accessToken; 35 | 36 | before(done => { 37 | const options = merge({}, app.get('authentication'), { jwt: { expiresIn: '1ms' } }); 38 | app.passport.createJWT({}, options) 39 | .then(token => { 40 | expiredToken = token; 41 | return app.passport.createJWT({ userId: 0 }, app.get('authentication')); 42 | }) 43 | .then(token => { 44 | accessToken = token; 45 | expiringServer = expiringApp.listen(1336); 46 | expiringServer.once('listening', () => { 47 | server = app.listen(port); 48 | server.once('listening', () => { 49 | app.io.on('connect', s => { serverSocket = s; }); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | }); 55 | 56 | beforeEach(() => { 57 | expiringSocket = io('http://localhost:1336'); 58 | socket = io(baseURL); 59 | }); 60 | 61 | afterEach(() => { 62 | hook.resetHistory(); 63 | }); 64 | 65 | after(() => { 66 | expiringServer.close(); 67 | server.close(); 68 | }); 69 | 70 | describe('Authenticating against auth service', () => { 71 | describe('Using local strategy', () => { 72 | let data; 73 | 74 | beforeEach(() => { 75 | data = { 76 | strategy: 'local', 77 | email: 'admin@feathersjs.com', 78 | password: 'admin' 79 | }; 80 | }); 81 | 82 | describe('when using valid credentials', () => { 83 | it('returns a valid access token, does not send real-time event', done => { 84 | socket.once('authentication created', () => 85 | done(new Error('real-time events for authentication should not be emitted')) 86 | ); 87 | 88 | socket.emit('authenticate', data, (error, response) => { 89 | expect(error).to.not.equal(undefined); 90 | expect(response.accessToken).to.exist; 91 | app.passport.verifyJWT(response.accessToken, app.get('authentication')).then(payload => { 92 | expect(payload).to.exist; 93 | expect(payload.iss).to.equal('feathers'); 94 | expect(payload.userId).to.equal(0); 95 | expect(hook).to.be.calledWith(sinon.match({ params: { data: 'Hello world' } })); 96 | done(); 97 | }); 98 | }); 99 | }); 100 | 101 | it('sets the user on the socket', done => { 102 | socket.emit('authenticate', data, (error, response) => { 103 | expect(error).to.not.equal(undefined); 104 | expect(response.accessToken).to.exist; 105 | expect(serverSocket.feathers.user).to.not.equal(undefined); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('does never publish events from the authentication service', done => { 111 | socket.once('authentication created', () => done(new Error('Should not get here'))); 112 | 113 | socket.emit('authenticate', data, (error, response) => { 114 | expect(error).to.not.equal(undefined); 115 | expect(response.accessToken).to.exist; 116 | expect(serverSocket.feathers.user).to.not.equal(undefined); 117 | done(); 118 | }); 119 | }); 120 | 121 | it('updates the user on the socket', done => { 122 | socket.emit('authenticate', data, (error, response) => { 123 | expect(error).to.not.equal(undefined); 124 | // Clone the socket user and replace it with the clone so that feathers-memory 125 | // doesn't have a reference to the same object. 126 | const socketUser = clone(serverSocket.feathers.user); 127 | serverSocket.feathers.user = socketUser; 128 | 129 | const email = 'test@feathersjs.com'; 130 | const oldEmail = socketUser.email; 131 | 132 | app.service('users').patch(socketUser.id, { email }) 133 | .then(user => { 134 | expect(socketUser.email).to.equal(email); 135 | return app.service('users').patch(socketUser.id, { email: oldEmail }); 136 | }) 137 | .then(user => { 138 | expect(socketUser.email).to.equal(oldEmail); 139 | done(); 140 | }); 141 | }); 142 | }); 143 | 144 | it('sets entity specified in strategy', done => { 145 | data.strategy = 'org-local'; 146 | socket.emit('authenticate', data, (error, response) => { 147 | expect(error).to.not.be.ok; 148 | expect(response.accessToken).to.exist; 149 | expect(serverSocket.feathers.org).to.not.equal(undefined); 150 | done(); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('when using invalid credentials', () => { 156 | it('returns NotAuthenticated error', done => { 157 | data.password = 'invalid'; 158 | socket.emit('authenticate', data, error => { 159 | expect(error.code).to.equal(401); 160 | done(); 161 | }); 162 | }); 163 | 164 | it('returns NotAuthenticated error when strategy is invalid', done => { 165 | delete data.strategy; 166 | 167 | socket.emit('authenticate', data, error => { 168 | expect(error.code).to.equal(401); 169 | done(); 170 | }); 171 | }); 172 | 173 | it('returns NotAuthenticated error when data is not an object', done => { 174 | socket.emit('authenticate', undefined, error => { 175 | expect(error.code).to.equal(401); 176 | done(); 177 | }); 178 | }); 179 | 180 | it('returns NotAuthenticated error when data is not passed', done => { 181 | socket.emit('authenticate', error => { 182 | expect(error.code).to.equal(401); 183 | done(); 184 | }); 185 | }); 186 | }); 187 | 188 | describe('when missing credentials', () => { 189 | it('returns BadRequest error', done => { 190 | socket.emit('authenticate', { strategy: 'local' }, error => { 191 | expect(error.code).to.equal(400); 192 | done(); 193 | }); 194 | }); 195 | }); 196 | 197 | describe('when missing strategy and server strategy does not match', () => { 198 | it('returns NotAuthenticated error', done => { 199 | delete data.strategy; 200 | socket.emit('authenticate', data, error => { 201 | expect(error.code).to.equal(401); 202 | done(); 203 | }); 204 | }); 205 | }); 206 | }); 207 | 208 | describe('Using JWT strategy', () => { 209 | let data; 210 | 211 | beforeEach(() => { 212 | data = { 213 | strategy: 'jwt', 214 | accessToken 215 | }; 216 | }); 217 | 218 | describe('when using a valid access token', () => { 219 | it('returns a valid access token', done => { 220 | socket.emit('authenticate', data, (error, response) => { 221 | expect(error).to.not.be.ok; 222 | expect(response.accessToken).to.exist; 223 | app.passport.verifyJWT(response.accessToken, app.get('authentication')).then(payload => { 224 | expect(payload).to.exist; 225 | expect(payload.iss).to.equal('feathers'); 226 | expect(payload.userId).to.equal(0); 227 | done(); 228 | }); 229 | }); 230 | }); 231 | }); 232 | 233 | describe.skip('when using a valid refresh token', () => { 234 | it('returns a valid access token', done => { 235 | delete data.accessToken; 236 | data.refreshToken = 'refresh'; 237 | socket.emit('authenticate', data, (error, response) => { 238 | expect(error).to.not.be.ok; 239 | expect(response.accessToken).to.exist; 240 | app.passport.verifyJWT(response.accessToken, app.get('authentication')).then(payload => { 241 | expect(payload).to.exist; 242 | expect(payload.iss).to.equal('feathers'); 243 | expect(payload.userId).to.equal(0); 244 | done(); 245 | }); 246 | }); 247 | }); 248 | }); 249 | 250 | describe('when access token is invalid', () => { 251 | it('returns NotAuthenticated error', done => { 252 | data.accessToken = 'invalid'; 253 | socket.emit('authenticate', data, error => { 254 | expect(error.code).to.equal(401); 255 | done(); 256 | }); 257 | }); 258 | }); 259 | 260 | describe('when access token is missing', () => { 261 | it('returns NotAuthenticated error', done => { 262 | delete data.accessToken; 263 | socket.emit('authenticate', data, error => { 264 | expect(error.code).to.equal(401); 265 | done(); 266 | }); 267 | }); 268 | }); 269 | 270 | describe('when access token is expired', () => { 271 | it('returns NotAuthenticated error', done => { 272 | data.accessToken = expiredToken; 273 | socket.emit('authenticate', data, error => { 274 | expect(error.code).to.equal(401); 275 | done(); 276 | }); 277 | }); 278 | }); 279 | 280 | describe('when missing strategy it uses the auth strategy specified on the server', () => { 281 | it('returns an accessToken', done => { 282 | delete data.strategy; 283 | socket.emit('authenticate', data, (error, response) => { 284 | expect(error).to.equal(null); 285 | expect(response.accessToken).to.not.equal(undefined); 286 | done(); 287 | }); 288 | }); 289 | }); 290 | }); 291 | }); 292 | 293 | describe('when expiry time is very long', () => { 294 | const longExpiringApp = createApplication({ 295 | secret: 'supersecret', 296 | jwt: { expiresIn: '1y' } 297 | }, 'socketio'); 298 | 299 | let longExpiringServer; 300 | let longExpiringSocket; 301 | 302 | before(done => { 303 | longExpiringServer = longExpiringApp.listen(1338); 304 | longExpiringServer.once('listening', () => { 305 | longExpiringSocket = io('http://localhost:1338'); 306 | done(); 307 | }); 308 | }); 309 | 310 | after(() => { 311 | longExpiringServer.close(); 312 | }); 313 | 314 | it('should not immediately logout', done => { 315 | const data = { 316 | strategy: 'local', 317 | email: 'admin@feathersjs.com', 318 | password: 'admin' 319 | }; 320 | 321 | longExpiringSocket.emit('authenticate', data, (error, response) => { 322 | expect(error).to.not.be.ok; 323 | expect(response).to.be.ok; 324 | // Wait for a little bit 325 | setTimeout(function () { 326 | longExpiringSocket.emit('users::find', {}, (error, response) => { 327 | expect(error).to.not.be.ok; 328 | expect(response).to.be.ok; 329 | done(); 330 | }); 331 | }, 100); 332 | }); 333 | }); 334 | }); 335 | 336 | describe('reauthenticating extends jwt expiry', () => { 337 | const longExpiringApp = createApplication({ 338 | secret: 'supersecret', 339 | jwt: { expiresIn: '10s' } 340 | }, 'socketio'); 341 | 342 | let longExpiringServer; 343 | let longExpiringSocket; 344 | 345 | before(done => { 346 | longExpiringServer = longExpiringApp.listen(1338); 347 | longExpiringServer.once('listening', () => { 348 | longExpiringSocket = io('http://localhost:1338'); 349 | done(); 350 | }); 351 | }); 352 | 353 | after(() => { 354 | longExpiringServer.close(); 355 | }); 356 | 357 | it('should not be logout after reauthenticate', done => { 358 | const data = { 359 | strategy: 'local', 360 | email: 'admin@feathersjs.com', 361 | password: 'admin' 362 | }; 363 | 364 | // token expires in 10 secs 365 | longExpiringSocket.emit('authenticate', data, (error, response) => { 366 | expect(error).to.not.be.ok; 367 | expect(response).to.be.ok; 368 | 369 | // reauth at 5 secs 370 | setTimeout(function () { 371 | longExpiringSocket.emit('authenticate', response, (error, response) => { 372 | expect(error).to.not.be.ok; 373 | expect(response).to.be.ok; 374 | }); 375 | }, 5 * 1000); 376 | 377 | // check if token expiry exceeds 10 secs 378 | setTimeout(function () { 379 | longExpiringSocket.emit('users::find', {}, (error, response) => { 380 | expect(error).to.not.be.ok; 381 | expect(response).to.be.ok; 382 | done(); 383 | }); 384 | }, 14 * 1000); 385 | }); 386 | }); 387 | }); 388 | 389 | describe('when calling a protected service method', () => { 390 | describe('when not authenticated', () => { 391 | it('returns NotAuthenticated error', done => { 392 | socket.emit('users::find', {}, error => { 393 | expect(error.code).to.equal(401); 394 | done(); 395 | }); 396 | }); 397 | }); 398 | 399 | describe('when access token is expired', () => { 400 | it('returns NotAuthenticated error', done => { 401 | const data = { 402 | strategy: 'local', 403 | email: 'admin@feathersjs.com', 404 | password: 'admin' 405 | }; 406 | 407 | expiringSocket.emit('authenticate', data, (error, response) => { 408 | expect(error).to.not.be.ok; 409 | expect(response).to.be.ok; 410 | // Wait for the accessToken to expire 411 | setTimeout(function () { 412 | expiringSocket.emit('users::find', {}, (error, response) => { 413 | expect(error.code).to.equal(401); 414 | done(); 415 | }); 416 | }, 1000); 417 | }); 418 | }); 419 | }); 420 | 421 | describe('when authenticated', () => { 422 | it('returns data', done => { 423 | const data = { 424 | strategy: 'jwt', 425 | accessToken 426 | }; 427 | 428 | socket.emit('authenticate', data, (error, response) => { 429 | expect(error).to.not.be.ok; 430 | expect(response).to.be.ok; 431 | socket.emit('users::find', {}, (error, response) => { 432 | expect(error).to.not.be.ok; 433 | expect(response.length).to.equal(1); 434 | expect(response[0].id).to.equal(0); 435 | done(); 436 | }); 437 | }); 438 | }); 439 | }); 440 | }); 441 | 442 | describe('when calling an un-protected service method', () => { 443 | describe('when not authenticated', () => { 444 | it('returns data', done => { 445 | socket.emit('users::get', 0, (error, response) => { 446 | expect(error).to.not.be.ok; 447 | expect(response.id).to.equal(0); 448 | done(); 449 | }); 450 | }); 451 | }); 452 | 453 | describe('when access token is expired', () => { 454 | it('returns data', done => { 455 | const data = { 456 | strategy: 'local', 457 | email: 'admin@feathersjs.com', 458 | password: 'admin' 459 | }; 460 | 461 | expiringSocket.emit('authenticate', data, (error, response) => { 462 | expect(error).to.not.be.ok; 463 | expect(response).to.be.ok; 464 | // Wait for the accessToken to expire 465 | setTimeout(function () { 466 | socket.emit('users::get', 0, (error, response) => { 467 | expect(error).to.not.be.ok; 468 | expect(response.id).to.equal(0); 469 | done(); 470 | }); 471 | }, 1000); 472 | }); 473 | }); 474 | }); 475 | 476 | describe('when authenticated', () => { 477 | it('returns data', done => { 478 | const data = { 479 | strategy: 'jwt', 480 | accessToken 481 | }; 482 | 483 | socket.emit('authenticate', data, (error, response) => { 484 | expect(error).to.not.equal(undefined); 485 | expect(response).to.be.ok; 486 | socket.emit('users::get', 0, (error, response) => { 487 | expect(error).to.not.equal(undefined); 488 | expect(response.id).to.equal(0); 489 | done(); 490 | }); 491 | }); 492 | }); 493 | }); 494 | }); 495 | 496 | describe.skip('when redirects are enabled', () => { 497 | let data; 498 | 499 | beforeEach(() => { 500 | data = { 501 | strategy: 'local', 502 | email: 'admin@feathersjs.com', 503 | password: 'admin' 504 | }; 505 | }); 506 | 507 | describe('authentication succeeds', () => { 508 | it('redirects', done => { 509 | socket.emit('authenticate', data, (error, response) => { 510 | expect(error).to.not.equal(undefined); 511 | expect(response.redirect).to.equal(true); 512 | expect(response.url).to.be.ok; 513 | done(); 514 | }); 515 | }); 516 | }); 517 | 518 | describe('authentication fails', () => { 519 | it('redirects', done => { 520 | delete data.password; 521 | socket.emit('authenticate', data, (error, response) => { 522 | expect(error).to.not.equal(undefined); 523 | expect(response.redirect).to.equal(true); 524 | expect(response.url).to.be.ok; 525 | done(); 526 | }); 527 | }); 528 | }); 529 | }); 530 | 531 | describe('events', () => { 532 | let data; 533 | 534 | beforeEach(() => { 535 | data = { 536 | strategy: 'local', 537 | email: 'admin@feathersjs.com', 538 | password: 'admin' 539 | }; 540 | }); 541 | 542 | describe('when authentication succeeds', () => { 543 | it('emits login event', done => { 544 | app.once('login', function (auth, info) { 545 | expect(info.provider).to.equal('socketio'); 546 | expect(info.socket).to.exist; 547 | expect(info.connection).to.exist; 548 | done(); 549 | }); 550 | 551 | socket.emit('authenticate', data); 552 | }); 553 | }); 554 | 555 | describe('authentication fails', () => { 556 | it('does not emit login event', done => { 557 | data.password = 'invalid'; 558 | const handler = sinon.spy(); 559 | app.once('login', handler); 560 | 561 | socket.emit('authenticate', data, error => { 562 | expect(error.code).to.equal(401); 563 | 564 | setTimeout(function () { 565 | expect(handler).to.not.have.been.called; 566 | done(); 567 | }, 100); 568 | }); 569 | }); 570 | }); 571 | 572 | describe('when logout succeeds', () => { 573 | it('emits logout event', done => { 574 | app.once('logout', function (auth, info) { 575 | expect(info.provider).to.equal('socketio'); 576 | expect(info.socket).to.exist; 577 | expect(info.connection).to.exist; 578 | done(); 579 | }); 580 | 581 | socket.emit('authenticate', data, (error, response) => { 582 | expect(error).to.not.equal(undefined); 583 | expect(response).to.be.ok; 584 | socket.emit('logout', data); 585 | }); 586 | }); 587 | }); 588 | }); 589 | }); 590 | -------------------------------------------------------------------------------- /test/options.test.js: -------------------------------------------------------------------------------- 1 | const getOptions = require('../lib/options'); 2 | const { expect } = require('chai'); 3 | 4 | describe('options', () => { 5 | it('is CommonJS compatible', () => { 6 | expect(typeof require('../lib/options')).to.equal('function'); 7 | }); 8 | 9 | it('is ES6 compatible', () => { 10 | expect(typeof getOptions).to.equal('function'); 11 | }); 12 | 13 | describe('default options', () => { 14 | let options; 15 | 16 | before(() => { 17 | options = getOptions(); 18 | }); 19 | 20 | it('sets the service path', () => { 21 | expect(options.path).to.equal('/authentication'); 22 | }); 23 | 24 | it('sets the header', () => { 25 | expect(options.header).to.equal('Authorization'); 26 | }); 27 | 28 | it('sets the entity to add to the req, socket, and hook.params', () => { 29 | expect(options.entity).to.equal('user'); 30 | }); 31 | 32 | it('sets the service to lookup the entity', () => { 33 | expect(options.service).to.equal('users'); 34 | }); 35 | 36 | it('sets passReqToCallback', () => { 37 | expect(options.passReqToCallback).to.equal(true); 38 | }); 39 | 40 | it('disables sessions', () => { 41 | expect(options.session).to.equal(false); 42 | }); 43 | 44 | describe('cookie', () => { 45 | it('it is disabled', () => { 46 | expect(options.cookie.enabled).to.equal(false); 47 | }); 48 | 49 | it('sets the name to feathers-jwt', () => { 50 | expect(options.cookie.name).to.equal('feathers-jwt'); 51 | }); 52 | 53 | it('makes the cookie as httpOnly', () => { 54 | expect(options.cookie.httpOnly).to.equal(false); 55 | }); 56 | 57 | it('sets the maxAge', () => { 58 | expect(options.cookie.maxAge).to.equal(undefined); 59 | }); 60 | 61 | it('sets the cookie as secure', () => { 62 | expect(options.cookie.secure).to.equal(true); 63 | }); 64 | }); 65 | 66 | describe('jwt', () => { 67 | it('sets the header', () => { 68 | expect(options.jwt.header).to.deep.equal({ typ: 'access' }); 69 | }); 70 | 71 | it('sets the audience', () => { 72 | expect(options.jwt.audience).to.equal('https://yourdomain.com'); 73 | }); 74 | 75 | it('sets the subject', () => { 76 | expect(options.jwt.subject).to.equal('anonymous'); 77 | }); 78 | 79 | it('sets the issuer', () => { 80 | expect(options.jwt.issuer).to.equal('feathers'); 81 | }); 82 | 83 | it('sets the algorithm', () => { 84 | expect(options.jwt.algorithm).to.equal('HS256'); 85 | }); 86 | 87 | it('sets the expiresIn', () => { 88 | expect(options.jwt.expiresIn).to.equal('1d'); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('when custom options are passed', () => { 94 | it('can add new options', () => { 95 | let options = getOptions({ custom: 'custom option' }); 96 | expect(options.custom).to.equal('custom option'); 97 | }); 98 | 99 | it('can override existing options', () => { 100 | let options = getOptions({ header: 'X-Authorization' }); 101 | expect(options.header).to.equal('X-Authorization'); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/passport/authenticate.test.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const authenticate = require('../../lib/passport/authenticate'); 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const MockStrategy = require('../fixtures/strategy'); 7 | const { expect } = chai; 8 | 9 | chai.use(sinonChai); 10 | 11 | describe('passport:authenticate', () => { 12 | it('it returns a function', () => { 13 | expect(typeof authenticate()).to.equal('function'); 14 | }); 15 | 16 | describe('when authenticating with a single strategy', () => { 17 | let authenticator; 18 | let verifier; 19 | let data; 20 | let req; 21 | 22 | beforeEach(() => { 23 | req = { 24 | body: {}, 25 | headers: {}, 26 | session: {}, 27 | cookies: {} 28 | }; 29 | }); 30 | 31 | it.skip('returns an error when passport strategy is not registered', () => { 32 | const authenticator = authenticate()(passport, 'mock'); 33 | return authenticator().catch(error => { 34 | expect(error).to.not.equal(undefined); 35 | }); 36 | }); 37 | 38 | it('calls authenticate for a given strategy', () => { 39 | verifier = (cb) => cb(null, {}); 40 | const strategyOptions = { assignProperty: 'organization' }; 41 | const strategy = new MockStrategy({}, verifier); 42 | 43 | sinon.spy(strategy, 'authenticate'); 44 | 45 | passport.use(strategy); 46 | authenticator = authenticate()(passport, 'mock', strategyOptions); 47 | 48 | return authenticator(req).then(result => { 49 | expect(strategy.authenticate).to.have.been.calledWith(req, strategyOptions); 50 | strategy.authenticate.restore(); 51 | }); 52 | }); 53 | 54 | describe('passport strategy methods', () => { 55 | describe('redirect', () => { 56 | beforeEach(() => { 57 | data = { 58 | url: 'http://feathersjs.com', 59 | status: 301 60 | }; 61 | 62 | verifier = (cb) => cb(null, null, data); 63 | passport.use(new MockStrategy({}, verifier)); 64 | authenticator = authenticate()(passport, 'mock'); 65 | }); 66 | 67 | it('sets redirect true', () => { 68 | return authenticator(req).then(result => { 69 | expect(result.redirect).to.equal(true); 70 | }); 71 | }); 72 | 73 | it('sets the redirect url', () => { 74 | return authenticator(req).then(result => { 75 | expect(result.url).to.equal(data.url); 76 | }); 77 | }); 78 | 79 | it('sets the status default code', () => { 80 | return authenticator(req).then(result => { 81 | expect(result.status).to.equal(data.status); 82 | }); 83 | }); 84 | 85 | it('sets the default status code', () => { 86 | data = { 87 | url: 'http://feathersjs.com' 88 | }; 89 | 90 | verifier = (cb) => cb(null, null, data); 91 | passport.use(new MockStrategy({}, verifier)); 92 | authenticator = authenticate()(passport, 'mock'); 93 | 94 | return authenticator(req).then(result => { 95 | expect(result.status).to.equal(302); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('fail', () => { 101 | beforeEach(() => { 102 | data = { 103 | challenge: 'Missing credentials', 104 | status: 400 105 | }; 106 | 107 | verifier = (cb) => cb(null, null, data); 108 | passport.use(new MockStrategy({}, verifier)); 109 | authenticator = authenticate()(passport, 'mock'); 110 | }); 111 | 112 | it('sets fail true', () => { 113 | return authenticator(req).then(result => { 114 | expect(result.fail).to.equal(true); 115 | }); 116 | }); 117 | 118 | it('sets the challenge', () => { 119 | return authenticator(req).then(result => { 120 | expect(result.challenge).to.equal(data.challenge); 121 | }); 122 | }); 123 | 124 | it('sets status', () => { 125 | return authenticator(req).then(result => { 126 | expect(result.status).to.equal(data.status); 127 | }); 128 | }); 129 | }); 130 | 131 | describe('error', () => { 132 | it('returns the error', () => { 133 | const err = new Error('Authentication Failed'); 134 | verifier = (cb) => cb(err, null); 135 | passport.use(new MockStrategy({}, verifier)); 136 | authenticator = authenticate()(passport, 'mock'); 137 | 138 | return authenticator(req).catch(error => { 139 | expect(error).to.equal(err); 140 | }); 141 | }); 142 | }); 143 | 144 | describe('success', () => { 145 | let payload; 146 | let user; 147 | let organization; 148 | 149 | beforeEach(() => { 150 | payload = { platform: 'feathers' }; 151 | user = { name: 'Bob' }; 152 | organization = { name: 'Apple' }; 153 | verifier = (cb) => cb(null, user, payload); 154 | passport.use(new MockStrategy({}, verifier)); 155 | authenticator = authenticate({ entity: 'user' })(passport, 'mock'); 156 | }); 157 | 158 | it('sets success true', () => { 159 | return authenticator(req).then(result => { 160 | expect(result.success).to.equal(true); 161 | }); 162 | }); 163 | 164 | it('namespaces passport strategy data to "user"', () => { 165 | return authenticator(req).then(result => { 166 | expect(result.data.user).to.deep.equal(user); 167 | }); 168 | }); 169 | 170 | it('returns the passport payload', () => { 171 | return authenticator(req).then(result => { 172 | expect(result.data.payload).to.deep.equal(payload); 173 | }); 174 | }); 175 | 176 | it('supports custom namespaces via passports assignProperty', () => { 177 | verifier = (cb) => cb(null, organization); 178 | passport.use(new MockStrategy({}, verifier)); 179 | authenticator = authenticate()(passport, 'mock', { assignProperty: 'organization' }); 180 | 181 | return authenticator(req).then(result => { 182 | expect(result.data.organization).to.deep.equal(organization); 183 | }); 184 | }); 185 | 186 | it('supports custom namespaces via strategy options', () => { 187 | verifier = (cb) => cb(null, organization); 188 | passport.use(new MockStrategy({}, verifier)); 189 | authenticator = authenticate()(passport, 'mock', { entity: 'organization' }); 190 | 191 | return authenticator(req).then(result => { 192 | expect(result.data.organization).to.deep.equal(organization); 193 | }); 194 | }); 195 | }); 196 | 197 | describe('pass', () => { 198 | it('does nothing', () => { 199 | verifier = (cb) => cb(null, null, { pass: true }); 200 | passport.use(new MockStrategy({}, verifier)); 201 | authenticator = authenticate()(passport, 'mock'); 202 | 203 | return authenticator(req).then(result => { 204 | expect(result).to.equal(undefined); 205 | }); 206 | }); 207 | }); 208 | }); 209 | }); 210 | 211 | describe.skip('when authenticating with multiple chained strategies', () => { 212 | it('calls authenticate for a each strategy', () => { 213 | // TODO (EK) 214 | }); 215 | 216 | it('returns an error if all strategies fail', () => { 217 | // TODO (EK) 218 | }); 219 | 220 | it('succeeds if at least one strategy succeeds', () => { 221 | // TODO (EK) 222 | }); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /test/passport/index.test.js: -------------------------------------------------------------------------------- 1 | const feathers = require('@feathersjs/feathers'); 2 | const expressify = require('@feathersjs/express'); 3 | const { expect } = require('chai'); 4 | const adapter = require('../../lib/passport'); 5 | 6 | describe('Feathers Passport Adapter', () => { 7 | let app; 8 | 9 | before(() => { app = expressify(feathers()); }); 10 | 11 | it('is CommonJS compatible', () => { 12 | expect(typeof require('../../lib/passport')).to.equal('function'); 13 | }); 14 | 15 | it('is ES6 compatible', () => { 16 | expect(typeof adapter).to.equal('function'); 17 | }); 18 | 19 | it('exposes initialize function', () => { 20 | expect(typeof adapter.call(app).initialize).to.equal('function'); 21 | }); 22 | 23 | it('exposes authenticate function', () => { 24 | expect(typeof adapter.call(app).authenticate).to.equal('function'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/passport/initialize.test.js: -------------------------------------------------------------------------------- 1 | const initialize = require('../../lib/passport/initialize'); 2 | const { expect } = require('chai'); 3 | 4 | describe('passport:initialize', () => { 5 | it('it returns a function', () => { 6 | expect(typeof initialize()).to.equal('function'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/service.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const feathers = require('@feathersjs/feathers'); 3 | const expressify = require('@feathersjs/express'); 4 | const authentication = require('../lib'); 5 | const chai = require('chai'); 6 | const sinon = require('sinon'); 7 | const sinonChai = require('sinon-chai'); 8 | 9 | const { expect } = chai; 10 | const { express } = authentication; 11 | 12 | chai.use(sinonChai); 13 | 14 | describe('/authentication service', () => { 15 | let app; 16 | 17 | beforeEach(() => { 18 | sinon.spy(express, 'emitEvents'); 19 | sinon.spy(express, 'setCookie'); 20 | sinon.spy(express, 'successRedirect'); 21 | sinon.spy(express, 'failureRedirect'); 22 | 23 | app = expressify(feathers()) 24 | .configure(authentication({ secret: 'supersecret' })); 25 | }); 26 | 27 | afterEach(() => { 28 | express.emitEvents.restore(); 29 | express.setCookie.restore(); 30 | express.successRedirect.restore(); 31 | express.failureRedirect.restore(); 32 | }); 33 | 34 | it('throws an error when path option is missing', () => { 35 | expect(() => { 36 | express(feathers()).configure(authentication({ 37 | secret: 'dummy', 38 | path: null 39 | })); 40 | }).to.throw; 41 | }); 42 | 43 | it('registers the service at the path', () => { 44 | expect(app.service('authentication')).to.not.equal(undefined); 45 | }); 46 | 47 | it('keeps a reference to app', () => { 48 | expect(app.service('authentication').app).to.not.equal(undefined); 49 | }); 50 | 51 | it('keeps a reference to passport', () => { 52 | expect(app.service('authentication').passport).to.not.equal(undefined); 53 | }); 54 | 55 | it('registers the emitEvents express middleware', () => { 56 | expect(express.emitEvents).to.have.been.calledOnce; 57 | }); 58 | 59 | it('registers the setCookie express middleware', () => { 60 | expect(express.setCookie).to.have.been.calledOnce; 61 | }); 62 | 63 | it('registers the successRedirect express middleware', () => { 64 | expect(express.successRedirect).to.have.been.calledOnce; 65 | }); 66 | 67 | it('registers the failureRedirect express middleware', () => { 68 | expect(express.failureRedirect).to.have.been.calledOnce; 69 | }); 70 | 71 | describe('create', () => { 72 | const data = { 73 | payload: { id: 1 } 74 | }; 75 | 76 | it('creates an accessToken', () => { 77 | return app.service('authentication').create(data).then(result => { 78 | expect(result.accessToken).to.not.equal(undefined); 79 | }); 80 | }); 81 | 82 | it('creates a custom token', () => { 83 | const params = { 84 | jwt: { 85 | header: { typ: 'refresh' }, 86 | expiresIn: '1y' 87 | } 88 | }; 89 | 90 | return app.service('authentication').create(data, params).then(result => { 91 | expect(result.accessToken).to.not.equal(undefined); 92 | }); 93 | }); 94 | 95 | it('creates multiple custom tokens without side effect on expiration', () => { 96 | const params = { 97 | jwt: { 98 | header: { typ: 'refresh' }, 99 | expiresIn: '1y' 100 | } 101 | }; 102 | 103 | return app.service('authentication').create(data, params).then(result => { 104 | return app.service('authentication').create(data).then(result => { 105 | return app.passport 106 | .verifyJWT(result.accessToken, app.get('authentication')) 107 | .then(payload => { 108 | const delta = (payload.exp - payload.iat); 109 | expect(delta).to.equal(24 * 60 * 60); 110 | }); 111 | }); 112 | }); 113 | }); 114 | }); 115 | 116 | describe('remove', () => { 117 | let accessToken; 118 | 119 | beforeEach(() => { 120 | return app.passport 121 | .createJWT({ id: 1 }, app.get('authentication')) 122 | .then(token => { accessToken = token; }); 123 | }); 124 | 125 | it('verifies an accessToken and returns it', () => { 126 | return app.service('authentication').remove(accessToken).then(response => { 127 | expect(response).to.deep.equal({ accessToken }); 128 | }); 129 | }); 130 | 131 | it('verifies an accessToken in the header', () => { 132 | const params = { headers: { authorization: accessToken } }; 133 | return app.service('authentication').remove(null, params).then(response => { 134 | expect(response).to.deep.equal({ accessToken }); 135 | }); 136 | }); 137 | 138 | it('verifies an accessToken in the header with Bearer scheme', () => { 139 | const params = { headers: { authorization: `Bearer ${accessToken}` } }; 140 | return app.service('authentication').remove(null, params).then(response => { 141 | expect(response).to.deep.equal({ accessToken }); 142 | }); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/socket/index.test.js: -------------------------------------------------------------------------------- 1 | const socket = require('../../lib/socket'); 2 | const { expect } = require('chai'); 3 | 4 | describe('Feathers Socket Handlers', () => { 5 | it('is CommonJS compatible', () => { 6 | expect(typeof require('../../lib/socket')).to.equal('object'); 7 | }); 8 | 9 | it('is ES6 compatible', () => { 10 | expect(typeof socket).to.equal('object'); 11 | }); 12 | 13 | it('exposes socketio handler function', () => { 14 | expect(typeof socket.socketio).to.equal('function'); 15 | }); 16 | 17 | it('exposes primus handler function', () => { 18 | expect(typeof socket.primus).to.equal('function'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/socket/update-entity.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { ObjectID } = require('mongodb'); 3 | const updateEntity = require('../../lib/socket/update-entity'); 4 | 5 | const TEST_OBJECT_ID = '59499c9a901604391cab65f5'; 6 | 7 | describe('Socket "Update Entity" Handler', function () { 8 | it('updates the passed-in entity for socket.io', function () { 9 | const app = { 10 | get () { 11 | return { 12 | entity: 'user', 13 | service: 'users' 14 | }; 15 | }, 16 | io: { 17 | sockets: { 18 | sockets: { 19 | 'my-socket': { 20 | feathers: { 21 | user: { _id: 5, email: 'admin@feathersjs.com' } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | services: { 28 | users: { 29 | id: '_id' 30 | } 31 | }, 32 | service (location) { 33 | return this.services[location]; 34 | } 35 | }; 36 | const user = { _id: 5, email: 'test@feathersjs.com' }; 37 | 38 | updateEntity(app)(user); 39 | 40 | expect(app.io.sockets.sockets['my-socket'].feathers.user.email).to.equal('test@feathersjs.com'); 41 | }); 42 | 43 | it('updates the passed-in entity for primus', function () { 44 | const app = { 45 | get () { 46 | return { 47 | entity: 'user', 48 | service: 'users' 49 | }; 50 | }, 51 | primus: { 52 | connections: { 53 | 'my-socket': { 54 | request: { 55 | feathers: { 56 | user: { _id: 5, email: 'admin@feathersjs.com' } 57 | } 58 | } 59 | } 60 | } 61 | }, 62 | services: { 63 | users: { 64 | id: '_id' 65 | } 66 | }, 67 | service (location) { 68 | return this.services[location]; 69 | } 70 | }; 71 | const user = { _id: 5, email: 'test@feathersjs.com' }; 72 | 73 | updateEntity(app)(user); 74 | 75 | expect(app.primus.connections['my-socket'].request.feathers.user.email).to.equal('test@feathersjs.com'); 76 | }); 77 | 78 | it('sets idField to id if entity.id exists and the service has no `id` property', function () { 79 | const app = { 80 | get () { 81 | return { 82 | entity: 'user', 83 | service: 'users' 84 | }; 85 | }, 86 | primus: { 87 | connections: { 88 | 'my-socket': { 89 | request: { 90 | feathers: { 91 | user: { id: 5, email: 'admin@feathersjs.com' } 92 | } 93 | } 94 | } 95 | } 96 | }, 97 | services: { 98 | users: { } 99 | }, 100 | service (location) { 101 | return this.services[location]; 102 | } 103 | }; 104 | const user = { id: 5, email: 'test@feathersjs.com' }; 105 | 106 | updateEntity(app)(user); 107 | 108 | expect(app.primus.connections['my-socket'].request.feathers.user.email).to.equal('test@feathersjs.com'); 109 | }); 110 | 111 | it('gracefully handles unauthenticated connections', function () { 112 | const app = { 113 | get () { 114 | return { 115 | entity: 'user', 116 | service: 'users' 117 | }; 118 | }, 119 | io: { 120 | sockets: { 121 | sockets: { 122 | 'unauthenticated': { 123 | request: {}, 124 | feathers: {} 125 | }, 126 | 'my-socket': { 127 | feathers: { 128 | user: { _id: 5, email: 'admin@feathersjs.com' } 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | services: { 135 | users: { 136 | id: '_id' 137 | } 138 | }, 139 | service (location) { 140 | return this.services[location]; 141 | } 142 | }; 143 | const user = { _id: 5, email: 'test@feathersjs.com' }; 144 | 145 | updateEntity(app)(user); 146 | 147 | expect(app.io.sockets.sockets['my-socket'].feathers.user.email).to.equal('test@feathersjs.com'); 148 | }); 149 | 150 | it('updates the passed-in entity when the idField is an ObjectID', function () { 151 | const app = { 152 | get () { 153 | return { 154 | entity: 'user', 155 | service: 'users' 156 | }; 157 | }, 158 | io: { 159 | sockets: { 160 | sockets: { 161 | 'my-socket': { 162 | feathers: { 163 | user: { _id: new ObjectID(TEST_OBJECT_ID), email: 'admin@feathersjs.com' } 164 | } 165 | } 166 | } 167 | } 168 | }, 169 | services: { 170 | users: { 171 | id: '_id' 172 | } 173 | }, 174 | service (location) { 175 | return this.services[location]; 176 | } 177 | }; 178 | const user = { _id: new ObjectID(TEST_OBJECT_ID), email: 'test@feathersjs.com' }; 179 | 180 | updateEntity(app)(user); 181 | 182 | expect(app.io.sockets.sockets['my-socket'].feathers.user.email).to.equal('test@feathersjs.com'); 183 | }); 184 | 185 | it('socket entity should "deep equal" passed-in entity', function () { 186 | const app = { 187 | get () { 188 | return { 189 | entity: 'user', 190 | service: 'users' 191 | }; 192 | }, 193 | io: { 194 | sockets: { 195 | sockets: { 196 | 'my-socket': { 197 | feathers: { 198 | user: { 199 | _id: 5, 200 | email: 'admin@feathersjs.com', 201 | nested: { value: 1 }, 202 | optional: true 203 | } 204 | } 205 | } 206 | } 207 | } 208 | }, 209 | services: { 210 | users: { 211 | id: '_id' 212 | } 213 | }, 214 | service (location) { 215 | return this.services[location]; 216 | } 217 | }; 218 | const user = { _id: 5, email: 'test@feathersjs.com', nested: { value: 3 } }; 219 | 220 | updateEntity(app)(user); 221 | 222 | expect(app.io.sockets.sockets['my-socket'].feathers.user).to.deep.equal(user); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const chai = require('chai'); 3 | const chaiUuid = require('chai-uuid'); 4 | const { createJWT, verifyJWT } = require('../lib/utils'); 5 | const getOptions = require('../lib/options'); 6 | const { expect } = require('chai'); 7 | 8 | chai.use(chaiUuid); 9 | 10 | describe('utils', () => { 11 | let options; 12 | let payload; 13 | 14 | beforeEach(() => { 15 | options = getOptions({ 16 | secret: 'supersecret' 17 | }); 18 | 19 | payload = { id: 1 }; 20 | }); 21 | 22 | describe('createJWT', () => { 23 | describe('when secret is undefined', () => { 24 | it('returns an error', () => { 25 | return createJWT().catch(error => { 26 | expect(error).to.not.equal(undefined); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('when using default options', () => { 32 | let token; 33 | let decoded; 34 | 35 | beforeEach(() => { 36 | return createJWT(payload, options).then(t => { 37 | token = t; 38 | decoded = jwt.decode(token); 39 | }); 40 | }); 41 | 42 | it('returns a JWT', () => { 43 | expect(token).not.equal(undefined); 44 | }); 45 | 46 | it('encodes the payload', () => { 47 | expect(decoded.id).to.deep.equal(payload.id); 48 | }); 49 | 50 | it('has the correct audience', () => { 51 | expect(decoded.aud).to.equal(options.jwt.audience); 52 | }); 53 | 54 | it('has the correct subject', () => { 55 | expect(decoded.sub).to.equal(options.jwt.subject); 56 | }); 57 | 58 | it('has the correct issuer', () => { 59 | expect(decoded.iss).to.equal(options.jwt.issuer); 60 | }); 61 | 62 | it('has a uuidv4 jwtid', () => { 63 | expect(decoded.jti).to.be.a.uuid('v4'); 64 | }); 65 | }); 66 | 67 | it('does not error if payload has jti property', () => { 68 | return createJWT({ 69 | id: 1, 70 | jti: 'test' 71 | }, options).then(t => { 72 | const decoded = jwt.decode(t); 73 | 74 | expect(decoded.jti).to.equal('test'); 75 | }); 76 | }); 77 | 78 | describe('when passing custom options', () => { 79 | let token; 80 | let decoded; 81 | 82 | beforeEach(() => { 83 | options.jwt.subject = 'refresh'; 84 | options.jwt.issuer = 'custom'; 85 | options.jwt.audience = 'org'; 86 | options.jwt.expiresIn = '1y'; // expires in 1 year 87 | options.jwt.notBefore = '1h'; // token is valid 1 hour from now 88 | options.jwt.jwtid = '1234'; 89 | 90 | return createJWT(payload, options).then(t => { 91 | token = t; 92 | decoded = jwt.decode(token); 93 | }); 94 | }); 95 | 96 | it('returns a JWT', () => { 97 | expect(token).not.equal(undefined); 98 | }); 99 | 100 | it('encodes the payload', () => { 101 | expect(decoded.id).to.deep.equal(payload.id); 102 | }); 103 | 104 | it('has the correct audience', () => { 105 | expect(decoded.aud).to.equal('org'); 106 | }); 107 | 108 | it('has the correct subject', () => { 109 | expect(decoded.sub).to.equal('refresh'); 110 | }); 111 | 112 | it('has the correct issuer', () => { 113 | expect(decoded.iss).to.equal('custom'); 114 | }); 115 | 116 | it('has the correct jwtid', () => { 117 | expect(decoded.jti).to.equal('1234'); 118 | }); 119 | }); 120 | }); 121 | 122 | describe('verifyJWT', () => { 123 | let validToken; 124 | let expiredToken; 125 | 126 | beforeEach(() => { 127 | return createJWT(payload, options).then(vt => { 128 | validToken = vt; 129 | options.jwt.expiresIn = '1ms'; 130 | 131 | return createJWT(payload, options).then(et => { 132 | expiredToken = et; 133 | }); 134 | }); 135 | }); 136 | 137 | describe('when secret is undefined', () => { 138 | it('returns an error', () => { 139 | return verifyJWT().catch(error => { 140 | expect(error).to.not.equal(undefined); 141 | }); 142 | }); 143 | }); 144 | 145 | describe('when using default options', () => { 146 | describe('when token is valid', () => { 147 | it('returns payload', () => { 148 | return verifyJWT(validToken, options).then(payload => { 149 | expect(payload.id).to.equal(1); 150 | }); 151 | }); 152 | }); 153 | 154 | describe('when token is expired', () => { 155 | it('returns an error', () => { 156 | return verifyJWT(expiredToken, options).catch(error => { 157 | expect(error).to.not.equal(undefined); 158 | }); 159 | }); 160 | }); 161 | 162 | describe('when token is invalid', () => { 163 | it('returns payload', () => { 164 | return verifyJWT('invalid', options).catch(error => { 165 | expect(error).to.not.equal(undefined); 166 | }); 167 | }); 168 | }); 169 | }); 170 | 171 | describe('when using custom options', () => { 172 | describe('when secret does not match', () => { 173 | it('returns an error', () => { 174 | options.secret = 'invalid'; 175 | return verifyJWT(validToken, options).catch(error => { 176 | expect(error).to.not.equal(undefined); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('when audience does not match', () => { 182 | it('returns an error', () => { 183 | options.jwt.audience = 'invalid'; 184 | return verifyJWT(validToken, options).catch(error => { 185 | expect(error).to.not.equal(undefined); 186 | }); 187 | }); 188 | }); 189 | 190 | describe('when subject does not match', () => { 191 | it('returns an error', () => { 192 | options.jwt.subject = 'invalid'; 193 | return verifyJWT(validToken, options).catch(error => { 194 | expect(error).to.not.equal(undefined); 195 | }); 196 | }); 197 | }); 198 | 199 | describe('when issuer does not match', () => { 200 | it('returns an error', () => { 201 | options.jwt.issuer = 'invalid'; 202 | return verifyJWT(validToken, options).catch(error => { 203 | expect(error).to.not.equal(undefined); 204 | }); 205 | }); 206 | }); 207 | 208 | describe('when algorithm does not match', () => { 209 | it('returns an error', () => { 210 | options.jwt.algorithm = 'HS512'; 211 | return verifyJWT(validToken, options).catch(error => { 212 | expect(error).to.not.equal(undefined); 213 | }); 214 | }); 215 | }); 216 | }); 217 | }); 218 | }); 219 | --------------------------------------------------------------------------------