├── .editorconfig ├── .github ├── contributing.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── hooks │ ├── index.js │ ├── populate-access-token.js │ ├── populate-entity.js │ └── populate-header.js ├── index.js ├── passport.js └── utils.js ├── mocha.opts ├── package-lock.json ├── package.json └── test ├── fixtures └── server.js ├── hooks ├── index.test.js ├── populate-access-token.test.js ├── populate-entity.test.js └── populate-header.test.js ├── index.test.js ├── integration ├── primus.test.js ├── rest.test.js └── socketio.test.js └── passport.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 | -------------------------------------------------------------------------------- /.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 | language: node_js 2 | node_js: 3 | - node 4 | - '6' 5 | install: npm install 6 | addons: 7 | code_climate: 8 | repo_token: 49a284ea8b525786f42250a2b0b670077bbe8e9560cfff081eb59b282fd480f4 9 | notifications: 10 | email: false 11 | slack: 12 | rooms: 13 | secure: a0nrIsyw7VWxD9PANMWyzjdhrHt2j4dd7adk10uPVq68H5XVD2d4DCHLd8UGWcMqkNVRpMuOAUY44wCeg+3ev/70r5wDUEGVWbUn7z6AdfVyd7leC8glr2WEdJNXmRX/0vljQlA9Nhj2DAcT7euaZItumzHol3T8dKinzGmwObF9xctFkAS9lzgTuWtuFUNH2zDndALMt6E7V2zXDsnQezGmUUcXkM8VZBu6K4VHsWuzqPOozjlYAoSrWg+1Hu9yyb79Lq3pNAVrHK2n0uASR9AHjUJFnbRFlQwjyZj2nsLrAg1EP6wHupzI8JD3jsX/IbgrCWGGv1QfoCJCN8F+VKjajq7eRIE6bJsOTpry8DcqTl3FKenGH+e1I6nrznRp26JN9cgpFgeX4pYjh5PyZ92g1YbVDfvrMC7UppA8qkQ/J5zVzg9K3bqUL6fGJi6EMyx2I26MaNw/izaRupZR7Lc4puMmgfuxmF2GqO0KKf0irCJtpA5u0g6j3eDKLe5UuT/ILNu02rmH6F6shk5k104TW6W0kEMJnViObfM94PszC1WxEJGxG7ybeo9VnzQw5wvpTtIopf+s3+5HGOwmnuNSHiItThuBMHGfSV2MqbORQguV0G1b4vs+Q5pJAD2VzoA59JkNBly9gIjYpihw6ouMW0pCJtVU673bcHJRNuI= 14 | before_script: 15 | - npm install -g codeclimate-test-reporter 16 | after_script: 17 | - codeclimate-test-reporter < coverage/lcov.info 18 | sudo: false 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.0.2](https://github.com/feathersjs/authentication-client/tree/v1.0.2) (2018-01-03) 4 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v1.0.1...v1.0.2) 5 | 6 | **Closed issues:** 7 | 8 | - No Auth header added when sending 1st request [\#80](https://github.com/feathersjs/authentication-client/issues/80) 9 | 10 | **Merged pull requests:** 11 | 12 | - Update to correspond with latest release [\#84](https://github.com/feathersjs/authentication-client/pull/84) ([daffl](https://github.com/daffl)) 13 | - Update semistandard to the latest version 🚀 [\#83](https://github.com/feathersjs/authentication-client/pull/83) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 14 | - Update feathers-memory to the latest version 🚀 [\#82](https://github.com/feathersjs/authentication-client/pull/82) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 15 | 16 | ## [v1.0.1](https://github.com/feathersjs/authentication-client/tree/v1.0.1) (2017-11-16) 17 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v1.0.0...v1.0.1) 18 | 19 | **Merged pull requests:** 20 | 21 | - Add default export for better ES module \(TypeScript\) compatibility [\#81](https://github.com/feathersjs/authentication-client/pull/81) ([daffl](https://github.com/daffl)) 22 | - Update @feathersjs/authentication to the latest version 🚀 [\#79](https://github.com/feathersjs/authentication-client/pull/79) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 23 | 24 | ## [v1.0.0](https://github.com/feathersjs/authentication-client/tree/v1.0.0) (2017-11-01) 25 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v1.0.0-pre.1...v1.0.0) 26 | 27 | **Merged pull requests:** 28 | 29 | - Update dependencies for release [\#78](https://github.com/feathersjs/authentication-client/pull/78) ([daffl](https://github.com/daffl)) 30 | 31 | ## [v1.0.0-pre.1](https://github.com/feathersjs/authentication-client/tree/v1.0.0-pre.1) (2017-10-25) 32 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.3.3...v1.0.0-pre.1) 33 | 34 | **Closed issues:** 35 | 36 | - Error authenticating! Error: Token provided to verifyJWT is missing or not a string ? [\#73](https://github.com/feathersjs/authentication-client/issues/73) 37 | - Authorization Header not sent!! [\#69](https://github.com/feathersjs/authentication-client/issues/69) 38 | - users.get\(id\) failed \(Not authenticated\) after successful login. [\#66](https://github.com/feathersjs/authentication-client/issues/66) 39 | 40 | **Merged pull requests:** 41 | 42 | - Updates for Feathers v3 [\#77](https://github.com/feathersjs/authentication-client/pull/77) ([daffl](https://github.com/daffl)) 43 | - Update Codeclimate token and badges [\#76](https://github.com/feathersjs/authentication-client/pull/76) ([daffl](https://github.com/daffl)) 44 | - Rename repository and use npm scope [\#75](https://github.com/feathersjs/authentication-client/pull/75) ([daffl](https://github.com/daffl)) 45 | - Update to new plugin infrastructure [\#74](https://github.com/feathersjs/authentication-client/pull/74) ([daffl](https://github.com/daffl)) 46 | - Update mocha to the latest version 🚀 [\#72](https://github.com/feathersjs/authentication-client/pull/72) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 47 | - Add babel-polyfill and package-lock.json [\#68](https://github.com/feathersjs/authentication-client/pull/68) ([daffl](https://github.com/daffl)) 48 | - Passport.verifyJWT should return Promise\, not Promise\ [\#65](https://github.com/feathersjs/authentication-client/pull/65) ([zxh19890103](https://github.com/zxh19890103)) 49 | - Update debug to the latest version 🚀 [\#61](https://github.com/feathersjs/authentication-client/pull/61) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 50 | - Update ws to the latest version 🚀 [\#60](https://github.com/feathersjs/authentication-client/pull/60) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 51 | 52 | ## [v0.3.3](https://github.com/feathersjs/authentication-client/tree/v0.3.3) (2017-07-18) 53 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.3.2...v0.3.3) 54 | 55 | **Closed issues:** 56 | 57 | - An in-range update of feathers is breaking the build 🚨 [\#59](https://github.com/feathersjs/authentication-client/issues/59) 58 | - An in-range update of feathers is breaking the build 🚨 [\#58](https://github.com/feathersjs/authentication-client/issues/58) 59 | 60 | **Merged pull requests:** 61 | 62 | - typings: add auth methods to feathers.Application interface [\#57](https://github.com/feathersjs/authentication-client/pull/57) ([j2L4e](https://github.com/j2L4e)) 63 | - Update feathers-authentication-local to the latest version 🚀 [\#55](https://github.com/feathersjs/authentication-client/pull/55) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 64 | - Update chai to the latest version 🚀 [\#54](https://github.com/feathersjs/authentication-client/pull/54) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 65 | - Update feathers-socketio to the latest version 🚀 [\#50](https://github.com/feathersjs/authentication-client/pull/50) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 66 | - Update socket.io-client to the latest version 🚀 [\#49](https://github.com/feathersjs/authentication-client/pull/49) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 67 | - Update README.md [\#47](https://github.com/feathersjs/authentication-client/pull/47) ([bertho-zero](https://github.com/bertho-zero)) 68 | 69 | ## [v0.3.2](https://github.com/feathersjs/authentication-client/tree/v0.3.2) (2017-04-30) 70 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.3.1...v0.3.2) 71 | 72 | **Closed issues:** 73 | 74 | - An in-range update of feathers-errors is breaking the build 🚨 [\#45](https://github.com/feathersjs/authentication-client/issues/45) 75 | - Proper way to save jwt in cookies [\#41](https://github.com/feathersjs/authentication-client/issues/41) 76 | - Allow customizing the `tokenField` [\#38](https://github.com/feathersjs/authentication-client/issues/38) 77 | - Show blank page in safari@iOS 8.3 [\#37](https://github.com/feathersjs/authentication-client/issues/37) 78 | 79 | **Merged pull requests:** 80 | 81 | - Catch getJWT promise errors [\#46](https://github.com/feathersjs/authentication-client/pull/46) ([NikitaVlaznev](https://github.com/NikitaVlaznev)) 82 | - Update semistandard to the latest version 🚀 [\#43](https://github.com/feathersjs/authentication-client/pull/43) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 83 | - Update feathers-hooks to the latest version 🚀 [\#42](https://github.com/feathersjs/authentication-client/pull/42) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 84 | - Update dependencies to enable Greenkeeper 🌴 [\#40](https://github.com/feathersjs/authentication-client/pull/40) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 85 | - Note that auth must be configured after rest/socket clients [\#36](https://github.com/feathersjs/authentication-client/pull/36) ([hubgit](https://github.com/hubgit)) 86 | 87 | ## [v0.3.1](https://github.com/feathersjs/authentication-client/tree/v0.3.1) (2017-03-10) 88 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.3.0...v0.3.1) 89 | 90 | **Closed issues:** 91 | 92 | - The latest tag on NPM is wrong [\#35](https://github.com/feathersjs/authentication-client/issues/35) 93 | - exp claim should be optional [\#33](https://github.com/feathersjs/authentication-client/issues/33) 94 | 95 | **Merged pull requests:** 96 | 97 | - Fix \#33 exp claim should be optional [\#34](https://github.com/feathersjs/authentication-client/pull/34) ([whollacsek](https://github.com/whollacsek)) 98 | 99 | ## [v0.3.0](https://github.com/feathersjs/authentication-client/tree/v0.3.0) (2017-03-08) 100 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.2.0...v0.3.0) 101 | 102 | ## [v0.2.0](https://github.com/feathersjs/authentication-client/tree/v0.2.0) (2017-03-07) 103 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.10...v0.2.0) 104 | 105 | **Closed issues:** 106 | 107 | - Support `authenticated` and `logout` client side events [\#29](https://github.com/feathersjs/authentication-client/issues/29) 108 | - The default header mismatches the default feathers-authentication header [\#23](https://github.com/feathersjs/authentication-client/issues/23) 109 | - Re-authenticating fails when passing options [\#22](https://github.com/feathersjs/authentication-client/issues/22) 110 | - Socket.io timeout does nothing when there is JWT token available [\#19](https://github.com/feathersjs/authentication-client/issues/19) 111 | 112 | **Merged pull requests:** 113 | 114 | - Fix header casing [\#32](https://github.com/feathersjs/authentication-client/pull/32) ([daffl](https://github.com/daffl)) 115 | - Add client side `authenticated` and `logout` events [\#31](https://github.com/feathersjs/authentication-client/pull/31) ([daffl](https://github.com/daffl)) 116 | - Add support for socket timeouts and some refactoring [\#30](https://github.com/feathersjs/authentication-client/pull/30) ([daffl](https://github.com/daffl)) 117 | 118 | ## [v0.1.10](https://github.com/feathersjs/authentication-client/tree/v0.1.10) (2017-03-03) 119 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.9...v0.1.10) 120 | 121 | **Merged pull requests:** 122 | 123 | - Remove hardcoded values for Config and Credentials typings [\#28](https://github.com/feathersjs/authentication-client/pull/28) ([myknbani](https://github.com/myknbani)) 124 | 125 | ## [v0.1.9](https://github.com/feathersjs/authentication-client/tree/v0.1.9) (2017-03-01) 126 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.8...v0.1.9) 127 | 128 | **Merged pull requests:** 129 | 130 | - Typescript Definitions [\#25](https://github.com/feathersjs/authentication-client/pull/25) ([AbraaoAlves](https://github.com/AbraaoAlves)) 131 | 132 | ## [v0.1.8](https://github.com/feathersjs/authentication-client/tree/v0.1.8) (2017-02-05) 133 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.7...v0.1.8) 134 | 135 | **Closed issues:** 136 | 137 | - Uncaught TypeError: Cannot read property 'options' of undefined [\#26](https://github.com/feathersjs/authentication-client/issues/26) 138 | - Browser Version [\#24](https://github.com/feathersjs/authentication-client/issues/24) 139 | 140 | **Merged pull requests:** 141 | 142 | - Hoist upgrade handler into current scope by using an arrow function [\#27](https://github.com/feathersjs/authentication-client/pull/27) ([daffl](https://github.com/daffl)) 143 | 144 | ## [v0.1.7](https://github.com/feathersjs/authentication-client/tree/v0.1.7) (2017-01-29) 145 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.6...v0.1.7) 146 | 147 | **Closed issues:** 148 | 149 | - \[Webpack\] TypeError: \_this4.storage.getItem is not a function [\#18](https://github.com/feathersjs/authentication-client/issues/18) 150 | - \[Feature request\] Signup via socket [\#17](https://github.com/feathersjs/authentication-client/issues/17) 151 | - Missing auth token when used with feathers-rest in comparison to feathers-socketio [\#16](https://github.com/feathersjs/authentication-client/issues/16) 152 | - Cannot read property 'on' of undefined - feathers-authentication-client [\#12](https://github.com/feathersjs/authentication-client/issues/12) 153 | 154 | **Merged pull requests:** 155 | 156 | - Update passport.js [\#20](https://github.com/feathersjs/authentication-client/pull/20) ([bertho-zero](https://github.com/bertho-zero)) 157 | 158 | ## [v0.1.6](https://github.com/feathersjs/authentication-client/tree/v0.1.6) (2016-12-14) 159 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.5...v0.1.6) 160 | 161 | **Closed issues:** 162 | 163 | - `logout\(\)` doesn't resolve [\#10](https://github.com/feathersjs/authentication-client/issues/10) 164 | 165 | **Merged pull requests:** 166 | 167 | - Fix linting [\#13](https://github.com/feathersjs/authentication-client/pull/13) ([marshallswain](https://github.com/marshallswain)) 168 | 169 | ## [v0.1.5](https://github.com/feathersjs/authentication-client/tree/v0.1.5) (2016-12-13) 170 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.4...v0.1.5) 171 | 172 | ## [v0.1.4](https://github.com/feathersjs/authentication-client/tree/v0.1.4) (2016-12-13) 173 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.3...v0.1.4) 174 | 175 | **Closed issues:** 176 | 177 | - populateAccessToken tries to access non-existent property [\#11](https://github.com/feathersjs/authentication-client/issues/11) 178 | - Socket client should automatically auth on reconnect [\#2](https://github.com/feathersjs/authentication-client/issues/2) 179 | 180 | **Merged pull requests:** 181 | 182 | - More specific imports for StealJS [\#14](https://github.com/feathersjs/authentication-client/pull/14) ([marshallswain](https://github.com/marshallswain)) 183 | 184 | ## [v0.1.3](https://github.com/feathersjs/authentication-client/tree/v0.1.3) (2016-11-23) 185 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.2...v0.1.3) 186 | 187 | **Closed issues:** 188 | 189 | - Client should ensure socket.io upgrade is complete before authenticating [\#4](https://github.com/feathersjs/authentication-client/issues/4) 190 | 191 | **Merged pull requests:** 192 | 193 | - Socket reconnect [\#9](https://github.com/feathersjs/authentication-client/pull/9) ([ekryski](https://github.com/ekryski)) 194 | 195 | ## [v0.1.2](https://github.com/feathersjs/authentication-client/tree/v0.1.2) (2016-11-22) 196 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.1...v0.1.2) 197 | 198 | **Merged pull requests:** 199 | 200 | - Custom jwt strategy names [\#8](https://github.com/feathersjs/authentication-client/pull/8) ([ekryski](https://github.com/ekryski)) 201 | 202 | ## [v0.1.1](https://github.com/feathersjs/authentication-client/tree/v0.1.1) (2016-11-21) 203 | [Full Changelog](https://github.com/feathersjs/authentication-client/compare/v0.1.0...v0.1.1) 204 | 205 | **Merged pull requests:** 206 | 207 | - Socket reconnect upgrade auth [\#3](https://github.com/feathersjs/authentication-client/pull/3) ([marshallswain](https://github.com/marshallswain)) 208 | 209 | ## [v0.1.0](https://github.com/feathersjs/authentication-client/tree/v0.1.0) (2016-11-18) 210 | **Closed issues:** 211 | 212 | - Relation with feathers-authentication [\#6](https://github.com/feathersjs/authentication-client/issues/6) 213 | - Client: Docs for getJWT & verifyJWT [\#1](https://github.com/feathersjs/authentication-client/issues/1) 214 | 215 | **Merged pull requests:** 216 | 217 | - Feathers authentication 1.0 compatible client [\#7](https://github.com/feathersjs/authentication-client/pull/7) ([ekryski](https://github.com/ekryski)) 218 | 219 | 220 | 221 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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-client 2 | 3 | 4 | > __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-client)). Please open issues and pull requests there. No changes in your existing Feathers applications are necessary. 5 | 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs/authentication-client.svg)](https://greenkeeper.io/) 7 | 8 | [![Build Status](https://travis-ci.org/feathersjs/authentication-client.png?branch=master)](https://travis-ci.org/feathersjs/authentication-client) 9 | [![Test Coverage](https://api.codeclimate.com/v1/badges/b2d2b018d2bf75f9bcc8/test_coverage)](https://codeclimate.com/github/feathersjs/authentication-client/test_coverage) 10 | [![Dependency Status](https://img.shields.io/david/feathersjs/authentication-client.svg?style=flat-square)](https://david-dm.org/feathersjs/authentication-client) 11 | [![Download Status](https://img.shields.io/npm/dm/@feathersjs/authentication-client.svg?style=flat-square)](https://www.npmjs.com/package/@feathersjs/authentication-client) 12 | 13 | > The authentication plugin for feathers-client 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install @feathersjs/authentication-client --save 19 | ``` 20 | 21 | ## Quick example 22 | 23 | ```js 24 | const feathers = require('@feathersjs/feathers'); 25 | const auth = require('@feathersjs/authentication-client'); 26 | 27 | const app = feathers(); 28 | 29 | // Available options are listed in the "Options" section 30 | app.configure(auth({ 31 | storage: window.localStorage 32 | })) 33 | ``` 34 | 35 | ## Documentation 36 | 37 | Please refer to the [@feathersjs/authentication-client documentation](https://docs.feathersjs.com/api/authentication/client.html) for more details. 38 | 39 | ## License 40 | 41 | Copyright (c) 2018 42 | 43 | Licensed under the [MIT license](LICENSE). 44 | -------------------------------------------------------------------------------- /lib/hooks/index.js: -------------------------------------------------------------------------------- 1 | const populateHeader = require('./populate-header'); 2 | const populateAccessToken = require('./populate-access-token'); 3 | const populateEntity = require('./populate-entity'); 4 | 5 | let hooks = { 6 | populateHeader, 7 | populateAccessToken, 8 | populateEntity 9 | }; 10 | 11 | module.exports = hooks; 12 | -------------------------------------------------------------------------------- /lib/hooks/populate-access-token.js: -------------------------------------------------------------------------------- 1 | module.exports = function populateAccessToken () { 2 | return function (hook) { 3 | const app = hook.app; 4 | 5 | if (hook.type !== 'before') { 6 | return Promise.reject(new Error(`The 'populateAccessToken' hook should only be used as a 'before' hook.`)); 7 | } 8 | 9 | Object.assign(hook.params, { accessToken: app.get('accessToken') }); 10 | 11 | return Promise.resolve(hook); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/hooks/populate-entity.js: -------------------------------------------------------------------------------- 1 | module.exports = function populateEntity (options = {}) { 2 | if (!options.service) { 3 | throw new Error(`You need to pass 'options.service' to the populateEntity() hook.`); 4 | } 5 | 6 | if (!options.field) { 7 | throw new Error(`You need to pass 'options.field' to the populateEntity() hook.`); 8 | } 9 | 10 | if (!options.entity) { 11 | throw new Error(`You need to pass 'options.entity' to the populateEntity() hook.`); 12 | } 13 | 14 | return function (hook) { 15 | const app = hook.app; 16 | 17 | if (hook.type !== 'after') { 18 | return Promise.reject(new Error(`The 'populateEntity' hook should only be used as an 'after' hook.`)); 19 | } 20 | 21 | return app.passport 22 | .verifyJWT(hook.result.accessToken) 23 | .then(payload => { 24 | const id = payload[options.field]; 25 | 26 | if (!id) { 27 | return Promise.reject(new Error(`Access token payload is missing the '${options.field}' field.`)); 28 | } 29 | 30 | return app.service(options.service).get(id); 31 | }) 32 | .then(entity => { 33 | hook.result[options.entity] = entity; 34 | app.set(options.entity, entity); 35 | 36 | return Promise.resolve(hook); 37 | }); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/hooks/populate-header.js: -------------------------------------------------------------------------------- 1 | module.exports = function populateHeader (options = {}) { 2 | if (!options.header) { 3 | throw new Error(`You need to pass 'options.header' to the populateHeader() hook.`); 4 | } 5 | 6 | return function (hook) { 7 | if (hook.type !== 'before') { 8 | return Promise.reject(new Error(`The 'populateHeader' hook should only be used as a 'before' hook.`)); 9 | } 10 | 11 | if (hook.params.accessToken) { 12 | hook.params.headers = Object.assign({}, { 13 | [options.header]: options.prefix ? `${options.prefix} ${hook.params.accessToken}` : hook.params.accessToken 14 | }, hook.params.headers); 15 | } 16 | 17 | return Promise.resolve(hook); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const hooks = require('./hooks/index'); 2 | const Passport = require('./passport'); 3 | 4 | const defaults = { 5 | header: 'Authorization', 6 | cookie: 'feathers-jwt', 7 | storageKey: 'feathers-jwt', 8 | jwtStrategy: 'jwt', 9 | path: '/authentication', 10 | entity: 'user', 11 | service: 'users', 12 | timeout: 5000 13 | }; 14 | 15 | function init (config = {}) { 16 | const options = Object.assign({}, defaults, config); 17 | 18 | return function () { 19 | const app = this; 20 | 21 | app.passport = new Passport(app, options); 22 | app.authenticate = app.passport.authenticate.bind(app.passport); 23 | app.logout = app.passport.logout.bind(app.passport); 24 | 25 | // Set up hook that adds token and user to params so that 26 | // it they can be accessed by client side hooks and services 27 | app.mixins.push(function (service) { 28 | // if (typeof service.hooks !== 'function') { 29 | if (app.version < '3.0.0') { 30 | throw new Error(`This version of @feathersjs/authentication-client only works with @feathersjs/feathers v3.0.0 or later.`); 31 | } 32 | 33 | service.hooks({ 34 | before: hooks.populateAccessToken(options) 35 | }); 36 | }); 37 | 38 | // Set up hook that adds authorization header for REST provider 39 | if (app.rest) { 40 | app.mixins.push(function (service) { 41 | service.hooks({ 42 | before: hooks.populateHeader(options) 43 | }); 44 | }); 45 | } 46 | }; 47 | } 48 | 49 | module.exports = init; 50 | 51 | module.exports.default = init; 52 | module.exports.defaults = defaults; 53 | -------------------------------------------------------------------------------- /lib/passport.js: -------------------------------------------------------------------------------- 1 | const errors = require('@feathersjs/errors'); 2 | const decode = require('jwt-decode'); 3 | const Debug = require('debug'); 4 | 5 | const { 6 | Storage, 7 | payloadIsValid, 8 | getCookie, 9 | clearCookie 10 | } = require('./utils'); 11 | 12 | const debug = Debug('@feathersjs/authentication-client'); 13 | 14 | module.exports = class Passport { 15 | constructor (app, options) { 16 | if (app.passport) { 17 | throw new Error('You have already registered authentication on this client app instance. You only need to do it once.'); 18 | } 19 | 20 | Object.assign(this, { 21 | options, 22 | app, 23 | payloadIsValid, 24 | getCookie, 25 | clearCookie, 26 | storage: app.get('storage') || this.getStorage(options.storage) 27 | }); 28 | 29 | this.setJWT = this.setJWT.bind(this); 30 | 31 | app.set('storage', this.storage); 32 | this.getJWT().then(this.setJWT); 33 | 34 | this.setupSocketListeners(); 35 | } 36 | 37 | setupSocketListeners () { 38 | const app = this.app; 39 | const socket = app.io || app.primus; 40 | const emit = app.io ? 'emit' : 'send'; 41 | const reconnected = app.io ? 'reconnect' : 'reconnected'; 42 | 43 | if (!socket) { 44 | return; 45 | } 46 | 47 | socket.on(reconnected, () => { 48 | debug('Socket reconnected'); 49 | 50 | // If socket was already authenticated then re-authenticate 51 | // it with the server automatically. 52 | if (socket.authenticated) { 53 | const data = { 54 | strategy: this.options.jwtStrategy, 55 | accessToken: app.get('accessToken') 56 | }; 57 | this.authenticateSocket(data, socket, emit) 58 | .then(this.setJWT) 59 | .catch(error => { 60 | debug('Error re-authenticating after socket reconnect', error); 61 | socket.authenticated = false; 62 | app.emit('reauthentication-error', error); 63 | }); 64 | } 65 | }); 66 | 67 | const socketUpgradeHandler = () => { 68 | socket.io.engine.on('upgrade', () => { 69 | debug('Socket upgrading'); 70 | 71 | // If socket was already authenticated then re-authenticate 72 | // it with the server automatically. 73 | if (socket.authenticated) { 74 | const data = { 75 | strategy: this.options.jwtStrategy, 76 | accessToken: app.get('accessToken') 77 | }; 78 | 79 | this.authenticateSocket(data, socket, emit) 80 | .then(this.setJWT) 81 | .catch(error => { 82 | debug('Error re-authenticating after socket upgrade', error); 83 | socket.authenticated = false; 84 | app.emit('reauthentication-error', error); 85 | }); 86 | } 87 | }); 88 | }; 89 | 90 | if (socket.io && socket.io.engine) { 91 | socketUpgradeHandler(); 92 | } else { 93 | socket.on('connect', socketUpgradeHandler); 94 | } 95 | } 96 | 97 | connected () { 98 | const app = this.app; 99 | 100 | if (app.rest) { 101 | return Promise.resolve(); 102 | } 103 | 104 | const socket = app.io || app.primus; 105 | 106 | if (!socket) { 107 | return Promise.reject(new Error(`It looks like your client connection has not been configured.`)); 108 | } 109 | 110 | if ((app.io && socket.connected) || (app.primus && socket.readyState === 3)) { 111 | debug('Socket already connected'); 112 | return Promise.resolve(socket); 113 | } 114 | 115 | return new Promise((resolve, reject) => { 116 | const connected = app.primus ? 'open' : 'connect'; 117 | const disconnect = app.io ? 'disconnect' : 'end'; 118 | const timeout = setTimeout(() => { 119 | debug('Socket connection timed out'); 120 | reject(new Error('Socket connection timed out')); 121 | }, this.options.timeout); 122 | 123 | debug('Waiting for socket connection'); 124 | 125 | const handleDisconnect = () => { 126 | debug('Socket disconnected before it could connect'); 127 | socket.authenticated = false; 128 | }; 129 | 130 | // If disconnect happens before `connect` the promise will be rejected. 131 | socket.once(disconnect, handleDisconnect); 132 | socket.once(connected, () => { 133 | debug('Socket connected'); 134 | debug(`Removing ${disconnect} listener`); 135 | socket.removeListener(disconnect, handleDisconnect); 136 | clearTimeout(timeout); 137 | resolve(socket); 138 | }); 139 | }); 140 | } 141 | 142 | authenticate (credentials = {}) { 143 | const app = this.app; 144 | let getCredentials = Promise.resolve(credentials); 145 | 146 | // If no strategy was given let's try to authenticate with a stored JWT 147 | if (!credentials.strategy) { 148 | if (credentials.accessToken) { 149 | credentials.strategy = this.options.jwtStrategy; 150 | } else { 151 | getCredentials = this.getJWT().then(accessToken => { 152 | if (!accessToken) { 153 | return Promise.reject(new errors.NotAuthenticated(`Could not find stored JWT and no authentication strategy was given`)); 154 | } 155 | return { strategy: this.options.jwtStrategy, accessToken }; 156 | }); 157 | } 158 | } 159 | 160 | return getCredentials.then(credentials => { 161 | return this.connected(app).then(socket => { 162 | if (app.rest) { 163 | return app.service(this.options.path).create(credentials).then(this.setJWT); 164 | } 165 | 166 | const emit = app.io ? 'emit' : 'send'; 167 | return this.authenticateSocket(credentials, socket, emit).then(this.setJWT); 168 | }); 169 | }).then(payload => { 170 | app.emit('authenticated', payload); 171 | return payload; 172 | }); 173 | } 174 | 175 | // Returns a promise that authenticates a socket 176 | authenticateSocket (credentials, socket, emit) { 177 | return new Promise((resolve, reject) => { 178 | const timeout = setTimeout(() => { 179 | debug('authenticateSocket timed out'); 180 | reject(new Error('Authentication timed out')); 181 | }, this.options.timeout); 182 | 183 | debug('Attempting to authenticate socket'); 184 | socket[emit]('authenticate', credentials, (error, data) => { 185 | if (error) { 186 | return reject(error); 187 | } 188 | 189 | clearTimeout(timeout); 190 | socket.authenticated = true; 191 | debug('Socket authenticated!'); 192 | 193 | resolve(data); 194 | }); 195 | }); 196 | } 197 | 198 | logoutSocket (socket, emit) { 199 | return new Promise((resolve, reject) => { 200 | const timeout = setTimeout(() => { 201 | debug('logoutSocket timed out'); 202 | reject(new Error('Logout timed out')); 203 | }, this.options.timeout); 204 | 205 | socket[emit]('logout', error => { 206 | clearTimeout(timeout); 207 | socket.authenticated = false; 208 | 209 | if (error) { 210 | return reject(error); 211 | } 212 | 213 | resolve(); 214 | }); 215 | }); 216 | } 217 | 218 | logout () { 219 | const app = this.app; 220 | 221 | app.set('accessToken', null); 222 | this.clearCookie(this.options.cookie); 223 | 224 | // remove the accessToken from localStorage 225 | return Promise.resolve(app.get('storage').removeItem(this.options.storageKey)).then(() => { 226 | // If using sockets de-authenticate the socket 227 | if (app.io || app.primus) { 228 | const method = app.io ? 'emit' : 'send'; 229 | const socket = app.io ? app.io : app.primus; 230 | 231 | return this.logoutSocket(socket, method); 232 | } 233 | }).then(result => { 234 | app.emit('logout', result); 235 | 236 | return result; 237 | }); 238 | } 239 | 240 | setJWT (data) { 241 | const accessToken = (data && data.accessToken) ? data.accessToken : data; 242 | 243 | if (accessToken) { 244 | this.app.set('accessToken', accessToken); 245 | this.app.get('storage').setItem(this.options.storageKey, accessToken); 246 | } 247 | 248 | return Promise.resolve(data); 249 | } 250 | 251 | getJWT () { 252 | const app = this.app; 253 | return new Promise((resolve, reject) => { 254 | const accessToken = app.get('accessToken'); 255 | 256 | if (accessToken) { 257 | return resolve(accessToken); 258 | } 259 | 260 | return Promise.resolve(this.storage.getItem(this.options.storageKey)) 261 | .then(jwt => { 262 | let token = jwt || this.getCookie(this.options.cookie); 263 | 264 | if (token && token !== 'null' && !this.payloadIsValid(decode(token))) { 265 | token = undefined; 266 | } 267 | 268 | return resolve(token); 269 | }) 270 | .catch(reject); 271 | }); 272 | } 273 | 274 | // Pass a jwt token, get back a payload if it's valid. 275 | verifyJWT (token) { 276 | if (typeof token !== 'string') { 277 | return Promise.reject(new Error('Token provided to verifyJWT is missing or not a string')); 278 | } 279 | 280 | try { 281 | let payload = decode(token); 282 | 283 | if (this.payloadIsValid(payload)) { 284 | return Promise.resolve(payload); 285 | } 286 | 287 | return Promise.reject(new Error('Invalid token: expired')); 288 | } catch (error) { 289 | return Promise.reject(new Error('Cannot decode malformed token.')); 290 | } 291 | } 292 | 293 | // Returns a storage implementation 294 | getStorage (storage) { 295 | if (storage) { 296 | return storage; 297 | } 298 | 299 | return new Storage(); 300 | } 301 | }; 302 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | exports.Storage = class Storage { 2 | constructor () { 3 | this.store = {}; 4 | } 5 | 6 | getItem (key) { 7 | return this.store[key]; 8 | } 9 | 10 | setItem (key, value) { 11 | return (this.store[key] = value); 12 | } 13 | 14 | removeItem (key) { 15 | delete this.store[key]; 16 | return this; 17 | } 18 | }; 19 | 20 | exports.payloadIsValid = function payloadIsValid (payload) { 21 | return payload && (!payload.exp || payload.exp * 1000 > new Date().getTime()); 22 | }; 23 | 24 | exports.getCookie = function getCookie (name) { 25 | if (typeof document !== 'undefined') { 26 | const value = `; ${document.cookie}`; 27 | const parts = value.split(`; ${name}=`); 28 | 29 | if (parts.length === 2) { 30 | return parts.pop().split(';').shift(); 31 | } 32 | } 33 | 34 | return null; 35 | }; 36 | 37 | exports.clearCookie = function clearCookie (name) { 38 | if (typeof document !== 'undefined') { 39 | document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`; 40 | } 41 | 42 | return null; 43 | }; 44 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive test/ 2 | --exit 3 | --timeout 10000 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feathersjs/authentication-client", 3 | "description": "The authentication plugin for feathers-client", 4 | "version": "1.0.2", 5 | "homepage": "https://github.com/feathersjs/authentication-client", 6 | "main": "lib/index.js", 7 | "keywords": [ 8 | "feathers", 9 | "feathers-plugin" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/feathersjs/authentication-client.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-client/issues" 24 | }, 25 | "engines": { 26 | "node": ">= 6" 27 | }, 28 | "scripts": { 29 | "publish": "git push origin --tags && npm run changelog && git push origin", 30 | "release:pre": "npm version prerelease && npm publish --tag pre --access public", 31 | "release:patch": "npm version patch && npm publish --access public", 32 | "release:minor": "npm version minor && npm publish --access public", 33 | "release:major": "npm version major && npm publish --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 | "coverage": "istanbul cover _mocha -- --opts mocha.opts", 38 | "test": "npm run lint && npm run coverage" 39 | }, 40 | "semistandard": { 41 | "sourceType": "module", 42 | "env": [ 43 | "mocha" 44 | ] 45 | }, 46 | "directories": { 47 | "lib": "lib" 48 | }, 49 | "dependencies": { 50 | "@feathersjs/errors": "^3.0.0", 51 | "debug": "^3.1.0", 52 | "jwt-decode": "^2.1.0" 53 | }, 54 | "devDependencies": { 55 | "@feathersjs/authentication": "^2.0.0", 56 | "@feathersjs/authentication-jwt": "^2.0.0", 57 | "@feathersjs/authentication-local": "^1.0.0", 58 | "@feathersjs/express": "^1.0.0", 59 | "@feathersjs/feathers": "^3.0.5", 60 | "@feathersjs/primus": "^3.0.3", 61 | "@feathersjs/primus-client": "^1.0.0", 62 | "@feathersjs/rest-client": "^1.3.0", 63 | "@feathersjs/socketio": "^3.0.0", 64 | "@feathersjs/socketio-client": "^1.0.0", 65 | "body-parser": "^1.15.2", 66 | "chai": "^4.0.0", 67 | "feathers-memory": "^2.0.0", 68 | "istanbul": "^1.1.0-alpha.1", 69 | "localstorage-memory": "^1.0.2", 70 | "mocha": "^5.0.0", 71 | "primus": "^7.0.0", 72 | "primus-emitter": "^3.1.1", 73 | "semistandard": "^13.0.1", 74 | "socket.io-client": "^2.0.0", 75 | "superagent": "^3.5.2", 76 | "ws": "^6.0.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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('@feathersjs/authentication'); 13 | 14 | const User = { 15 | email: 'admin@feathersjs.com', 16 | password: 'admin', 17 | permissions: ['*'] 18 | }; 19 | 20 | module.exports = function (settings, socketProvider) { 21 | const app = express(feathers()); 22 | 23 | app.configure(rest()) 24 | .configure(socketProvider === 'socketio' ? socketio() : primus({ 25 | transformer: 'websockets' 26 | })) 27 | .use(bodyParser.json()) 28 | .use(bodyParser.urlencoded({ extended: true })) 29 | .configure(auth(settings)) 30 | .configure(local()) 31 | .configure(jwt()) 32 | .use('/users', memory()) 33 | .use('/', express.static(path.resolve(__dirname, '/public'))) 34 | .use(errorHandler()); 35 | 36 | app.service('authentication').hooks({ 37 | before: { 38 | create: [ 39 | auth.hooks.authenticate(['jwt', 'local']) 40 | ], 41 | remove: [ 42 | auth.hooks.authenticate('jwt') 43 | ] 44 | } 45 | }); 46 | 47 | // Add a hook to the user service that automatically replaces 48 | // the password with a hash of the password before saving it. 49 | app.service('users').hooks({ 50 | before: { 51 | find: [ 52 | auth.hooks.authenticate('jwt') 53 | ], 54 | create: [ 55 | local.hooks.hashPassword({ passwordField: 'password' }) 56 | ] 57 | } 58 | }); 59 | 60 | // Create a user that we can use to log in 61 | app.service('users').create(User).catch(console.error); 62 | 63 | return app; 64 | }; 65 | -------------------------------------------------------------------------------- /test/hooks/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | 4 | const hooks = require('../../lib/hooks'); 5 | 6 | describe('hooks', () => { 7 | it('is CommonJS compatible', () => { 8 | expect(typeof require('../../lib/hooks')).to.equal('object'); 9 | }); 10 | 11 | it('is ES6 compatible', () => { 12 | expect(typeof hooks).to.equal('object'); 13 | }); 14 | 15 | it('exposes populateEntity hook', () => { 16 | expect(typeof hooks.populateEntity).to.equal('function'); 17 | }); 18 | 19 | it('exposes populateHeader hook', () => { 20 | expect(typeof hooks.populateHeader).to.equal('function'); 21 | }); 22 | 23 | it('exposes populateAccessToken hook', () => { 24 | expect(typeof hooks.populateAccessToken).to.equal('function'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/hooks/populate-access-token.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | const { populateAccessToken } = require('../../lib/hooks'); 4 | 5 | describe('hooks:populateAccessToken', () => { 6 | let hook; 7 | 8 | beforeEach(() => { 9 | hook = { 10 | type: 'before', 11 | params: {}, 12 | app: { 13 | get: () => 'my token' 14 | } 15 | }; 16 | }); 17 | 18 | describe('when not called as a before hook', () => { 19 | it('returns an error', () => { 20 | hook.type = 'after'; 21 | 22 | return populateAccessToken()(hook).catch(error => { 23 | expect(error).to.not.equal(undefined); 24 | }); 25 | }); 26 | }); 27 | 28 | it('adds the accessToken to hook.params', () => { 29 | return populateAccessToken()(hook).then(hook => { 30 | expect(hook.params.accessToken).to.equal('my token'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/hooks/populate-entity.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | const { populateEntity } = require('../../lib/hooks'); 4 | 5 | const user = { id: '1', name: 'Bob' }; 6 | 7 | describe('hooks:populateEntity', () => { 8 | let hook; 9 | let options; 10 | 11 | beforeEach(() => { 12 | hook = { 13 | type: 'after', 14 | params: { 15 | accessToken: 'my token', 16 | headers: {} 17 | }, 18 | result: {}, 19 | app: { 20 | passport: { 21 | verifyJWT: () => Promise.resolve({ userId: '1' }) 22 | }, 23 | set: () => {}, 24 | service: () => { 25 | return { 26 | get: () => Promise.resolve(user) 27 | }; 28 | } 29 | } 30 | }; 31 | 32 | options = { 33 | service: 'users', 34 | field: 'userId', 35 | entity: 'user' 36 | }; 37 | }); 38 | 39 | describe('when options.service is missing', () => { 40 | it('throws an error', () => { 41 | delete options.service; 42 | 43 | expect(() => { 44 | populateEntity(options); 45 | }).to.throw; 46 | }); 47 | }); 48 | 49 | describe('when options.field is missing', () => { 50 | it('throws an error', () => { 51 | delete options.field; 52 | 53 | expect(() => { 54 | populateEntity(options); 55 | }).to.throw; 56 | }); 57 | }); 58 | 59 | describe('when options.entity is missing', () => { 60 | it('throws an error', () => { 61 | delete options.entity; 62 | 63 | expect(() => { 64 | populateEntity(options); 65 | }).to.throw; 66 | }); 67 | }); 68 | 69 | describe('when not called as an after hook', () => { 70 | it('returns an error', () => { 71 | hook.type = 'before'; 72 | 73 | return populateEntity(options)(hook).catch(error => { 74 | expect(error).to.not.equal(undefined); 75 | }); 76 | }); 77 | }); 78 | 79 | it('populates an entity by token payload id', () => { 80 | return populateEntity(options)(hook).then(hook => { 81 | expect(hook.result.user).to.deep.equal(user); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/hooks/populate-header.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | const { populateHeader } = require('../../lib/hooks'); 4 | 5 | describe('hooks:populateHeader', () => { 6 | let hook; 7 | let options; 8 | 9 | beforeEach(() => { 10 | options = { header: 'Authorization' }; 11 | hook = { 12 | type: 'before', 13 | params: { 14 | accessToken: 'my token', 15 | headers: {} 16 | } 17 | }; 18 | }); 19 | 20 | describe('when options.header is missing', () => { 21 | it('throws an error', () => { 22 | delete options.header; 23 | 24 | expect(() => { 25 | populateHeader(options); 26 | }).to.throw; 27 | }); 28 | }); 29 | 30 | describe('when not called as a before hook', () => { 31 | it('returns an error', () => { 32 | hook.type = 'after'; 33 | 34 | return populateHeader(options)(hook).catch(error => { 35 | expect(error).to.not.equal(undefined); 36 | }); 37 | }); 38 | }); 39 | 40 | describe('when accessToken is missing', () => { 41 | it('does nothing', () => { 42 | delete hook.params.accessToken; 43 | return populateHeader(options)(hook).then(newHook => { 44 | expect(newHook).to.deep.equal(hook); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('when accessToken is present', () => { 50 | it('adds the accessToken to authorization header', () => { 51 | return populateHeader(options)(hook).then(hook => { 52 | expect(hook.params.headers.Authorization).to.equal('my token'); 53 | }); 54 | }); 55 | 56 | it('retains existing headers', () => { 57 | hook.params.headers = { 58 | authorization: 'existing', 59 | custom: 'custom' 60 | }; 61 | 62 | return populateHeader(options)(hook).then(hook => { 63 | expect(hook.params.headers.authorization).to.equal('existing'); 64 | expect(hook.params.headers.custom).to.equal('custom'); 65 | }); 66 | }); 67 | 68 | it('supports a custom token header', () => { 69 | options.header = 'custom'; 70 | return populateHeader(options)(hook).then(hook => { 71 | expect(hook.params.headers.custom).to.equal('my token'); 72 | }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | 4 | const feathers = require('@feathersjs/feathers'); 5 | const auth = require('../lib/index'); 6 | 7 | describe('Feathers Authentication Client', () => { 8 | let client; 9 | 10 | beforeEach(() => { 11 | client = feathers() 12 | .configure(auth()); 13 | }); 14 | 15 | it('throws an error if registered twice', () => { 16 | expect(() => { 17 | client.configure(auth()); 18 | }).to.throw(Error); 19 | }); 20 | 21 | it('exports default', () => { 22 | expect(auth.default).to.equal(auth); 23 | }); 24 | 25 | describe('default options', () => { 26 | it('sets the authorization header', () => { 27 | expect(client.passport.options.header).to.equal('Authorization'); 28 | }); 29 | 30 | it('sets the cookie name', () => { 31 | expect(client.passport.options.cookie).to.equal('feathers-jwt'); 32 | }); 33 | 34 | it('sets the name used for localstorage', () => { 35 | expect(client.passport.options.storageKey).to.equal('feathers-jwt'); 36 | }); 37 | 38 | it('sets the jwtStrategy', () => { 39 | expect(client.passport.options.jwtStrategy).to.equal('jwt'); 40 | }); 41 | 42 | it('sets the auth service path', () => { 43 | expect(client.passport.options.path).to.equal('/authentication'); 44 | }); 45 | 46 | it('sets the entity', () => { 47 | expect(client.passport.options.entity).to.equal('user'); 48 | }); 49 | 50 | it('sets the entity service', () => { 51 | expect(client.passport.options.service).to.equal('users'); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/integration/primus.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const feathers = require('@feathersjs/feathers'); 3 | const primus = require('@feathersjs/primus-client'); 4 | const localstorage = require('localstorage-memory'); 5 | const Primus = require('primus'); 6 | const Emitter = require('primus-emitter'); 7 | const { expect } = require('chai'); 8 | 9 | const authentication = require('../../lib/index'); 10 | const createApplication = require('../fixtures/server'); 11 | 12 | const port = 8998; 13 | const baseURL = `http://localhost:${port}`; 14 | const Socket = Primus.createSocket({ 15 | transformer: 'websockets', 16 | plugin: { 17 | 'emitter': Emitter 18 | } 19 | }); 20 | 21 | const app = createApplication({ secret: 'supersecret' }, 'primus'); 22 | let options; 23 | 24 | describe('Primus client authentication', function () { 25 | this.timeout(20000); 26 | let socket; 27 | let server; 28 | let client; 29 | 30 | before(done => { 31 | server = app.listen(port); 32 | server.once('listening', () => { 33 | socket = new Socket(baseURL); 34 | client = feathers() 35 | .configure(primus(socket, { timeout: 1000 })) 36 | .configure(authentication()); 37 | 38 | done(); 39 | }); 40 | }); 41 | 42 | beforeEach(() => { 43 | options = { 44 | strategy: 'local', 45 | email: 'admin@feathersjs.com', 46 | password: 'admin' 47 | }; 48 | }); 49 | 50 | after(done => { 51 | socket.socket.close(); 52 | server.close(done); 53 | }); 54 | 55 | it('can use client.passport.getJWT() to get the accessToken', () => { 56 | return client.authenticate(options).then(response => { 57 | client.passport.getJWT().then(accessToken => { 58 | expect(accessToken).to.equal(response.accessToken); 59 | }); 60 | }); 61 | }); 62 | 63 | it('can decode an accessToken with client.passport.verifyToken()', () => { 64 | return client.authenticate(options).then(response => { 65 | return client.passport.verifyJWT(response.accessToken).then(payload => { 66 | expect(payload.userId).to.equal(0); 67 | expect(payload.iss).to.equal('feathers'); 68 | expect(payload.sub).to.equal('anonymous'); 69 | }); 70 | }); 71 | }); 72 | 73 | it('local username password authentication', () => { 74 | return client.authenticate(options).then(response => { 75 | expect(response.accessToken).to.not.equal(undefined); 76 | expect(client.get('accessToken')).to.deep.equal(response.accessToken); 77 | }); 78 | }); 79 | 80 | it('supports socket timeouts', () => { 81 | return client.passport.connected().then(() => { 82 | client.passport.options.timeout = 0; 83 | 84 | return client.authenticate(options).catch(error => { 85 | client.passport.options.timeout = 5000; 86 | expect(error.message).to.equal('Authentication timed out'); 87 | }); 88 | }); 89 | }); 90 | 91 | it('`authenticated` event', done => { 92 | client.once('authenticated', response => { 93 | try { 94 | expect(response.accessToken).to.not.equal(undefined); 95 | expect(client.get('accessToken')).to.deep.equal(response.accessToken); 96 | done(); 97 | } catch (e) { 98 | done(e); 99 | } 100 | }); 101 | 102 | client.authenticate(options); 103 | }); 104 | 105 | it('local username password authentication and access to protected service', () => { 106 | return client.authenticate(options).then(response => { 107 | expect(response.accessToken).to.not.equal(undefined); 108 | return client.service('users').get(0).then(user => { 109 | expect(user.id).to.equal(0); 110 | }); 111 | }); 112 | }); 113 | 114 | it('local authentication with wrong credentials fails', () => { 115 | options.password = 'this is wrong'; 116 | return client.authenticate(options).catch(error => { 117 | expect(error.name).to.equal('NotAuthenticated'); 118 | expect(error.code).to.equal(401); 119 | }); 120 | }); 121 | 122 | it('authentication with no options and no stored accessToken fails', () => { 123 | return client.authenticate().catch(error => { 124 | expect(error.message).to.equal('Could not find stored JWT and no authentication type was given'); 125 | expect(error.code).to.equal(401); 126 | }); 127 | }); 128 | 129 | it('uses localStorage compatible stores', () => { 130 | const oldStorage = client.get('storage'); 131 | client.set('storage', localstorage); 132 | 133 | return client.authenticate(options).then(response => { 134 | expect(response.accessToken).to.equal(localstorage.getItem('feathers-jwt')); 135 | client.set('storage', oldStorage); 136 | }); 137 | }); 138 | 139 | it('accessToken is stored and re-authentication with stored accessToken works', () => { 140 | return client.authenticate(options).then(response => { 141 | expect(response.accessToken).to.not.equal(undefined); 142 | 143 | return client.authenticate().then(response => { 144 | expect(client.get('accessToken')).to.equal(response.accessToken); 145 | }); 146 | }); 147 | }); 148 | 149 | it('.logout works, does not grant access to protected service and accessToken is removed from localstorage', () => { 150 | return client.authenticate(options).then(response => { 151 | expect(response.accessToken).to.not.equal(undefined); 152 | return client.logout(); 153 | }) 154 | .then(() => { 155 | expect(client.get('accessToken')).to.equal(null); 156 | return Promise.resolve(client.get('storage').getItem('feathers-jwt')); 157 | }) 158 | .then(accessToken => { 159 | expect(accessToken).to.equal(undefined); 160 | 161 | return client.service('users').get(0).catch(error => { 162 | expect(error.code).to.equal(401); 163 | }); 164 | }); 165 | }); 166 | 167 | it('`logout` event', done => { 168 | client.once('logout', () => done()); 169 | 170 | client.authenticate(options).then(response => { 171 | expect(response.accessToken).to.not.equal(undefined); 172 | return client.logout(); 173 | }); 174 | }); 175 | 176 | it('authenticates automatically after reconnection', done => { 177 | client.authenticate(options).then(response => { 178 | app.primus.end({ reconnect: true }); 179 | server.close(() => { 180 | setTimeout(() => { 181 | const newApp = createApplication({ secret: 'supersecret' }, 'primus'); 182 | 183 | server = newApp.listen(port); 184 | server.once('listening', () => { 185 | newApp.primus.on('connection', s => { 186 | s.once('authenticate', data => { 187 | expect(data.accessToken).to.equal(response.accessToken); 188 | done(); 189 | }); 190 | }); 191 | }); 192 | }, 1000); 193 | }); 194 | }); 195 | }); 196 | 197 | it.skip('authenticates automatically after upgrade', done => { 198 | // TODO (EK): This is working but I have no idea how to manually 199 | // trigger socket upgrade events. 200 | app.primus.on('connection', serverSocket => { 201 | serverSocket.on('upgrade', () => { 202 | console.log('upgraded'); 203 | 204 | serverSocket.on('authenticate', data => { 205 | expect(data.accessToken).to.be.ok; 206 | done(); 207 | }); 208 | }); 209 | }); 210 | 211 | client.authenticate(options).then(response => { 212 | setTimeout(() => { 213 | app.primus.send('upgrade'); 214 | }, 500); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /test/integration/rest.test.js: -------------------------------------------------------------------------------- 1 | const feathers = require('@feathersjs/feathers'); 2 | const rest = require('@feathersjs/rest-client'); 3 | const localstorage = require('localstorage-memory'); 4 | const superagent = require('superagent'); 5 | const { expect } = require('chai'); 6 | 7 | const authentication = require('../../lib/index'); 8 | const createApplication = require('../fixtures/server'); 9 | 10 | const port = 8998; 11 | const baseURL = `http://localhost:${port}`; 12 | 13 | const app = createApplication({ secret: 'supersecret' }); 14 | let options; 15 | 16 | describe('REST client authentication', () => { 17 | let server; 18 | let client; 19 | 20 | before(done => { 21 | server = app.listen(port); 22 | server.once('listening', () => { 23 | client = feathers() 24 | .configure(rest(baseURL).superagent(superagent)) 25 | .configure(authentication()); 26 | 27 | done(); 28 | }); 29 | }); 30 | 31 | beforeEach(() => { 32 | options = { 33 | strategy: 'local', 34 | email: 'admin@feathersjs.com', 35 | password: 'admin' 36 | }; 37 | }); 38 | 39 | after(done => { 40 | server.close(); 41 | done(); 42 | }); 43 | 44 | it('can use client.passport.getJWT() to get the accessToken', () => { 45 | return client.authenticate(options).then(response => { 46 | client.passport.getJWT().then(accessToken => { 47 | expect(accessToken).to.equal(response.accessToken); 48 | }); 49 | }); 50 | }); 51 | 52 | it('can decode a accessToken with client.passport.verifyToken()', () => { 53 | return client.authenticate(options).then(response => { 54 | return client.passport.verifyJWT(response.accessToken).then(payload => { 55 | expect(payload.userId).to.equal(0); 56 | expect(payload.iss).to.equal('feathers'); 57 | expect(payload.sub).to.equal('anonymous'); 58 | }); 59 | }); 60 | }); 61 | 62 | it('local username password authentication', () => { 63 | return client.authenticate(options).then(response => { 64 | expect(response.accessToken).to.not.equal(undefined); 65 | expect(client.get('accessToken')).to.deep.equal(response.accessToken); 66 | }); 67 | }); 68 | 69 | it('`authenticated` event', done => { 70 | client.once('authenticated', response => { 71 | try { 72 | expect(response.accessToken).to.not.equal(undefined); 73 | expect(client.get('accessToken')).to.deep.equal(response.accessToken); 74 | done(); 75 | } catch (e) { 76 | done(e); 77 | } 78 | }); 79 | 80 | client.authenticate(options); 81 | }); 82 | 83 | it('local username password authentication and access to protected service', () => { 84 | return client.authenticate(options).then(response => { 85 | expect(response.accessToken).to.not.equal(undefined); 86 | return client.service('users').get(0).then(user => { 87 | expect(user.id).to.equal(0); 88 | }); 89 | }); 90 | }); 91 | 92 | it('local authentication with wrong credentials fails', () => { 93 | options.password = 'this is wrong'; 94 | return client.authenticate(options).catch(error => { 95 | expect(error.name).to.equal('NotAuthenticated'); 96 | expect(error.code).to.equal(401); 97 | }); 98 | }); 99 | 100 | it('authentication with no options and no stored accessToken fails', () => { 101 | return client.authenticate().catch(error => { 102 | expect(error.message).to.equal('Could not find stored JWT and no authentication type was given'); 103 | expect(error.code).to.equal(401); 104 | }); 105 | }); 106 | 107 | it('uses localStorage compatible stores', () => { 108 | const oldStorage = client.get('storage'); 109 | client.set('storage', localstorage); 110 | 111 | return client.authenticate(options).then(response => { 112 | expect(response.accessToken).to.equal(localstorage.getItem('feathers-jwt')); 113 | client.set('storage', oldStorage); 114 | }); 115 | }); 116 | 117 | it('accessToken is stored and re-authentication with stored accessToken works', () => { 118 | return client.authenticate(options).then(response => { 119 | expect(response.accessToken).to.not.equal(undefined); 120 | 121 | return client.authenticate().then(response => { 122 | expect(client.get('accessToken')).to.equal(response.accessToken); 123 | }); 124 | }); 125 | }); 126 | 127 | it('.logout works, does not grant access to protected service and accessToken is removed from localstorage', () => { 128 | return client.authenticate(options).then(response => { 129 | expect(response.accessToken).to.not.equal(undefined); 130 | return client.logout(); 131 | }) 132 | .then(() => { 133 | expect(client.get('accessToken')).to.equal(null); 134 | return Promise.resolve(client.get('storage').getItem('feathers-jwt')); 135 | }) 136 | .then(accessToken => { 137 | expect(accessToken).to.equal(undefined); 138 | 139 | return client.service('users').get(0).catch(error => { 140 | expect(error.code).to.equal(401); 141 | }); 142 | }); 143 | }); 144 | 145 | it('`logout` event', done => { 146 | client.once('logout', () => done()); 147 | 148 | client.authenticate(options).then(response => { 149 | expect(response.accessToken).to.not.equal(undefined); 150 | return client.logout(); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/integration/socketio.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const feathers = require('@feathersjs/feathers'); 3 | const socketio = require('@feathersjs/socketio-client'); 4 | const localstorage = require('localstorage-memory'); 5 | const io = require('socket.io-client'); 6 | const { expect } = require('chai'); 7 | 8 | const authentication = require('../../lib/index'); 9 | const createApplication = require('../fixtures/server'); 10 | 11 | const port = 8998; 12 | const baseURL = `http://localhost:${port}`; 13 | 14 | const app = createApplication({ secret: 'supersecret' }, 'socketio'); 15 | let options; 16 | 17 | describe('Socket.io client authentication', function () { 18 | this.timeout(20000); 19 | let socket; 20 | let server; 21 | let client; 22 | 23 | before(done => { 24 | server = app.listen(port); 25 | server.once('listening', () => { 26 | socket = io(baseURL); 27 | client = feathers() 28 | .configure(socketio(socket)) 29 | .configure(authentication()); 30 | 31 | done(); 32 | }); 33 | }); 34 | 35 | beforeEach(() => { 36 | options = { 37 | strategy: 'local', 38 | email: 'admin@feathersjs.com', 39 | password: 'admin' 40 | }; 41 | }); 42 | 43 | after(done => { 44 | socket.close(); 45 | server.close(done); 46 | }); 47 | 48 | it('can use client.passport.getJWT() to get the accessToken', () => { 49 | return client.authenticate(options).then(response => { 50 | client.passport.getJWT().then(accessToken => { 51 | expect(accessToken).to.equal(response.accessToken); 52 | }); 53 | }); 54 | }); 55 | 56 | it('can decode an accessToken with client.passport.verifyToken()', () => { 57 | return client.authenticate(options).then(response => { 58 | return client.passport.verifyJWT(response.accessToken).then(payload => { 59 | expect(payload.userId).to.equal(0); 60 | expect(payload.iss).to.equal('feathers'); 61 | expect(payload.sub).to.equal('anonymous'); 62 | }); 63 | }); 64 | }); 65 | 66 | it('local username password authentication', () => { 67 | return client.authenticate(options).then(response => { 68 | expect(response.accessToken).to.not.equal(undefined); 69 | expect(client.get('accessToken')).to.deep.equal(response.accessToken); 70 | }); 71 | }); 72 | 73 | it('supports socket timeouts', () => { 74 | return client.passport.connected().then(() => { 75 | client.passport.options.timeout = 0; 76 | 77 | return client.authenticate(options).catch(error => { 78 | client.passport.options.timeout = 5000; 79 | expect(error.message).to.equal('Authentication timed out'); 80 | }); 81 | }); 82 | }); 83 | 84 | it('`authenticated` event', done => { 85 | client.once('authenticated', response => { 86 | try { 87 | expect(response.accessToken).to.not.equal(undefined); 88 | expect(client.get('accessToken')).to.deep.equal(response.accessToken); 89 | done(); 90 | } catch (e) { 91 | done(e); 92 | } 93 | }); 94 | 95 | client.authenticate(options); 96 | }); 97 | 98 | it('local username password authentication and access to protected service', () => { 99 | return client.authenticate(options).then(response => { 100 | expect(response.accessToken).to.not.equal(undefined); 101 | return client.service('users').get(0).then(user => { 102 | expect(user.id).to.equal(0); 103 | }); 104 | }); 105 | }); 106 | 107 | it('local authentication with wrong credentials fails', () => { 108 | options.password = 'this is wrong'; 109 | return client.authenticate(options).catch(error => { 110 | expect(error.name).to.equal('NotAuthenticated'); 111 | expect(error.code).to.equal(401); 112 | }); 113 | }); 114 | 115 | it('authentication with no options and no stored accessToken fails', () => { 116 | return client.authenticate().catch(error => { 117 | expect(error.message).to.equal('Could not find stored JWT and no authentication type was given'); 118 | expect(error.code).to.equal(401); 119 | }); 120 | }); 121 | 122 | it('uses localStorage compatible stores', () => { 123 | const oldStorage = client.get('storage'); 124 | client.set('storage', localstorage); 125 | 126 | return client.authenticate(options).then(response => { 127 | expect(response.accessToken).to.equal(localstorage.getItem('feathers-jwt')); 128 | client.set('storage', oldStorage); 129 | }); 130 | }); 131 | 132 | it('accessToken is stored and re-authentication with stored accessToken works', () => { 133 | return client.authenticate(options).then(response => { 134 | expect(response.accessToken).to.not.equal(undefined); 135 | 136 | return client.authenticate().then(response => { 137 | expect(client.get('accessToken')).to.equal(response.accessToken); 138 | }); 139 | }); 140 | }); 141 | 142 | it('.logout works, does not grant access to protected service and accessToken is removed from localstorage', () => { 143 | return client.authenticate(options).then(response => { 144 | expect(response.accessToken).to.not.equal(undefined); 145 | return client.logout(); 146 | }) 147 | .then(() => { 148 | expect(client.get('accessToken')).to.equal(null); 149 | return Promise.resolve(client.get('storage').getItem('feathers-jwt')); 150 | }) 151 | .then(accessToken => { 152 | expect(accessToken).to.equal(undefined); 153 | 154 | return client.service('users').get(0).catch(error => { 155 | expect(error.code).to.equal(401); 156 | }); 157 | }); 158 | }); 159 | 160 | it('`logout` event', done => { 161 | client.once('logout', () => done()); 162 | 163 | client.authenticate(options).then(response => { 164 | expect(response.accessToken).to.not.equal(undefined); 165 | return client.logout(); 166 | }); 167 | }); 168 | 169 | it('authenticates automatically after reconnection', done => { 170 | client.authenticate(options).then(response => { 171 | app.io.close(); 172 | server.close(() => { 173 | setTimeout(() => { 174 | const newApp = createApplication({ secret: 'supersecret' }, 'socketio'); 175 | server = newApp.listen(port); 176 | 177 | server.once('listening', () => { 178 | newApp.io.on('connect', s => { 179 | s.once('authenticate', data => { 180 | expect(data.accessToken).to.equal(response.accessToken); 181 | done(); 182 | }); 183 | }); 184 | }); 185 | }, 1000); 186 | }); 187 | }); 188 | }); 189 | 190 | it.skip('authenticates automatically after upgrade', done => { 191 | // TODO (EK): This is working but I have no idea how to manually 192 | // trigger socket upgrade events. 193 | app.io.on('connect', serverSocket => { 194 | serverSocket.on('upgrade', () => { 195 | console.log('upgraded'); 196 | 197 | serverSocket.on('authenticate', data => { 198 | expect(data.accessToken).to.be.ok; 199 | done(); 200 | }); 201 | }); 202 | }); 203 | 204 | client.authenticate(options).then(response => { 205 | setTimeout(() => { 206 | app.io.engine.emit('upgrade'); 207 | }, 500); 208 | }); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /test/passport.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | 4 | const feathers = require('@feathersjs/feathers'); 5 | const Passport = require('../lib/passport'); 6 | const auth = require('../lib'); 7 | 8 | const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZXhwIjozNDc2MzkyNDgwLCJpYXQiOjE0NzYzOTI0ODAsImlzcyI6ImZlYXRoZXJzIn0.0V6NKoNszBPeIA72xWs2FDW6aPxOnHzEmskulq20uyo'; 9 | const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJleHAiOjE0NzYzOTI0ODAsImlhdCI6MTQ3NjM5MjQ4MCwiaXNzIjoiZmVhdGhlcnMifQ.6rzpXFqWSmNEotnWo8f-SQ2Ey4rbar3f0pQKNTHdq9A'; 10 | 11 | describe('Passport', () => { 12 | let passport; 13 | let app; 14 | let options; 15 | 16 | before(() => { 17 | options = Object.assign({}, auth.defaults); 18 | app = feathers(); 19 | passport = new Passport(app, options); 20 | }); 21 | 22 | describe.skip('getJWT', () => { 23 | it(`get unexpired token from storage`, () => { 24 | let storage = { 25 | getItem () { 26 | return Promise.resolve(validToken); 27 | } 28 | }; 29 | passport.getJWT('feathers-jwt', 'feathers-jwt', storage).then(jwt => { 30 | expect(jwt).to.equal(validToken); 31 | }); 32 | }); 33 | 34 | it(`expired jwt returns undefined`, () => { 35 | let storage = { 36 | getItem () { 37 | return Promise.resolve(expiredToken); 38 | } 39 | }; 40 | passport.getJWT('feathers-jwt', 'feathers-jwt', storage).then(jwt => { 41 | expect(jwt).to.equal(undefined); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('verifyJWT', () => { 47 | it('returns an error when token is missing', () => { 48 | return passport.verifyJWT().catch(error => { 49 | expect(error instanceof Error).to.equal(true); 50 | }); 51 | }); 52 | 53 | it('returns an error token is not a string', () => { 54 | return passport.verifyJWT(true).catch(error => { 55 | expect(error instanceof Error).to.equal(true); 56 | }); 57 | }); 58 | 59 | it('decodes a token string properly', () => { 60 | return passport.verifyJWT(validToken).then(payload => { 61 | expect(payload).to.deep.equal({ 62 | id: 1, 63 | exp: 3476392480, 64 | iat: 1476392480, 65 | iss: 'feathers' 66 | }); 67 | }); 68 | }); 69 | 70 | it('gracefully handles an invalid token', () => { 71 | let token = `lily`; 72 | return passport.verifyJWT(token).catch(error => { 73 | expect(error instanceof Error).to.equal(true); 74 | }); 75 | }); 76 | 77 | it('returns an error with an expired token', () => { 78 | return passport.verifyJWT(expiredToken).catch(error => { 79 | expect(error instanceof Error).to.equal(true); 80 | }); 81 | }); 82 | }); 83 | }); 84 | --------------------------------------------------------------------------------