├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── contributing.md ├── issue_template.md ├── pull_request_template.md └── workflows │ └── node.js.yml ├── .gitignore ├── .mocharc.js ├── .nvmrc ├── .nycrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── development.md ├── docs ├── .vitepress │ ├── components.d.ts │ ├── components │ │ ├── Tab.vue │ │ └── Tabs.vue │ ├── config.mts │ ├── meta.ts │ └── theme │ │ ├── Layout.vue │ │ ├── custom.css │ │ ├── index.ts │ │ └── store.ts ├── best-practices.md ├── configuration.md ├── getting-started.md ├── index.md ├── migration.md ├── overview.md ├── process-flows.md ├── public │ ├── favicon.ico │ ├── images │ │ ├── resendVerifySignup.png │ │ └── sendResetPwd.png │ ├── logo.png │ └── logo.svg ├── service-calls.md ├── service-hooks.md └── vite.config.ts ├── examples ├── js │ ├── .editorconfig │ ├── .eslintrc.json │ ├── .gitignore │ ├── config │ │ ├── default.json │ │ ├── production.json │ │ └── test.json │ ├── package.json │ ├── public │ │ ├── authmgmt.html │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── app.hooks.js │ │ ├── app.js │ │ ├── authentication.js │ │ ├── channels.js │ │ ├── index.js │ │ ├── logger.js │ │ ├── middleware │ │ │ └── index.js │ │ ├── sequelize.js │ │ └── services │ │ │ ├── auth-management │ │ │ ├── auth-management.class.js │ │ │ ├── auth-management.hooks.js │ │ │ ├── auth-management.service.js │ │ │ └── auth-management.utils.js │ │ │ ├── index.js │ │ │ ├── mailer │ │ │ ├── mailer.class.js │ │ │ ├── mailer.hooks.js │ │ │ └── mailer.service.js │ │ │ └── users │ │ │ ├── users.class.js │ │ │ ├── users.hooks.js │ │ │ ├── users.model.js │ │ │ └── users.service.js │ └── test │ │ ├── app.test.js │ │ ├── authentication.test.js │ │ └── services │ │ ├── auth-management.test.js │ │ ├── mailer.test.js │ │ └── users.test.js └── vue │ ├── ChangePassword.vue │ ├── ForgotPassword.vue │ ├── Login.vue │ ├── Readme.md │ ├── Signup.vue │ ├── VerifyMail.vue │ └── router.js ├── package-lock.json ├── package.json ├── src ├── client.ts ├── helpers │ ├── clone-object.ts │ ├── compare-passwords.ts │ ├── crypto.ts │ ├── date-or-number-to-number.ts │ ├── get-long-token.ts │ ├── get-short-token.ts │ ├── get-user-data.ts │ ├── hash-password.ts │ ├── index.ts │ ├── notify.ts │ ├── random-bytes.ts │ ├── random-digits.ts │ ├── sanitize-user-for-client.ts │ └── sanitize-user-for-notifier.ts ├── hooks │ ├── add-verification.ts │ ├── index.ts │ ├── is-verified.ts │ └── remove-verification.ts ├── index.ts ├── methods │ ├── check-unique.ts │ ├── identity-change.ts │ ├── password-change.ts │ ├── resend-verify-signup.ts │ ├── reset-password.ts │ ├── send-reset-pwd.ts │ ├── verify-signup-set-password.ts │ └── verify-signup.ts ├── options.ts ├── services │ ├── AuthenticationManagementBase.ts │ ├── AuthenticationManagementService.ts │ ├── CheckUniqueService.ts │ ├── IdentityChangeService.ts │ ├── PasswordChangeService.ts │ ├── ResendVerifySignupService.ts │ ├── ResetPwdLongService.ts │ ├── ResetPwdShortService.ts │ ├── SendResetPwdService.ts │ ├── VerifySignupLongService.ts │ ├── VerifySignupSetPasswordLongService.ts │ ├── VerifySignupSetPasswordShortService.ts │ ├── VerifySignupShortService.ts │ └── index.ts ├── setupAuthManagement.ts └── types.ts ├── test ├── client.test.ts ├── errors-async-await.test.ts ├── helpers.test.ts ├── helpers │ └── date-or-number-to-number.test.ts ├── hooks │ ├── add-verification.test.ts │ ├── is-verified.test.ts │ └── remove-verification.test.ts ├── methods │ ├── check-unique.test.ts │ ├── identity-change.test.ts │ ├── password-change.test.ts │ ├── resend-verify-signup.test.ts │ ├── reset-pwd-long.test.ts │ ├── reset-pwd-short.test.ts │ ├── send-reset-pwd.test.ts │ ├── verify-signup-long.test.ts │ ├── verify-signup-set-password-long.test.ts │ ├── verify-signup-set-password-short.test.ts │ └── verify-signup-short.test.ts ├── randoms.test.ts ├── scaffolding.test.ts ├── spy-on.test.ts ├── test-helpers │ ├── about-equal-date-time.ts │ ├── authenticationService.ts │ ├── basic-spy.ts │ ├── config.ts │ ├── index.ts │ └── make-date-time.ts └── types.ts ├── tsconfig.json └── tsconfig.test.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | fixme: 3 | enabled: true 4 | checks: 5 | argument-count: 6 | config: 7 | threshold: 5 8 | complex-logic: 9 | config: 10 | threshold: 4 11 | file-lines: 12 | config: 13 | threshold: 500 14 | method-complexity: 15 | config: 16 | threshold: 20 17 | method-count: 18 | config: 19 | threshold: 20 20 | method-lines: 21 | config: 22 | threshold: 100 23 | nested-control-flow: 24 | config: 25 | threshold: 4 26 | return-statements: 27 | config: 28 | threshold: 4 29 | similar-code: 30 | enabled: false 31 | identical-code: 32 | enabled: false 33 | 34 | exclude_patterns: 35 | - "examples" 36 | - "test" 37 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage 7 | 8 | test/ 9 | examples/ 10 | docs/ 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "./tsconfig.json" 10 | }, 11 | "plugins": [ 12 | "@typescript-eslint" 13 | ], 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:import/recommended", 18 | "plugin:import/typescript" 19 | ], 20 | "rules": { 21 | "semi": ["error", "always"], 22 | "@typescript-eslint/semi": ["error", "always"], 23 | "@typescript-eslint/strict-boolean-expressions": ["off"], 24 | "spaced-comment": ["off"], 25 | "@typescript-eslint/consistent-type-imports": ["warn", { "prefer": "type-imports" }], 26 | // import 27 | "import/order": ["error", { "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"] }], 28 | "import/no-duplicates": ["error"] 29 | }, 30 | "overrides": [ 31 | { 32 | "files": ["test/**/*.ts"], 33 | "rules": { 34 | "@typescript-eslint/ban-ts-comment": ["off"] 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.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 e-mail 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 | ### Tests 38 | 39 | [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. 40 | 41 | ### Documentation 42 | 43 | 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. 44 | 45 | ## External Modules 46 | 47 | 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. 48 | 49 | 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: 50 | 51 | ## Contributor Code of Conduct 52 | 53 | 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. 54 | 55 | 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. 56 | 57 | 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. 58 | 59 | 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. 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 62 | 63 | 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/) 64 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | - [ ] Tell us what broke. The more detailed the better. 4 | - [ ] Include the `feathers-gen-specs.json` file from your app. 5 | - [ ] Include the `src/services/[serviceName]/[serviceName].schema.?s` files if the issue involves the fields in one or more services. 6 | 7 | These last 2 items usually allow us to regen enough of your app to recreate the issue. 8 | We may otherwise ask you to provide a minimal GitHub repo or gist isolating the issue. 9 | 10 | ### Expected behavior 11 | Tell us what should happen 12 | 13 | ### Actual behavior 14 | Tell us what happens instead 15 | 16 | ### System configuration 17 | 18 | Tell us about the applicable parts of your setup. 19 | 20 | **Module versions** (especially the part that's not working): 21 | 22 | **NodeJS version**: 23 | 24 | **Operating System**: 25 | 26 | **Browser Version**: 27 | 28 | **React Native Version**: 29 | 30 | **Module Loader**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [16.x, 18.x, 20.x] 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm test 24 | 25 | coverage: 26 | needs: [ test ] 27 | name: coverage 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@master 31 | - uses: actions/setup-node@master 32 | with: 33 | node-version: '18' 34 | - run: npm ci 35 | - uses: paambaati/codeclimate-action@v3.0.0 36 | env: 37 | CC_TEST_REPORTER_ID: ac461a4a35fe5e70a011b20969f4298ad55da3498f1efbe0019d2bc3b99cf885 38 | with: 39 | coverageCommand: npm run coverage 40 | 41 | test-compile: 42 | needs: [ test ] 43 | name: test-compile 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - name: Use Node.js ${{ matrix.node-version }} 48 | uses: actions/setup-node@v1 49 | with: 50 | node-version: ${{ matrix.node-version }} 51 | - run: npm ci 52 | - run: npm run compile 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | .nyc_output 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # Commenting this out is preferred by some people, see 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | 31 | # Users Environment Variables 32 | .lock-wscript 33 | 34 | # The compiled/babelified modules 35 | dist/ 36 | 37 | ## editor 38 | .idea/ 39 | 40 | #.vscode/ 41 | .prettierrc 42 | 43 | docs/.vitepress/cache 44 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | extension: ["ts", "js"], 6 | package: path.join(__dirname, "./package.json"), 7 | ui: "bdd", 8 | spec: [ 9 | "./test/**/*.test.*", 10 | ], 11 | exit: true 12 | }; 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.18.0 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "include": [ 4 | "src/**/*.js", 5 | "src/**/*.ts" 6 | ], 7 | "exclude": [ 8 | "coverage/**", 9 | "node_modules/**", 10 | "**/*.d.ts", 11 | "**/*.test.ts" 12 | ], 13 | "sourceMap": true, 14 | "reporter": ["text", "html", "lcov"] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "pwa-node", 6 | "resolveSourceMapLocations": [ 7 | "${workspaceFolder}/**", 8 | "!**/node_modules/**" 9 | ], 10 | "request": "launch", 11 | "name": "Mocha Tests", 12 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha", 13 | "args": [ 14 | "--require", 15 | "ts-node/register", 16 | "--timeout", 17 | "999999", 18 | "--colors", 19 | "--recursive" 20 | ], 21 | "internalConsoleOptions": "openOnSessionStart", 22 | "env": { 23 | "NODE_ENV": "test", 24 | "TS_NODE_PROJECT": "tsconfig.test.json" 25 | }, 26 | "outputCapture": "std", 27 | "skipFiles": [ 28 | "/**" 29 | ] 30 | }, 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/public": true, 6 | "**/package-lock.json": true, 7 | "**/yarn.lock": true 8 | }, 9 | "workbench.colorCustomizations": { 10 | "activityBar.background": "#520064", 11 | "titleBar.activeBackground": "#520064", 12 | "titleBar.activeForeground": "#FAFBF4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 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 | # feathers-authentication-management 2 | **Sign up verification, forgotten password reset, and other capabilities for local authentication.** 3 | 4 |

5 | 6 |

7 | 8 | [![Build Status](https://img.shields.io/github/workflow/status/feathersjs-ecosystem/feathers-authentication-management/Node.js%20CI)](https://github.com/feathersjs-ecosystem/feathers-authentication-management/actions/workflows/node.js.yml?query=branch%3Amaster) 9 | [![Code Climate](https://codeclimate.com/github/feathersjs-ecosystem/feathers-authentication-management/badges/gpa.svg)](https://codeclimate.com/github/feathersjs-ecosystem/feathers-authentication-management) 10 | [![Test Coverage](https://codeclimate.com/github/feathersjs-ecosystem/feathers-authentication-management/badges/coverage.svg)](https://codeclimate.com/github/feathersjs-ecosystem/feathers-authentication-management/coverage) 11 | [![Dependency Status](https://img.shields.io/librariesio/release/npm/feathers-authentication-management)](https://libraries.io/npm/feathers-authentication-management) 12 | [![Download Status](https://img.shields.io/npm/dm/feathers-authentication-management.svg?style=flat-square)](https://www.npmjs.com/package/feathers-authentication-management) 13 | [![GitHub license](https://img.shields.io/github/license/feathersjs-ecosystem/feathers-authentication-management)](https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/master/LICENSE) 14 | 15 | ```bash 16 | npm i feathers-authentication-management 17 | ``` 18 | 19 | ## ⭐️⭐️⭐️ Documentation 20 | 21 | You need more information? Please have a look at our new docs at [feathers-a-m.netlify.app](https://feathers-a-m.netlify.app/). 22 | 23 | ### Maintainers 24 | 25 | Refer to our [guidelines](./development.md). 26 | 27 | ## License 28 | 29 | This project is licensed under the MIT License - see the [license file](./LICENSE) for details 30 | -------------------------------------------------------------------------------- /development.md: -------------------------------------------------------------------------------- 1 | # Guidelines for maintainers 2 | 3 | ## Install Change log generator 4 | 5 | In order to be able to generate the changelog for your published app/modules you need this [gem](https://github.com/skywinder/github-changelog-generator), which creates a log file based on **tags**, **issues** and merged **pull requests** (and splits them into separate lists according to labels) from :octocat: GitHub Issue Tracker. This requires you to install (e.g. for Windows) [Ruby](http://rubyinstaller.org/downloads/) and its [DevKit](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit). 6 | 7 | ## Release 8 | 9 | The same process applies when releasing a patch, minor or major version, i.e. the following tasks are done automatically on release: 10 | * increase the package version number in the **package.json** file (frontend and backend API) 11 | * publish the module on the NPM registry 12 | * create a tag accordingly in the git repository and push it 13 | * generates the changelog in the git repository and push it 14 | 15 | *This requires you to have a NPM and GitHub account with the appropriate rights, if you'd like to become a maintainer please tell us* 16 | 17 | Depending on the release type the following command will do the job (where type is either `patch`, `minor`, `major`): 18 | ```bash 19 | npm run release:type 20 | ``` 21 | 22 | **As we have a lot of issues/PRs to be integrated in change log please [generate a GitHub token](https://github.com/github-changelog-generator/github-changelog-generator#github-token) to avoid rate-limiting on their API and set the `CHANGELOG_GITHUB_TOKEN` environment variable to your token before publishing** 23 | 24 | *The changelog suffers from the following [issue](https://github.com/github-changelog-generator/github-changelog-generator/issues/497) so you might have to edit the generated changelog when pushing on different branches* 25 | -------------------------------------------------------------------------------- /docs/.vitepress/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | Tab: typeof import('./components/Tab.vue')['default'] 11 | Tabs: typeof import('./components/Tabs.vue')['default'] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Tab.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Tabs.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 55 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { version } from '../../package.json' 3 | import { description, name, ogImage, ogUrl } from './meta'; 4 | 5 | export default defineConfig({ 6 | title: "feathers-auth-mgmt", 7 | description: 8 | "Sign up verification, forgotten password reset, and other capabilities for local authentication", 9 | head: [ 10 | ['link', { rel: 'icon', href: '/favicon.ico' }], 11 | ["meta", { name: "theme-color", content: "#64007a" }], 12 | ["meta", { property: "og:title", content: name }], 13 | ["meta", { property: "og:description", content: description }], 14 | ["meta", { property: "og:url", content: ogUrl }], 15 | ["meta", { property: "og:image", content: ogImage }], 16 | ["meta", { name: "twitter:title", content: name }], 17 | ["meta", { name: "twitter:description", content: description }], 18 | ["meta", { name: "twitter:image", content: ogImage }], 19 | ["meta", { name: "twitter:card", content: "summary_large_image" }], 20 | ], 21 | themeConfig: { 22 | logo: '/logo.svg', 23 | editLink: { 24 | pattern: 'https://github.com/feathersjs-ecosystem/feathers-authentication-management/edit/master/docs/:path', 25 | text: 'Edit this page on GitHub' 26 | }, 27 | socialLinks: [ 28 | { 29 | icon: "twitter", 30 | link: "https://twitter.com/feathersjs", 31 | }, 32 | { 33 | icon: "discord", 34 | link: "https://discord.gg/qa8kez8QBx", 35 | }, 36 | { 37 | icon: "github", 38 | link: "https://github.com/feathersjs-ecosystem/feathers-authentication-management", 39 | }, 40 | ], 41 | footer: { 42 | message: 'Released under the MIT License.', 43 | copyright: 'Copyright © 2022' 44 | }, 45 | sidebar: [ 46 | { 47 | text: 'Guide', 48 | items: [ 49 | { 50 | text: "Overview", 51 | link: "/overview" 52 | }, { 53 | text: "Getting Started", 54 | link: "/getting-started" 55 | }, { 56 | text: "Process Flows", 57 | link: "/process-flows" 58 | }, { 59 | text: "Configuration", 60 | link: "/configuration" 61 | }, { 62 | text: "Service Hooks", 63 | link: "/service-hooks" 64 | }, { 65 | text: "Service Calls", 66 | link: "/service-calls" 67 | }, { 68 | text: "Best Practices", 69 | link: "/best-practices" 70 | }, { 71 | text: "Migration", 72 | link: "/migration" 73 | } 74 | ], 75 | }, 76 | ], 77 | nav: [ 78 | { 79 | text: `v${version}`, 80 | items: [ 81 | { 82 | text: 'Changelog', 83 | link: 'https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/master/CHANGELOG.md' 84 | }, 85 | { 86 | text: 'Contributing', 87 | link: 'https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/master/.github/contributing.md' 88 | } 89 | ] 90 | } 91 | ], 92 | algolia: { 93 | appId: 'G92UWK8VPN', 94 | apiKey: 'fd13f8200bfdea8667089e1bb7857c1e', 95 | indexName: 'feathers-auth-mgmt' 96 | } 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /docs/.vitepress/meta.ts: -------------------------------------------------------------------------------- 1 | export const name = "feathers-authentication-management"; 2 | export const description = "Adds sign up verification, forgotten password reset, and other capabilities to local feathers-authentication"; 3 | 4 | export const ogUrl = "https://feathers-a-m.netlify.app/"; 5 | export const ogImage = "https://feathers-a-m.netlify.app/logo.png"; 6 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #64007a; 3 | --vp-c-accent: #64007a; 4 | --vp-c-brand: var(--primary); 5 | --vp-c-brand-light: #9000b0; 6 | --vp-c-brand-lighter: #ffffff; 7 | } 8 | 9 | .dark { 10 | --vp-c-brand: #b200da; 11 | --vp-c-brand-dark: #d000ff; 12 | --vp-c-brand-darker: #d000ff; 13 | } 14 | 15 | /** 16 | * Component: Home 17 | * -------------------------------------------------------------------------- */ 18 | 19 | :root { 20 | --vp-home-hero-name-color: transparent; 21 | --vp-home-hero-name-background: -webkit-linear-gradient( 22 | 120deg, 23 | #64007a 20%, 24 | #8e00ad 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // .vitepress/theme/index.js 2 | import Theme from 'vitepress/theme' 3 | import './custom.css' 4 | import 'uno.css' 5 | import Layout from './Layout.vue' 6 | 7 | import Tab from '../components/Tab.vue' 8 | import Tabs from '../components/Tabs.vue' 9 | 10 | export default { 11 | ...Theme, 12 | Layout, 13 | enhanceApp({ app }) { 14 | // Globally register components so they don't have to be imported in the template. 15 | app.component('Tabs', Tabs) 16 | app.component('Tab', Tab) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/store.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState, useStorage } from "@vueuse/core" 2 | 3 | export const useGlobalLanguage = createGlobalState(() => useStorage("global-id", "ts")) 4 | -------------------------------------------------------------------------------- /docs/best-practices.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Best Practices 3 | --- 4 | 5 | # Best Practices 6 | 7 | ## Security 8 | 9 | - The user must be authenticated when the short token is used, making the short token less appealing 10 | as an attack vector. 11 | - The long and short tokens are erased on successful verification and password reset attempts. 12 | New tokens must be acquired for another attempt. 13 | - API parameters are verified to be strings. If the parameter is an object, the values of its props are 14 | verified to be strings. 15 | - `options.identifyUserProps` restricts the property names allowed in param objects. 16 | - In order to protect sensitive data, you should set a hook that prevent `PATCH` or `PUT` calls on 17 | authentication-management related properties: 18 | 19 | ```javascript 20 | // users.hooks.js 21 | before: { 22 | update: [ 23 | disallow("external") 24 | ], 25 | patch: [ 26 | iff(isProvider('external'), preventChanges( 27 | true, 28 | 'isVerified', 29 | 'resetExpires' 30 | 'resetShortToken', 31 | 'resetToken', 32 | 'verifyChanges', 33 | 'verifyExpires', 34 | 'verifyShortToken', 35 | 'verifyToken', 36 | )), 37 | ], 38 | }, 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | layout: home 4 | hero: 5 | name: feathers-authentication-management 6 | tagline: Sign up verification, forgotten password reset, and other capabilities for local authentication. 7 | image: 8 | src: /logo.svg 9 | alt: feathers-authentication-management 10 | actions: 11 | - theme: brand 12 | text: Get Started 13 | link: /getting-started 14 | - theme: alt 15 | text: Config 16 | link: /configuration 17 | - theme: alt 18 | text: View on GitHub 19 | link: https://github.com/feathersjs-ecosystem/feathers-authentication-management 20 | --- 21 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | This guide explains the new features and changes necessary to migrate to `feathers-authentication-management` (called `f-a-m` from now on) v4. The migration should be fairly easy. There's no breaking change at all. So just install the pre-release. That should be it. Just continue reading if you're curious what has changed. 4 | 5 | ```bash 6 | npm i feathers-authentication-management 7 | ``` 8 | 9 | ## ❗️❗️❗️ Don't expose `f-a-m` data to socket.io by default 10 | 11 | Do you use `socket.io` and [channels](https://docs.feathersjs.com/api/channels.html) with `feathers-authentication-management`? Did you know, that the service `authManagment` publishes every `create` request to channels? We created a [test](https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/illustrate-publish-leak/test/scaffolding.test.js#L108) to illustrate the behavior. See the [test results](https://github.com/feathersjs-ecosystem/feathers-authentication-management/runs/4764626400?check_suite_focus=true). 12 | If you do not catch that, every client received your `data` from `authManagement`. This is fixed in `f-a-m` v4 and follows the same as the official `@feathersjs/authentication`. If you don't filter the data from `authManagement`, you should upgrade asap or handle data from `authManagement` in your `channels`-file! 13 | 14 | This could be a breaking change, if you use the published data of `authManagement` intentionally which is considered bad practice. You should use hooks instead! 15 | 16 | ## Typescript 17 | 18 | `feathers-authentication-management` v4 is rewritten in typescript. That means autocompletition in your IDE. 19 | 20 | ## Separate services and custom methods 21 | 22 | Before `f-a-m` v4 there was only a main service, with a lot of `actions` for the `create` method. `f-a-m` v4 also has that but also exposes two new options to configure your feathers app. So we have three ways: 23 | 1. `old fashioned`: one main service 24 | 2. `Separate services`: For every `action` we now have a separate service that just handles the `action` via its `create` method. 25 | 3. `Custom Methods`: Feathers v5 (yet to be released) introduces a way of defining custom methods. See the official [Feathers v5 docs](https://dove.docs.feathersjs.com/api/services.html#custom-methods). `f-a-m` v4 prepares for Feathers v5 with custom methods. The old fashioned main service also has all of the `actions` as custom methods. 26 | 27 | To illustrate the new services and custom methods and its corresponding old fashioned `action`, please see this table: 28 | 29 | | `action` | old-fashioned `create` on `Service` | separate Service with `create` method | `custom methods` on `Service` (feathers v5 preparation) | 30 | |---|---|---|---| 31 | | `checkUnique` | *unchanged* | `CheckUniqueService` | `checkUnique` method | 32 | | `resendVerifySignup` | *unchanged* | `ResendVerifySignupService` | `resendVerifySignup` method | 33 | | `verifySignupLong` | *unchanged* | `VerifySignupLongService` | `verifySignupLong` method | 34 | | `verifySignupShort` | *unchanged* | `VerifySignupShortService` | `verifySignupShort` method | 35 | | `verifySignupSetPasswordLong` | *unchanged* | `VerifySignupSetPasswordLongService` | `verifySignupSetPasswordLong` method | 36 | | `verifySignupSetPasswordShort` | *unchanged* | `VerifySignupSetPasswordShortService` | `verifySignupSetPasswordShort` method | 37 | | `sendResetPwd` | *unchanged* | `SendResetPwdService` | `sendResetPassword` method | 38 | | `resetPwdLong` | *unchanged* | `ResetPwdLongService` | `resetPasswordLong` method | 39 | | `resetPwdShort` | *unchanged* | `ResetPwdShortService` | `resetPasswordShort` method | 40 | | `passwordChange` | *unchanged* | `PasswordChangeService` | `passwordChange` method | 41 | | `identityChange` | *unchanged* | `IdentityChangeService` | `identityChange` method | 42 | | `options` | *unchanged* | *none* | *none* | 43 | 44 | But the `data` for the new **separate services** and **custom methods** is also flattened compared to the old fashioned main service. See the following example for the action: `'sendResetPwd'`: 45 | 46 | ### Old Fashioned 47 | 48 | ```js 49 | const { AuthenticationManagementService } = require('feathers-authentication-management'); 50 | app.use('auth-management', new AuthenticationManagementService(app, options)); 51 | 52 | app.service("auth-management").create({ 53 | action: "sendResetPwd", 54 | value: { 55 | email: "me@example.com", 56 | }, 57 | }); 58 | ``` 59 | 60 | ### Separate Service 61 | 62 | ```js 63 | const { SendResetPwdService } = require('feathers-authentication-management'); 64 | app.use("auth-management/send-reset-password", new SendResetPwdService(app, options)); 65 | 66 | app.service("auth-management/send-reset-password").create({ 67 | user: { 68 | email: "me@example.com", 69 | }, 70 | }); 71 | ``` 72 | 73 | ### Custom Method 74 | 75 | ```js 76 | const { AuthenticationManagementService } = require('feathers-authentication-management'); 77 | app.use('auth-management', new AuthenticationManagementService(app, options)); 78 | 79 | app.service("auth-management").sendResetPassword({ 80 | user: { 81 | email: "me@example.com", 82 | }, 83 | }); 84 | ``` 85 | 86 | This is also documented in the chapter [Service Calls](./service-calls). Check that chapter out, if you need more information. 87 | 88 | ## Multi support for hook `addVerification` 89 | 90 | The hook `addVerification` was not able to handle `multi` data. It is now. 91 | 92 | ## Recommended setup 93 | 94 | The old docs of `f-a-m` v3 said, that you need to use `app.configure(authManagement())`. We don't know why, but it is not necessary. You can use: 95 | ```js 96 | // services/auth-management/auth-management.service.js 97 | const { 98 | AuthenticationManagementService, 99 | } = require("feathers-authentication-management"); 100 | 101 | const notifier = require("./notifier"); 102 | 103 | module.exports = function (app) { 104 | app.use( 105 | "/auth-management", 106 | new AuthenticationManagementService(app, { 107 | notifier: notifier(app), 108 | }) 109 | ); 110 | }; 111 | ``` 112 | 113 | ## Recommended Service Path 114 | 115 | The default path of `app.configure(authManagement())` is `app.service('authManagement')`. Note the `camelCasing`. We consider this an anti-pattern and recommend using `kebab-casing`. That's why the [Getting Started](./getting-started) guide uses `kebab-casing`. 116 | 117 | The hook `addVerification` optionally takes the service path as first argument. Because we don't want to break things, the default path stays `'authManagement'` as in `f-a-m` v3. So, if you change to `kebab-case`, please make sure, to call `addVerification('auth-management')`. 118 | 119 | ## Option `passwordField` 120 | 121 | `f-a-m` v3 did not use a `passwordField` option. It defaults to `password`. This new option follows the options for `@feathersjs/authentication`. 122 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 |

4 | 5 |

6 | 7 | ![Build Status](https://img.shields.io/github/workflow/status/feathersjs-ecosystem/feathers-authentication-management/Node.js%20CI) 8 | ![Code Climate](https://codeclimate.com/github/feathersjs-ecosystem/feathers-authentication-management/badges/gpa.svg) 9 | ![Test Coverage](https://codeclimate.com/github/feathersjs-ecosystem/feathers-authentication-management/badges/coverage.svg) 10 | ![Dependency Status](https://img.shields.io/librariesio/release/npm/feathers-authentication-management) 11 | ![Download Status](https://img.shields.io/npm/dm/feathers-authentication-management.svg?style=flat-square) 12 | ![GitHub license](https://img.shields.io/github/license/feathersjs-ecosystem/feathers-authentication-management) 13 | 14 | Sign up verification, forgotten password reset, and other capabilities for local authentication. 15 | 16 | This project is built for [Feathers](http://feathersjs.com) – an open source framework for real-time applications and REST APIs. 17 | 18 | The Feathers core provides a [LocalStrategy](https://docs.feathersjs.com/api/authentication/local.html) for authenticating users with a username/e-mail and password combination. However, it does not include methods such as the verification of a user's e-mail address or sending a password reset link. The purpose of `feathers-authentication-management` is to extend Feathers's local authentication with such functionalities. 19 | 20 | ## Features 21 | 22 | - User verification by sending a token-based verification URL. 23 | - Password reset notifications, e. g. for forgotten password functions. 24 | - Secure password changes. 25 | - Processing of identity changes such as a new e-mail address, or cellphone number. 26 | 27 | These actions require the notification of the user via a communication transport, for which the identity of the user is verified. This can be an e-mail address, a cellphone number or any other communication endpoint. `feathers-authentication-management` can be configured to use any available communication transport. 28 | 29 | The installation and configuration of `feathers-authentication-management` require a Feathers application configured with local authentication and with a communication transport such as e-mail or SMS. A basic installation is described in chapter [Getting Started](./getting-started), while an overview of the functionality is given in chapter [Process Flows](./process-flows). 30 | 31 | For most of the actions, `feathers-authentication-management` sends notifications containing tokens to the users. These tokens can be long and embedded in URLs if e-mails are used for the notifications (token length of 30 characters by default). However, if the user has to enter the token manually, e. g. if the token is send directly in a SMS, the service provides also actions with short tokens (6 digits/characters by default). 32 | 33 | Details about settings and the implementation of a notifier function can be found in chapter [Configuration](./configuration). All possible actions of this service are described in chapter [Service Calls](./service-calls). 34 | 35 | ## Help 36 | 37 | - Open an issue or come talk on the Feathers Slack ([slack.feathersjs.com](http://slack.feathersjs.com/)). 38 | 39 | - Additional resources: 40 | 41 | - [Setting up email verification in FeathersJS](https://hackernoon.com/setting-up-email-verification-in-feathersjs-ce764907e4f2) – The classic how-to by Imre Gelens (02/2018). 42 | 43 | - [The how and why of 2FA using Twilio and Feathers.js — Simple 2FA tutorial](https://harryhopalot.medium.com/the-how-and-why-of-2fa-using-twilio-and-feathers-js-simple-2fa-tutorial-e64a930a57a8) – Medium article by Harry Blakiston Houston (06/2018), 44 | 45 | ## License 46 | 47 | Licensed under the [MIT license](https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/master/LICENSE). 48 | -------------------------------------------------------------------------------- /docs/process-flows.md: -------------------------------------------------------------------------------- 1 | # Process Flows 2 | 3 | Some of the process flows related to this service are described in this chapter in more detail. They are meant as examples how the service works and what additional implementations are required. 4 | 5 | ## Sign Up Verification 6 | 7 | The sign up verification process is an interplay between the Feathers backend, a client and a communication transport method such as e-mail. Its flow shown in the following figure: 8 | 9 |

10 | 11 |

12 | 13 | Some of these steps have to be implemented manually on client and server side, others are performed automatically by the `feathers-authentication-management` service. 14 | 15 | Sign up verification is performed in an `addVerification` hook in the `create` method of the users service. This hook creates a long and/or short verification token every time a new user is created. It stores these values in the fields `verifyToken`and `verifyShortToken` of the created user item. The expiration date of these tokens is calculated and stored in field `verifyExpires`. The service does not automatically send a notification to the new user, a call of the notifier function has to be implemented in the after hook of the users `create` method. See [Service Hooks](./service-hooks) for more details. 16 | 17 | The verification e-mail contains an URL with the long token as a parameter, or – in case of an SMS – it could also just contain the value of the short token. The details depend on your requirements. Also, the implementation of the URL target is up to you. For example, the URL could be a direct call of your Feathers API and trigger the verification. But in general you would like to give the users some feedback. In that case, the URL will lead to your client, the client will send the token to the Feathers API and informs your user about the response. The API could also trigger an additional notification about the success of the verification process (step 7 in the figure). 18 | 19 | ## Password Resets 20 | 21 | A change or reset of a password is more secure, if the user is verified, e. g. by asking the user for a verification by e-mail. The following figure shows such as process flow: 22 | 23 |

24 | 25 |

26 | 27 | The client sends a [sendResetPwd](./service-calls#sendresetpwd) service call to the `feathers-authentication-management` service. The service creates a short and/or long password reset token, stores it to the user item, and triggers the notifier function to send the token to the user. 28 | 29 | The user opens the URL containing the reset token in the e-mail. This URL leads to th client, where the user is asked for a new password. The client sends the new password together with the token to the Feathers API. The API could also trigger another notification about the success of the password change (step 8 in the figure). 30 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-authentication-management/5024c2d0dcc1fd95388ec13bed99d235ca74df74/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/images/resendVerifySignup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-authentication-management/5024c2d0dcc1fd95388ec13bed99d235ca74df74/docs/public/images/resendVerifySignup.png -------------------------------------------------------------------------------- /docs/public/images/sendResetPwd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-authentication-management/5024c2d0dcc1fd95388ec13bed99d235ca74df74/docs/public/images/sendResetPwd.png -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-authentication-management/5024c2d0dcc1fd95388ec13bed99d235ca74df74/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 5 | 7 | -------------------------------------------------------------------------------- /docs/service-hooks.md: -------------------------------------------------------------------------------- 1 | # Service Hooks 2 | 3 | The `feathers-authentication-management` service does not handle creation of a new user account nor the sending of the initial sign up verification notification. Instead hooks are provided to be used with the `users` service `create` method. If you set a service path other than the default of `'/authManagement'`, the custom path name must be passed into the hook. 4 | 5 | ## addVerification 6 | 7 | This hook is made exclusively for the `/users` service. Creates tokens and sets default `authManagement` data for users. 8 | 9 | | before | after | methods | multi | details | 10 | | ------ | ----- | --------------------- | ----- | ---------- | 11 | | yes | no | create, patch, update | no | [source](https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/master/src/hooks/add-verification.ts) | 12 | 13 | - **Arguments:** 14 | - `path?: string` 15 | 16 | | Argument | Type | Default | Description | 17 | | -------- | :------: | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | 18 | | `path` | `string` | `authManagement` | The path of the service. This is required, if the service is configured with a different path than the default `authManagement`. | 19 | 20 | - **Example:** 21 | 22 | ```javascript 23 | // src/services/users/users.hooks.js 24 | const { authenticate } = require("@feathersjs/authentication").hooks; 25 | const { hashPassword, protect } = require("@feathersjs/authentication-local").hooks; 26 | const { addVerification, removeVerification } = require("feathers-authentication-management"); 27 | const authNotifier = require("./path-to/your/notifier"); 28 | 29 | const { 30 | disallow, 31 | iff, 32 | isProvider, 33 | preventChanges, 34 | } = require("feathers-hooks-common"); 35 | 36 | module.exports = { 37 | before: { 38 | all: [], 39 | find: [authenticate("jwt")], 40 | get: [authenticate("jwt")], 41 | create: [ 42 | hashPassword("password"), 43 | addVerification("auth-management"), // adds .isVerified, .verifyExpires, .verifyToken, .verifyChanges 44 | ], 45 | update: [ 46 | disallow("external"), 47 | authenticate("jwt"), 48 | hashPassword("password"), 49 | ], 50 | patch: [ 51 | authenticate("jwt"), 52 | iff( 53 | isProvider("external"), 54 | preventChanges( 55 | true, 56 | "email", 57 | "isVerified", 58 | "verifyToken", 59 | "verifyShortToken", 60 | "verifyExpires", 61 | "verifyChanges", 62 | "resetToken", 63 | "resetShortToken", 64 | "resetExpires" 65 | ), 66 | hashPassword("password") 67 | ), 68 | ], 69 | remove: [authenticate("jwt"), hashPassword("password")], 70 | }, 71 | after: { 72 | all: [], 73 | find: [protect("password")], 74 | get: [protect("password")], 75 | create: [ 76 | protect("password"), 77 | (context) => { 78 | // Send an e-mail/SMS with the verification token 79 | authNotifier(context.app).notifier("verifySignupLong", context.result); 80 | }, 81 | removeVerification(), // removes verification/reset fields other than .isVerified from the response 82 | ], 83 | update: [protect("password")], 84 | patch: [protect("password")], 85 | remove: [protect("password")], 86 | }, 87 | error: { 88 | all: [], 89 | find: [], 90 | get: [], 91 | create: [], 92 | update: [], 93 | patch: [], 94 | remove: [], 95 | }, 96 | }; 97 | ``` 98 | 99 | ## isVerified 100 | 101 | Throws, if requesting user is not verified (`params.user.isVerified`) and passes otherwise. Please make sure to call `authenticate('jwt')` before. 102 | 103 | | before | after | methods | multi | details | 104 | | ------ | ----- | ------- | ----- | ---------- | 105 | | yes | no | all | yes | [source](https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/master/src/hooks/is-verified.ts) | 106 | 107 | - **Arguments:** 108 | 109 | - _none_ 110 | 111 | - **Example:** 112 | 113 | ```js 114 | const { authenticate } = require("@feathersjs/authentication").hooks; 115 | const { isVerified } = require("feathers-authentication-management"); 116 | 117 | module.exports = { 118 | before: { 119 | all: [authenticate("jwt"), isVerified()], 120 | find: [], 121 | get: [], 122 | create: [], 123 | update: [], 124 | patch: [], 125 | remove: [], 126 | }, 127 | after: { 128 | all: [], 129 | find: [], 130 | get: [], 131 | create: [], 132 | update: [], 133 | patch: [], 134 | remove: [], 135 | }, 136 | error: { 137 | all: [], 138 | find: [], 139 | get: [], 140 | create: [], 141 | update: [], 142 | patch: [], 143 | remove: [], 144 | }, 145 | }; 146 | ``` 147 | 148 | ## removeVerification 149 | 150 | This hook is made exclusively for the `/users` service. It deletes data on user items for external requests that was added for `feathers-authentication-management` to work. It is similar to the `protect('password')` hook from `@feathersjs/authentication-local`. 151 | It deletes `verifyExpires`, `resetExpires` and `verifyChanges` and if `ifReturnToken: true` it also deletes `verifyToken`, `verifyShortToken`, `resetToken` and `resetShortToken`. 152 | 153 | | before | after | methods | multi | details | 154 | | ------ | ----- | ------- | ----- | ---------- | 155 | | no | yes | all | yes | [source](https://github.com/feathersjs-ecosystem/feathers-authentication-management/blob/master/src/hooks/remove-verification.ts) | 156 | 157 | - **Arguments:** 158 | - `ifReturnToken?: boolean` 159 | 160 | | Argument | Type | Default | Description | 161 | | --------------- | :-------: | ------- | ----------- | 162 | | `ifReturnToken` | `boolean` | `false` | removes | 163 | 164 | - **Example:** 165 | See the example under [addVerification](./configuration#addverification) 166 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Components from 'unplugin-vue-components/vite' 3 | import Unocss from 'unocss/vite' 4 | 5 | import presetUno from '@unocss/preset-uno' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | Components({ 10 | include: [/\.vue/, /\.md/], 11 | dirs: '.vitepress/components', 12 | dts: '.vitepress/components.d.ts', 13 | }), 14 | Unocss({ 15 | presets: [ 16 | presetUno(), 17 | ], 18 | }) 19 | ], 20 | }) 21 | -------------------------------------------------------------------------------- /examples/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 | -------------------------------------------------------------------------------- /examples/js/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2018 10 | }, 11 | "extends": [ 12 | "eslint:recommended" 13 | ], 14 | "rules": { 15 | "indent": [ 16 | "error", 17 | 2 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/js/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/tasks.json 42 | !.vscode/launch.json 43 | !.vscode/extensions.json 44 | 45 | ### Linux ### 46 | *~ 47 | 48 | # temporary files which can be created if a process still has a handle open of a deleted file 49 | .fuse_hidden* 50 | 51 | # KDE directory preferences 52 | .directory 53 | 54 | # Linux trash folder which might appear on any partition or disk 55 | .Trash-* 56 | 57 | # .nfs files are created when an open file is removed but is still being accessed 58 | .nfs* 59 | 60 | ### OSX ### 61 | *.DS_Store 62 | .AppleDouble 63 | .LSOverride 64 | 65 | # Icon must end with two \r 66 | Icon 67 | 68 | 69 | # Thumbnails 70 | ._* 71 | 72 | # Files that might appear in the root of a volume 73 | .DocumentRevisions-V100 74 | .fseventsd 75 | .Spotlight-V100 76 | .TemporaryItems 77 | .Trashes 78 | .VolumeIcon.icns 79 | .com.apple.timemachine.donotpresent 80 | 81 | # Directories potentially created on remote AFP share 82 | .AppleDB 83 | .AppleDesktop 84 | Network Trash Folder 85 | Temporary Items 86 | .apdisk 87 | 88 | ### Windows ### 89 | # Windows thumbnail cache files 90 | Thumbs.db 91 | ehthumbs.db 92 | ehthumbs_vista.db 93 | 94 | # Folder config file 95 | Desktop.ini 96 | 97 | # Recycle Bin used on file shares 98 | $RECYCLE.BIN/ 99 | 100 | # Windows Installer files 101 | *.cab 102 | *.msi 103 | *.msm 104 | *.msp 105 | 106 | # Windows shortcuts 107 | *.lnk 108 | 109 | # Others 110 | lib/ 111 | data/ 112 | -------------------------------------------------------------------------------- /examples/js/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "public": "../public/", 5 | "paginate": { 6 | "default": 10, 7 | "max": 50 8 | }, 9 | "authentication": { 10 | "entity": "user", 11 | "service": "users", 12 | "secret": "v66dnIEkEpMTRXgn5N4t/uN/VvI=", 13 | "authStrategies": [ 14 | "jwt", 15 | "local" 16 | ], 17 | "jwtOptions": { 18 | "header": { 19 | "typ": "access" 20 | }, 21 | "audience": "https://yourdomain.com", 22 | "issuer": "feathers", 23 | "algorithm": "HS256", 24 | "expiresIn": "1d" 25 | }, 26 | "local": { 27 | "usernameField": "email", 28 | "passwordField": "password" 29 | } 30 | }, 31 | "postgres": "postgres://postgres:1@localhost:5432/js" 32 | } 33 | -------------------------------------------------------------------------------- /examples/js/config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "js-app.feathersjs.com", 3 | "port": "PORT" 4 | } 5 | -------------------------------------------------------------------------------- /examples/js/config/test.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "description": "", 4 | "version": "0.0.0", 5 | "homepage": "", 6 | "private": true, 7 | "main": "src", 8 | "keywords": [ 9 | "feathers" 10 | ], 11 | "author": { 12 | "name": "Fratzinger", 13 | "email": "22286818+fratzinger@users.noreply.github.com" 14 | }, 15 | "contributors": [], 16 | "bugs": {}, 17 | "directories": { 18 | "lib": "src", 19 | "test": "test/", 20 | "config": "config/" 21 | }, 22 | "engines": { 23 | "node": "^12.0.0", 24 | "npm": ">= 3.0.0" 25 | }, 26 | "scripts": { 27 | "test": "npm run lint && npm run mocha", 28 | "lint": "eslint src/. test/. --config .eslintrc.json --fix", 29 | "dev": "nodemon src/", 30 | "start": "node src/", 31 | "mocha": "mocha test/ --recursive --exit" 32 | }, 33 | "standard": { 34 | "env": [ 35 | "mocha" 36 | ], 37 | "ignore": [] 38 | }, 39 | "dependencies": { 40 | "@feathersjs/authentication": "^5.0.0-pre.31", 41 | "@feathersjs/authentication-local": "^5.0.0-pre.31", 42 | "@feathersjs/authentication-oauth": "^5.0.0-pre.31", 43 | "@feathersjs/configuration": "^5.0.0-pre.31", 44 | "@feathersjs/errors": "^5.0.0-pre.31", 45 | "@feathersjs/express": "^5.0.0-pre.31", 46 | "@feathersjs/feathers": "^5.0.0-pre.31", 47 | "@feathersjs/socketio": "^5.0.0-pre.31", 48 | "@feathersjs/transport-commons": "^5.0.0-pre.31", 49 | "compression": "^1.7.4", 50 | "cors": "^2.8.5", 51 | "feathers-authentication-management": "^4.0.0", 52 | "feathers-hooks-common": "^7.0.0-pre.1", 53 | "feathers-mailer": "^3.1.0", 54 | "feathers-sequelize": "^6.3.4", 55 | "helmet": "^6.0.0", 56 | "lodash": "^4.17.21", 57 | "nodemailer": "^6.7.8", 58 | "pg": "^8.8.0", 59 | "sequelize": "^6.23.0", 60 | "serve-favicon": "^2.5.0", 61 | "winston": "^3.8.2" 62 | }, 63 | "devDependencies": { 64 | "axios": "^0.27.2", 65 | "eslint": "^8.23.1", 66 | "mocha": "^10.0.0", 67 | "nodemon": "^2.0.20" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/js/public/authmgmt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Sending token 5 |

6 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/js/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-authentication-management/5024c2d0dcc1fd95388ec13bed99d235ca74df74/examples/js/public/favicon.ico -------------------------------------------------------------------------------- /examples/js/src/app.hooks.js: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | 3 | module.exports = { 4 | before: { 5 | all: [], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [] 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [] 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /examples/js/src/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const favicon = require('serve-favicon'); 3 | const compress = require('compression'); 4 | const helmet = require('helmet'); 5 | const cors = require('cors'); 6 | const logger = require('./logger'); 7 | 8 | const feathers = require('@feathersjs/feathers'); 9 | const configuration = require('@feathersjs/configuration'); 10 | const express = require('@feathersjs/express'); 11 | const socketio = require('@feathersjs/socketio'); 12 | 13 | 14 | const middleware = require('./middleware'); 15 | const services = require('./services'); 16 | const appHooks = require('./app.hooks'); 17 | const channels = require('./channels'); 18 | 19 | const authentication = require('./authentication'); 20 | 21 | const sequelize = require('./sequelize'); 22 | 23 | const app = express(feathers()); 24 | 25 | // Load app configuration 26 | app.configure(configuration()); 27 | // Enable security, CORS, compression, favicon and body parsing 28 | app.use(helmet({ 29 | contentSecurityPolicy: false 30 | })); 31 | app.use(cors()); 32 | app.use(compress()); 33 | app.use(express.json()); 34 | app.use(express.urlencoded({ extended: true })); 35 | app.use(favicon(path.join(app.get('public'), 'favicon.ico'))); 36 | // Host the public folder 37 | app.use('/', express.static(app.get('public'))); 38 | 39 | // Set up Plugins and providers 40 | app.configure(express.rest()); 41 | app.configure(socketio()); 42 | 43 | app.configure(sequelize); 44 | 45 | // Configure other middleware (see `middleware/index.js`) 46 | app.configure(middleware); 47 | app.configure(authentication); 48 | // Set up our services (see `services/index.js`) 49 | app.configure(services); 50 | // Set up event channels (see channels.js) 51 | app.configure(channels); 52 | 53 | // Configure a middleware for 404s and the error handler 54 | app.use(express.notFound()); 55 | app.use(express.errorHandler({ logger })); 56 | 57 | app.hooks(appHooks); 58 | 59 | module.exports = app; 60 | -------------------------------------------------------------------------------- /examples/js/src/authentication.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication'); 2 | const { LocalStrategy } = require('@feathersjs/authentication-local'); 3 | const { expressOauth } = require('@feathersjs/authentication-oauth'); 4 | 5 | module.exports = app => { 6 | const authentication = new AuthenticationService(app); 7 | 8 | authentication.register('jwt', new JWTStrategy()); 9 | authentication.register('local', new LocalStrategy()); 10 | 11 | app.use('/authentication', authentication); 12 | app.configure(expressOauth()); 13 | }; 14 | -------------------------------------------------------------------------------- /examples/js/src/channels.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | if(typeof app.channel !== 'function') { 3 | // If no real-time functionality has been configured just return 4 | return; 5 | } 6 | 7 | app.on('connection', connection => { 8 | // On a new real-time connection, add it to the anonymous channel 9 | app.channel('anonymous').join(connection); 10 | }); 11 | 12 | app.on('login', (authResult, { connection }) => { 13 | // connection can be undefined if there is no 14 | // real-time connection, e.g. when logging in via REST 15 | if(connection) { 16 | // Obtain the logged in user from the connection 17 | // const user = connection.user; 18 | 19 | // The connection is no longer anonymous, remove it 20 | app.channel('anonymous').leave(connection); 21 | 22 | // Add it to the authenticated user channel 23 | app.channel('authenticated').join(connection); 24 | 25 | // Channels can be named anything and joined on any condition 26 | 27 | // E.g. to send real-time events only to admins use 28 | // if(user.isAdmin) { app.channel('admins').join(connection); } 29 | 30 | // If the user has joined e.g. chat rooms 31 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection)); 32 | 33 | // Easily organize users by email and userid for things like messaging 34 | // app.channel(`emails/${user.email}`).join(connection); 35 | // app.channel(`userIds/${user.id}`).join(connection); 36 | } 37 | }); 38 | 39 | // eslint-disable-next-line no-unused-vars 40 | app.publish((data, hook) => { 41 | // Here you can add event publishers to channels set up in `channels.js` 42 | // To publish only for a specific event use `app.publish(eventname, () => {})` 43 | 44 | console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line 45 | 46 | // e.g. to publish all service events to all authenticated users use 47 | return app.channel('authenticated'); 48 | }); 49 | 50 | // Here you can also add service specific event publishers 51 | // e.g. the publish the `users` service `created` event to the `admins` channel 52 | // app.service('users').publish('created', () => app.channel('admins')); 53 | 54 | // With the userid and email organization from above you can easily select involved users 55 | // app.service('messages').publish(() => { 56 | // return [ 57 | // app.channel(`userIds/${data.createdBy}`), 58 | // app.channel(`emails/${data.recipientEmail}`) 59 | // ]; 60 | // }); 61 | }; 62 | -------------------------------------------------------------------------------- /examples/js/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const logger = require('./logger'); 3 | const app = require('./app'); 4 | const port = app.get('port'); 5 | const server = app.listen(port); 6 | 7 | process.on('unhandledRejection', (reason, p) => 8 | logger.error('Unhandled Rejection at: Promise ', p, reason) 9 | ); 10 | 11 | server.on('listening', () => 12 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 13 | ); 14 | -------------------------------------------------------------------------------- /examples/js/src/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | module.exports = logger; 17 | -------------------------------------------------------------------------------- /examples/js/src/middleware/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | module.exports = function (app) { 3 | // Add your custom middleware here. Remember that 4 | // in Express, the order matters. 5 | }; 6 | -------------------------------------------------------------------------------- /examples/js/src/sequelize.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | module.exports = function (app) { 4 | const connectionString = app.get('postgres'); 5 | const sequelize = new Sequelize(connectionString, { 6 | dialect: 'postgres', 7 | logging: false, 8 | define: { 9 | freezeTableName: true 10 | } 11 | }); 12 | const oldSetup = app.setup; 13 | 14 | app.set('sequelizeClient', sequelize); 15 | 16 | app.setup = function (...args) { 17 | const result = oldSetup.apply(this, args); 18 | 19 | // Set up data relationships 20 | const models = sequelize.models; 21 | Object.keys(models).forEach(name => { 22 | if ('associate' in models[name]) { 23 | models[name].associate(models); 24 | } 25 | }); 26 | 27 | // Sync to the database 28 | app.set('sequelizeSync', sequelize.sync()); 29 | 30 | return result; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /examples/js/src/services/auth-management/auth-management.class.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationManagementService } = require('../../../../../dist'); 2 | // const { AuthenticationManagementService } = require('feathers-authentication-management'); 3 | 4 | exports.AuthManagement = class AuthManagement extends AuthenticationManagementService { 5 | 6 | }; 7 | -------------------------------------------------------------------------------- /examples/js/src/services/auth-management/auth-management.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | const { iff } = require('feathers-hooks-common'); 3 | 4 | const isAction = (...args) => (hook) => args.includes(hook.data.action); 5 | 6 | module.exports = { 7 | before: { 8 | all: [], 9 | find: [], 10 | get: [], 11 | create: [ 12 | // The user must be signed in before being allowed to change their password or communication values. 13 | iff( 14 | isAction('passwordChange', 'identityChange'), 15 | authenticate('jwt') 16 | ) 17 | ], 18 | update: [], 19 | patch: [], 20 | remove: [] 21 | }, 22 | 23 | after: { 24 | all: [], 25 | find: [], 26 | get: [], 27 | create: [], 28 | update: [], 29 | patch: [], 30 | remove: [] 31 | }, 32 | 33 | error: { 34 | all: [], 35 | find: [], 36 | get: [], 37 | create: [], 38 | update: [], 39 | patch: [], 40 | remove: [] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /examples/js/src/services/auth-management/auth-management.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `auth-management` service on path `/auth-management` 2 | const { AuthManagement } = require('./auth-management.class'); 3 | const hooks = require('./auth-management.hooks'); 4 | 5 | const { 6 | makeNotifier, 7 | sanitizeUserForClient 8 | } = require('./auth-management.utils'); 9 | 10 | module.exports = function (app) { 11 | const notifier = makeNotifier(app); 12 | 13 | // these are the available options for feathers-authentication-management 14 | const options = { 15 | app, // <- this one is required. The following are optional 16 | service: '/users', 17 | notifier, 18 | longTokenLen: 15, // token's length will be twice this 19 | shortTokenLen: 6, 20 | shortTokenDigits: true, 21 | resetDelay: 1000 * 60 * 60 * 2, // 2 hours 22 | delay: 1000 * 60 * 60 * 24 * 5, // 5 days 23 | resetAttempts: 0, 24 | reuseResetToken: false, 25 | identifyUserProps: ['email'], 26 | sanitizeUserForClient, 27 | skipIsVerifiedCheck: false, 28 | passwordField: 'password' 29 | }; 30 | 31 | // Initialize our service with any options it requires 32 | app.use('/auth-management', new AuthManagement(options, app)); 33 | 34 | // Get our initialized service so that we can register hooks 35 | const service = app.service('auth-management'); 36 | 37 | service.hooks(hooks); 38 | }; 39 | -------------------------------------------------------------------------------- /examples/js/src/services/auth-management/auth-management.utils.js: -------------------------------------------------------------------------------- 1 | const _cloneDeep = require('lodash/cloneDeep'); 2 | 3 | function sanitizeUserForClient (user) { 4 | user = _cloneDeep(user); 5 | 6 | delete user.password; 7 | delete user.verifyExpires; 8 | delete user.verifyToken; 9 | delete user.verifyShortToken; 10 | delete user.verifyChanges; 11 | delete user.resetExpires; 12 | delete user.resetToken; 13 | delete user.resetShortToken; 14 | 15 | return user; 16 | } 17 | 18 | exports.sanitizeUserForClient = sanitizeUserForClient; 19 | 20 | function makeNotifier(app) { 21 | function getLink(action, hash) { 22 | const url = new URL('http://localhost:3030/'); 23 | url.pathname = '/authmgmt.html'; 24 | url.searchParams.append('action', action); 25 | url.searchParams.append('token', hash); 26 | return url.toString(); 27 | } 28 | 29 | async function sendEmail(email) { 30 | return await app.service('mailer').create(email).then(function (result) { 31 | console.log('Sent email', result); 32 | }).catch(err => { 33 | console.log('Error sending email', err); 34 | }); 35 | } 36 | 37 | // eslint-disable-next-line no-unused-vars 38 | return async function(type, user, notifierOptions) { 39 | if (type === 'resendVerifySignup') { 40 | const tokenLink = getLink('verify', user.verifyToken); 41 | return await sendEmail({ 42 | to: user.email, 43 | subject: 'Verify Signup', 44 | html: tokenLink 45 | }); 46 | } else if (type === 'verifySignup') { 47 | const tokenLink = getLink('verify', user.verifyToken); 48 | return await sendEmail({ 49 | to: user.email, 50 | subject: 'Confirm Signup', 51 | html: tokenLink 52 | }); 53 | } else if (type === 'sendResetPwd') { 54 | const tokenLink = getLink('reset', user.resetToken); 55 | return await sendEmail({ 56 | to: user.email, 57 | subject: 'Send Reset Password', 58 | html: tokenLink 59 | }); 60 | } else if (type === 'resetPwd') { 61 | const tokenLink = getLink('reset', user.resetToken); 62 | return await sendEmail({ 63 | to: user.email, 64 | subject: 'Reset Password', 65 | html: tokenLink 66 | }); 67 | } else if (type === 'passwordChange') { 68 | return await sendEmail({}); 69 | } else if (type === 'identityChange') { 70 | const tokenLink = getLink('verifyChanges', user.verifyToken); 71 | return await sendEmail({ 72 | to: user.email, 73 | subject: 'Change your identity', 74 | html: tokenLink 75 | }); 76 | } 77 | }; 78 | } 79 | 80 | exports.makeNotifier = makeNotifier; 81 | -------------------------------------------------------------------------------- /examples/js/src/services/index.js: -------------------------------------------------------------------------------- 1 | const users = require('./users/users.service.js'); 2 | const mailer = require('./mailer/mailer.service.js'); 3 | const authManagement = require('./auth-management/auth-management.service.js'); 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | module.exports = function (app) { 7 | app.configure(users); 8 | app.configure(mailer); 9 | app.configure(authManagement); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/js/src/services/mailer/mailer.class.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-mailer'); 2 | 3 | class Mailer extends Service { 4 | constructor(transport, defaults) { 5 | super(transport, defaults); 6 | } 7 | } 8 | 9 | exports.Mailer = Mailer; 10 | -------------------------------------------------------------------------------- /examples/js/src/services/mailer/mailer.hooks.js: -------------------------------------------------------------------------------- 1 | const { disallow } = require('feathers-hooks-common'); 2 | 3 | module.exports = { 4 | before: { 5 | all: [ 6 | disallow('external') 7 | ], 8 | find: [], 9 | get: [], 10 | create: [], 11 | update: [], 12 | patch: [], 13 | remove: [] 14 | }, 15 | 16 | after: { 17 | all: [], 18 | find: [], 19 | get: [], 20 | create: [], 21 | update: [], 22 | patch: [], 23 | remove: [] 24 | }, 25 | 26 | error: { 27 | all: [], 28 | find: [], 29 | get: [], 30 | create: [], 31 | update: [], 32 | patch: [], 33 | remove: [] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /examples/js/src/services/mailer/mailer.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `mailer` service on path `/mailer` 2 | const { Mailer } = require('./mailer.class'); 3 | const hooks = require('./mailer.hooks'); 4 | const nodemailer = require('nodemailer'); 5 | // see https://github.com/feathersjs-ecosystem/feathers-mailer 6 | 7 | module.exports = async function (app) { 8 | const account = await nodemailer.createTestAccount(); // internet required 9 | 10 | const transporter = { 11 | host: account.smtp.host, 12 | port: account.smtp.port, 13 | secure: account.smtp.secure, // 487 only 14 | requireTLS: true, 15 | auth: { 16 | user: account.user, // generated ethereal user 17 | pass: account.pass // generated ethereal password 18 | } 19 | }; 20 | 21 | 22 | // Initialize our service with any options it requires 23 | app.use('/mailer', new Mailer(transporter, { from: account.user })); 24 | 25 | // Get our initialized service so that we can register hooks 26 | const service = app.service('mailer'); 27 | 28 | service.hooks(hooks); 29 | 30 | service.hooks({ after: { create: (context) => { 31 | console.log(nodemailer.getTestMessageUrl(context.result)); 32 | }}}); 33 | }; 34 | -------------------------------------------------------------------------------- /examples/js/src/services/users/users.class.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-sequelize'); 2 | //const { Service } = require('feathers-mongoose'); 3 | 4 | exports.Users = class Users extends Service { 5 | 6 | }; 7 | -------------------------------------------------------------------------------- /examples/js/src/services/users/users.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | const { hashPassword, protect } = require('@feathersjs/authentication-local').hooks; 3 | const { addVerification, removeVerification } = require('feathers-authentication-management'); 4 | 5 | const { 6 | checkContext, 7 | disallow, 8 | iff, 9 | isProvider, 10 | preventChanges, 11 | getItems 12 | } = require('feathers-hooks-common'); 13 | 14 | const { makeNotifier } = require('../auth-management/auth-management.utils'); 15 | 16 | const sendVerifySignup = () => { 17 | return async context => { 18 | checkContext(context, 'after', 'create'); 19 | let users = getItems(context); 20 | users = (Array.isArray(users)) ? users : [users]; 21 | const notify = makeNotifier(context.app); 22 | const promises = users.map(user => notify( 23 | 'resendVerifySignup', 24 | user 25 | )); 26 | await Promise.all(promises); 27 | }; 28 | }; 29 | 30 | module.exports = { 31 | before: { 32 | all: [], 33 | find: [ authenticate('jwt') ], 34 | get: [ authenticate('jwt') ], 35 | create: [ 36 | hashPassword('password'), 37 | addVerification(), // adds .isVerified, .verifyExpires, .verifyToken, .verifyChanges 38 | ], 39 | update: [ 40 | disallow('external'), 41 | authenticate('jwt'), 42 | hashPassword('password') 43 | ], 44 | patch: [ 45 | authenticate('jwt'), 46 | iff( 47 | isProvider('external'), 48 | preventChanges( 49 | true, 50 | 'email', 51 | 'isVerified', 52 | 'verifyToken', 53 | 'verifyShortToken', 54 | 'verifyExpires', 55 | 'verifyChanges', 56 | 'resetToken', 57 | 'resetShortToken', 58 | 'resetExpires' 59 | ), 60 | hashPassword('password'), 61 | ) 62 | ], 63 | remove: [ 64 | authenticate('jwt'), 65 | hashPassword('password') 66 | ] 67 | }, 68 | after: { 69 | all: [ 70 | protect('password'), 71 | removeVerification() // removes verification/reset fields other than .isVerified from the response 72 | ], 73 | find: [], 74 | get: [], 75 | create: [ 76 | sendVerifySignup() 77 | ], 78 | update: [], 79 | patch: [], 80 | remove: [] 81 | }, 82 | error: { 83 | all: [], 84 | find: [], 85 | get: [], 86 | create: [], 87 | update: [], 88 | patch: [], 89 | remove: [] 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /examples/js/src/services/users/users.model.js: -------------------------------------------------------------------------------- 1 | // See http://docs.sequelizejs.com/en/latest/docs/models-definition/ 2 | // for more of what you can do here. 3 | const Sequelize = require('sequelize'); 4 | const DataTypes = Sequelize.DataTypes; 5 | 6 | module.exports = function (app) { 7 | const sequelizeClient = app.get('sequelizeClient'); 8 | const users = sequelizeClient.define('users', { 9 | email: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | unique: true 13 | }, 14 | password: { 15 | type: DataTypes.STRING, 16 | allowNull: false 17 | }, 18 | isVerified: { 19 | type: DataTypes.BOOLEAN 20 | }, 21 | verifyToken: { 22 | type: DataTypes.STRING 23 | }, 24 | verifyShortToken: { 25 | type: DataTypes.STRING 26 | }, 27 | verifyExpires: { 28 | type: DataTypes.DATE 29 | }, 30 | verifyChanges: { 31 | type: DataTypes.ARRAY(DataTypes.STRING) 32 | }, 33 | resetToken: { 34 | type: DataTypes.STRING 35 | }, 36 | resetShortToken: { 37 | type: DataTypes.STRING 38 | }, 39 | resetExpires: { 40 | type: DataTypes.DATE 41 | }, 42 | resetAttempts: { 43 | type: DataTypes.NUMBER 44 | } 45 | }, { 46 | hooks: { 47 | beforeCount(options) { 48 | options.raw = true; 49 | } 50 | } 51 | }); 52 | 53 | // eslint-disable-next-line no-unused-vars 54 | users.associate = function (models) { 55 | // Define associations here 56 | // See http://docs.sequelizejs.com/en/latest/docs/associations/ 57 | }; 58 | 59 | return users; 60 | }; 61 | -------------------------------------------------------------------------------- /examples/js/src/services/users/users.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `users` service on path `/users` 2 | const { Users } = require('./users.class'); 3 | const createModel = require('./users.model'); 4 | const hooks = require('./users.hooks'); 5 | 6 | module.exports = function (app) { 7 | const options = { 8 | Model: createModel(app), 9 | paginate: app.get('paginate') 10 | }; 11 | 12 | // Initialize our service with any options it requires 13 | app.use('/users', new Users(options, app)); 14 | 15 | // Get our initialized service so that we can register hooks 16 | const service = app.service('users'); 17 | 18 | service.hooks(hooks); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/js/test/app.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const axios = require('axios'); 3 | const url = require('url'); 4 | const app = require('../src/app'); 5 | 6 | const port = app.get('port') || 8998; 7 | const getUrl = pathname => url.format({ 8 | hostname: app.get('host') || 'localhost', 9 | protocol: 'http', 10 | port, 11 | pathname 12 | }); 13 | 14 | describe('Feathers application tests', () => { 15 | let server; 16 | 17 | before(function(done) { 18 | server = app.listen(port); 19 | server.once('listening', () => done()); 20 | }); 21 | 22 | after(function(done) { 23 | server.close(done); 24 | }); 25 | 26 | it('starts and shows the index page', async () => { 27 | const { data } = await axios.get(getUrl()); 28 | 29 | assert.ok(data.indexOf('') !== -1); 30 | }); 31 | 32 | describe('404', function() { 33 | it('shows a 404 HTML page', async () => { 34 | try { 35 | await axios.get(getUrl('path/to/nowhere'), { 36 | headers: { 37 | 'Accept': 'text/html' 38 | } 39 | }); 40 | assert.fail('should never get here'); 41 | } catch (error) { 42 | const { response } = error; 43 | 44 | assert.strictEqual(response.status, 404); 45 | assert.ok(response.data.indexOf('') !== -1); 46 | } 47 | }); 48 | 49 | it('shows a 404 JSON error without stack trace', async () => { 50 | try { 51 | await axios.get(getUrl('path/to/nowhere'), { 52 | json: true 53 | }); 54 | assert.fail('should never get here'); 55 | } catch (error) { 56 | const { response } = error; 57 | 58 | assert.strictEqual(response.status, 404); 59 | assert.strictEqual(response.data.code, 404); 60 | assert.strictEqual(response.data.message, 'Page not found'); 61 | assert.strictEqual(response.data.name, 'NotFound'); 62 | } 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /examples/js/test/authentication.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../src/app'); 3 | 4 | describe('authentication', () => { 5 | it('registered the authentication service', () => { 6 | assert.ok(app.service('authentication')); 7 | }); 8 | 9 | describe('local strategy', () => { 10 | const userInfo = { 11 | email: 'someone@example.com', 12 | password: 'supersecret' 13 | }; 14 | 15 | before(async () => { 16 | try { 17 | await app.service('users').create(userInfo); 18 | } catch (error) { 19 | // Do nothing, it just means the user already exists and can be tested 20 | } 21 | }); 22 | 23 | it('authenticates user and creates accessToken', async () => { 24 | const { user, accessToken } = await app.service('authentication').create({ 25 | strategy: 'local', 26 | ...userInfo 27 | }); 28 | 29 | assert.ok(accessToken, 'Created access token for user'); 30 | assert.ok(user, 'Includes user in authentication data'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/js/test/services/auth-management.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'auth-management\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('auth-management'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/js/test/services/mailer.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'mailer\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('mailer'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/js/test/services/users.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'users\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('users'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/vue/ChangePassword.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 55 | -------------------------------------------------------------------------------- /examples/vue/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | -------------------------------------------------------------------------------- /examples/vue/Login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 37 | -------------------------------------------------------------------------------- /examples/vue/Readme.md: -------------------------------------------------------------------------------- 1 | # Vue.js example implementation of feathers-authentication-management 2 | 3 | This example assumes you have the knowledge of the following: 4 | - vue.js (especially composition api) 5 | - vue-router 6 | - feathers.js 7 | - feathes-vuex (or feathers-pinia) 8 | 9 | The example does not use any fancy styling. It's just the bare minimum needed for `feathers-authentication-management`. 10 | -------------------------------------------------------------------------------- /examples/vue/Signup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 42 | -------------------------------------------------------------------------------- /examples/vue/VerifyMail.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 48 | -------------------------------------------------------------------------------- /examples/vue/router.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: "/login", 4 | name: "Login", 5 | component: () => import("./Login.vue") 6 | }, 7 | { 8 | path: "/singup", 9 | name: "Signup", 10 | component: () => import("./Signup.vue"); 11 | }, 12 | { 13 | path: "/forgot-password", 14 | name: "ForgotPassword", 15 | component: () => import("./ForgotPassword.vue") 16 | }, 17 | { 18 | path: "/change-password", 19 | name: "ChangePassword", 20 | component: () => import ("./ChangePassword.vue") 21 | }, 22 | { 23 | path: "/verify-email/:token", 24 | name: "VerifyMail", 25 | component: () => import("./VerifyMail.vue"), 26 | props: true 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-authentication-management", 3 | "description": "Adds sign up verification, forgotten password reset, and other capabilities to local feathers-authentication", 4 | "version": "5.1.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/feathersjs-ecosystem/feathers-authentication-management.git" 8 | }, 9 | "license": "MIT", 10 | "bugs": { 11 | "url": "https://github.com/feathersjs-ecosystem/feathers-authentication-management/issues" 12 | }, 13 | "homepage": "https://feathers-a-m.netlify.app/", 14 | "keywords": [ 15 | "feathers", 16 | "feathers-plugin", 17 | "hook", 18 | "hooks", 19 | "services", 20 | "authentication", 21 | "verification" 22 | ], 23 | "author": { 24 | "name": "Feathers contributors", 25 | "email": "hello@feathersjs.com", 26 | "url": "https://feathersjs.com" 27 | }, 28 | "engines": { 29 | "node": ">= 14" 30 | }, 31 | "main": "dist/", 32 | "types": "dist/", 33 | "directories": { 34 | "src": "src" 35 | }, 36 | "files": [ 37 | "CHANGELOG.md", 38 | "LICENSE", 39 | "README.md", 40 | "src/**", 41 | "lib/**", 42 | "dist/**" 43 | ], 44 | "scripts": { 45 | "preversion": "npm run lint && npm run test && npm run compile", 46 | "publish": "git push origin --tags && npm run changelog && git push origin", 47 | "release:pre": "npm version prerelease --preid=pre && npm publish --tag pre", 48 | "release:patch": "npm version patch && npm publish", 49 | "release:minor": "npm version minor && npm publish", 50 | "release:major": "npm version major && npm publish", 51 | "changelog": "github_changelog_generator -u feathersjs-ecosystem -p feathers-authentication-management && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 52 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 53 | "mocha": "cross-env TS_NODE_PROJECT='tsconfig.test.json' mocha --require ts-node/register --timeout 40000", 54 | "coverage": "nyc npm run mocha", 55 | "test": "npm run mocha", 56 | "compile": "shx rm -rf dist/ && tsc", 57 | "docs": "vitepress dev docs --port 3333 --open", 58 | "docs:build": "vitepress build docs", 59 | "check-updates": "ncu -u && ncu -u --cwd examples/js" 60 | }, 61 | "peerDependencies": { 62 | "@feathersjs/feathers": "^5.0.0" 63 | }, 64 | "dependencies": { 65 | "@feathersjs/authentication": "^5.0.11", 66 | "@feathersjs/authentication-client": "^5.0.11", 67 | "@feathersjs/authentication-local": "^5.0.11", 68 | "@feathersjs/errors": "^5.0.11", 69 | "bcryptjs": "^2.4.3", 70 | "debug": "^4.3.4", 71 | "feathers-hooks-common": "^8.1.1", 72 | "lodash": "^4.17.21", 73 | "type-fest": "^4.6.0" 74 | }, 75 | "devDependencies": { 76 | "@feathersjs/feathers": "^5.0.11", 77 | "@feathersjs/memory": "^5.0.11", 78 | "@feathersjs/socketio": "^5.0.11", 79 | "@feathersjs/transport-commons": "^5.0.11", 80 | "@types/bcryptjs": "^2.4.5", 81 | "@types/debug": "^4.1.10", 82 | "@types/lodash": "^4.14.200", 83 | "@types/mocha": "^10.0.3", 84 | "@types/node": "^20.8.10", 85 | "@typescript-eslint/eslint-plugin": "^6.9.1", 86 | "@typescript-eslint/parser": "^6.9.1", 87 | "@unocss/preset-uno": "^0.57.2", 88 | "@vueuse/core": "^10.5.0", 89 | "cross-env": "^7.0.3", 90 | "date-fns": "^2.30.0", 91 | "eslint": "^8.53.0", 92 | "eslint-import-resolver-typescript": "^3.6.1", 93 | "eslint-plugin-import": "^2.29.0", 94 | "mocha": "^10.2.0", 95 | "nyc": "^15.1.0", 96 | "shx": "^0.3.4", 97 | "ts-node": "^10.9.1", 98 | "typescript": "^5.2.2", 99 | "unocss": "^0.57.2", 100 | "unplugin-vue-components": "^0.25.2", 101 | "vitepress": "^1.0.0-rc.24" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | // Wrapper for client interface to feathers-authenticate-management 2 | 3 | import { defaultPath } from './options'; 4 | import type { AuthenticationClient } from '@feathersjs/authentication-client'; 5 | import type { Application, NullableId } from '@feathersjs/feathers'; 6 | 7 | import type { 8 | AuthenticationManagementClient, 9 | IdentifyUser, 10 | User, 11 | NotifierOptions, 12 | ClientOptions 13 | } from './types'; 14 | 15 | declare module '@feathersjs/feathers' { 16 | interface Application { 17 | authenticate: AuthenticationClient['authenticate'] 18 | logout: AuthenticationClient['logout'] 19 | } 20 | } 21 | 22 | const defaultOptions: ClientOptions = { 23 | path: defaultPath 24 | }; 25 | 26 | function makeClient (app: Application, _options?: Partial): AuthenticationManagementClient { 27 | const options: ClientOptions = Object.assign({}, defaultOptions, _options); 28 | 29 | const { 30 | path 31 | } = options; 32 | 33 | const authManagement = app.service(path); 34 | 35 | const client: AuthenticationManagementClient = { 36 | checkUnique: async (identifyUser: IdentifyUser, ownId: NullableId, ifErrMsg?: boolean) => { 37 | await authManagement.create({ 38 | action: 'checkUnique', 39 | value: identifyUser, 40 | ownId, 41 | meta: { noErrMsg: ifErrMsg } 42 | }); 43 | }, 44 | resendVerifySignup: async (identifyUser: IdentifyUser, notifierOptions?: NotifierOptions) => { 45 | await authManagement.create({ 46 | action: 'resendVerifySignup', 47 | value: identifyUser, 48 | notifierOptions 49 | }); 50 | }, 51 | verifySignupLong: async (verifyToken: string) => { 52 | await authManagement.create({ 53 | action: 'verifySignupLong', 54 | value: verifyToken 55 | }); 56 | }, 57 | 58 | verifySignupShort: async (verifyShortToken: string, identifyUser: IdentifyUser) => { 59 | await authManagement.create({ 60 | action: 'verifySignupShort', 61 | value: { 62 | token: verifyShortToken, 63 | user: identifyUser 64 | } 65 | }); 66 | }, 67 | 68 | sendResetPwd: async (identifyUser: IdentifyUser, notifierOptions?: NotifierOptions) => { 69 | await authManagement.create({ 70 | action: 'sendResetPwd', 71 | value: identifyUser, 72 | notifierOptions 73 | }); 74 | }, 75 | 76 | resetPwdLong: async (resetToken: string, password: string) => { 77 | await authManagement.create({ 78 | action: 'resetPwdLong', 79 | value: { 80 | password, 81 | token: resetToken 82 | } 83 | }); 84 | }, 85 | 86 | resetPwdShort: async (resetShortToken: string, identifyUser: IdentifyUser, password: string) => { 87 | await authManagement.create({ 88 | action: 'resetPwdShort', 89 | value: { 90 | password, 91 | token: resetShortToken, 92 | user: identifyUser 93 | } 94 | }); 95 | }, 96 | 97 | passwordChange: async (oldPassword: string, password: string, identifyUser: IdentifyUser) => { 98 | await authManagement.create({ 99 | action: 'passwordChange', 100 | value: { 101 | oldPassword, 102 | password, 103 | user: identifyUser 104 | } 105 | }); 106 | }, 107 | 108 | identityChange: async (password: string, changesIdentifyUser: Record, identifyUser: IdentifyUser) => { 109 | await authManagement.create({ 110 | action: 'identityChange', 111 | value: { 112 | user: identifyUser, 113 | password, 114 | changes: changesIdentifyUser 115 | } 116 | }); 117 | }, 118 | 119 | authenticate: async ( 120 | email: string, 121 | password: string, 122 | cb?: (err: Error | null, user?: Partial) => void 123 | ): Promise => { 124 | let cbCalled = false; 125 | 126 | const authResult = await app.authenticate({ type: 'local', email, password }); 127 | const user = authResult.data; 128 | 129 | try { 130 | if (!user || !user.isVerified) { 131 | await app.logout(); 132 | return cb(new Error(user ? 'User\'s email is not verified.' : 'No user returned.')); 133 | } 134 | 135 | if (cb) { 136 | cbCalled = true; 137 | return cb(null, user); 138 | } 139 | 140 | return user; 141 | } catch (err) { 142 | if (!cbCalled && cb) { 143 | cb(err); 144 | } 145 | } 146 | } 147 | }; 148 | 149 | return client; 150 | } 151 | 152 | export default makeClient; 153 | 154 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 155 | module.exports = makeClient; 156 | } 157 | -------------------------------------------------------------------------------- /src/helpers/clone-object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns new object with values cloned from the original object. Some objects 3 | * (like Sequelize or MongoDB model instances) contain circular references 4 | * and cause TypeError when trying to JSON.stringify() them. They may contain 5 | * custom toJSON() or toObject() method which allows to serialize them safely. 6 | * Object.assign() does not clone these methods, so the purpose of this method 7 | * is to use result of custom toJSON() or toObject() (if accessible) 8 | * for Object.assign(), but only in case of serialization failure. 9 | * 10 | * @param obj - Object to clone 11 | * @returns Cloned object 12 | */ 13 | export function cloneObject> ( 14 | obj: T 15 | ): T { 16 | if (typeof obj.toJSON === 'function' || typeof obj.toObject === 'function') { 17 | try { 18 | JSON.stringify(Object.assign({}, obj)); 19 | } catch (err) { 20 | return (typeof obj.toJSON === 'function') 21 | ? obj.toJSON() 22 | // @ts-expect-error does not know about toObject() 23 | : obj.toObject(); 24 | } 25 | } 26 | 27 | return Object.assign({}, obj); 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers/compare-passwords.ts: -------------------------------------------------------------------------------- 1 | import { compare } from 'bcryptjs'; 2 | 3 | export async function comparePasswords ( 4 | oldPassword: string, 5 | password: string, 6 | getError?: () => unknown 7 | ): Promise { 8 | return await new Promise((resolve, reject) => { 9 | compare(oldPassword, password, (err, data) => 10 | (err || !data) ? reject(getError?.() ?? err) : resolve() 11 | ); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/crypto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | randomBytes as _randomBytes 3 | } from 'crypto'; 4 | 5 | export function randomDigits (len: number): string { 6 | let str = ''; 7 | 8 | while (str.length < len) { 9 | str += parseInt('0x' + _randomBytes(4).toString('hex')).toString(); 10 | } 11 | 12 | return str.substr(0, len); 13 | } 14 | 15 | export async function randomBytes ( 16 | len: number 17 | ): Promise { 18 | return await new Promise((resolve, reject) => { 19 | _randomBytes(len, (err, buf) => err ? reject(err) : resolve(buf.toString('hex'))); 20 | }); 21 | } 22 | 23 | export const getLongToken = async (len: number): Promise => await randomBytes(len); 24 | 25 | export async function getShortToken ( 26 | len: number, 27 | ifDigits: boolean 28 | ): Promise { 29 | if (ifDigits) { 30 | return randomDigits(len); 31 | } 32 | 33 | const str1 = await randomBytes(Math.floor(len / 2) + 1); 34 | let str = str1.substr(0, len); 35 | 36 | if (str.match(/^[0-9]+$/)) { // tests will fail on all digits 37 | str = `q${str.substr(1)}`; // shhhh, secret. 38 | } 39 | 40 | return str; 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers/date-or-number-to-number.ts: -------------------------------------------------------------------------------- 1 | function isStringOfDigits(value: string) { 2 | return /^\d+$/.test(value); 3 | } 4 | 5 | /** 6 | * Converts a Date or number to a number. If a string is passed, it will be converted to a number if it is a string of digits, otherwise it will be converted to a Date (expects a date ISO string) and then a number. 7 | * @param dateOrNumber 8 | * @param fallBack The value to return if the value is undefined or null. Defaults to `0`. 9 | * @returns 10 | */ 11 | export function dateOrNumberToNumber(dateOrNumber: Date | number | string | undefined | null, fallBack = 0): number { 12 | if (!dateOrNumber) return fallBack; 13 | return typeof dateOrNumber === 'number' 14 | ? dateOrNumber 15 | : typeof dateOrNumber === 'string' 16 | ? isStringOfDigits(dateOrNumber) 17 | ? Number(dateOrNumber) 18 | : new Date(dateOrNumber).getTime() 19 | : new Date(dateOrNumber).getTime(); 20 | } 21 | -------------------------------------------------------------------------------- /src/helpers/get-long-token.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from './random-bytes'; 2 | 3 | export const getLongToken = async (len: number): Promise => await randomBytes(len); 4 | -------------------------------------------------------------------------------- /src/helpers/get-short-token.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from './random-bytes'; 2 | import { randomDigits } from './random-digits'; 3 | 4 | export async function getShortToken ( 5 | len: number, 6 | ifDigits: boolean 7 | ): Promise { 8 | if (ifDigits) { 9 | return randomDigits(len); 10 | } 11 | 12 | const str1 = await randomBytes(Math.floor(len / 2) + 1); 13 | let str = str1.substr(0, len); 14 | 15 | if (str.match(/^[0-9]+$/)) { // tests will fail on all digits 16 | str = `q${str.substr(1)}`; // shhhh, secret. 17 | } 18 | 19 | return str; 20 | } 21 | -------------------------------------------------------------------------------- /src/helpers/get-user-data.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors'; 2 | import { dateOrNumberToNumber } from './date-or-number-to-number'; 3 | import type { 4 | UsersArrayOrPaginated, 5 | User, 6 | GetUserDataCheckProps 7 | } from '../types'; 8 | 9 | function checkOneUser (users: User[]): User { 10 | if (users.length === 0) { 11 | throw new BadRequest( 12 | 'User not found.', 13 | { errors: { $className: 'badParams' } } 14 | ); 15 | } 16 | 17 | if (users.length !== 1) { 18 | throw new BadRequest( 19 | 'More than 1 user selected.', 20 | { errors: { $className: 'badParams' } } 21 | ); 22 | } 23 | 24 | return users[0]; 25 | } 26 | 27 | function checkUserChecks ( 28 | user: User, 29 | checks?: GetUserDataCheckProps 30 | ): void { 31 | checks = checks || []; 32 | 33 | if ( 34 | checks.includes('isNotVerified') && 35 | user.isVerified 36 | ) { 37 | throw new BadRequest( 38 | 'User is already verified.', 39 | { errors: { $className: 'isNotVerified' } } 40 | ); 41 | } 42 | 43 | if ( 44 | checks.includes('isNotVerifiedOrHasVerifyChanges') && 45 | user.isVerified && 46 | !Object.keys(user.verifyChanges || {}).length 47 | ) { 48 | throw new BadRequest( 49 | 'User is already verified & not awaiting changes.', 50 | { errors: { $className: 'nothingToVerify' } } 51 | ); 52 | } 53 | 54 | if ( 55 | checks.includes('isVerified') && 56 | !user.isVerified 57 | ) { 58 | throw new BadRequest( 59 | 'User is not verified.', 60 | { errors: { $className: 'isVerified' } } 61 | ); 62 | } 63 | 64 | if ( 65 | checks.includes('verifyNotExpired') && 66 | dateOrNumberToNumber(user.verifyExpires) < Date.now() 67 | ) { 68 | throw new BadRequest( 69 | 'Verification token has expired.', 70 | { errors: { $className: 'verifyExpired' } } 71 | ); 72 | } 73 | 74 | if ( 75 | checks.includes('resetNotExpired') && 76 | dateOrNumberToNumber(user.resetExpires) < Date.now() 77 | ) { 78 | throw new BadRequest( 79 | 'Password reset token has expired.', 80 | { errors: { $className: 'resetExpired' } } 81 | ); 82 | } 83 | } 84 | 85 | export function getUserData ( 86 | data: UsersArrayOrPaginated, 87 | checks?: GetUserDataCheckProps 88 | ): User { 89 | const users = Array.isArray(data) ? data : data.data; 90 | 91 | const user = checkOneUser(users); 92 | 93 | checkUserChecks(user, checks); 94 | 95 | return user; 96 | } 97 | -------------------------------------------------------------------------------- /src/helpers/hash-password.ts: -------------------------------------------------------------------------------- 1 | import { hooks as authLocalHooks } from '@feathersjs/authentication-local'; 2 | import type { Application, HookContext } from '@feathersjs/feathers'; 3 | 4 | export async function hashPassword ( 5 | app: Application, 6 | password: string, 7 | field: string 8 | ): Promise { 9 | if (!field) throw new Error('Field is missing'); 10 | const context = { 11 | type: 'before', 12 | data: { [field]: password }, 13 | params: { provider: null }, 14 | app 15 | }; 16 | await authLocalHooks.hashPassword(field)(context as HookContext); 17 | return context.data[field]; 18 | } 19 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors'; 2 | import type { Id } from '@feathersjs/feathers'; 3 | 4 | export function isDateAfterNow ( 5 | date: number | Date, 6 | delay = 0 7 | ): boolean { 8 | if (date instanceof Date) date = date.getTime(); 9 | 10 | return date > Date.now() + delay; 11 | } 12 | 13 | export function deconstructId ( 14 | token: string 15 | ): string { 16 | if (!token.includes('___')) { 17 | throw new BadRequest( 18 | 'Token is not in the correct format.', 19 | { errors: { $className: 'badParams' } } 20 | ); 21 | } 22 | 23 | return token.slice(0, token.indexOf('___')); 24 | } 25 | 26 | export function concatIDAndHash ( 27 | id: Id, 28 | token: string 29 | ): string { 30 | return `${id}___${token}`; 31 | } 32 | 33 | export function ensureValuesAreStrings ( 34 | ...values: string[] 35 | ): void { 36 | if (!values.every(str => typeof str === 'string')) { 37 | throw new BadRequest( 38 | 'Expected string value. (authLocalMgnt)', 39 | { errors: { $className: 'badParams' } } 40 | ); 41 | } 42 | } 43 | 44 | /** 45 | * Verify that obj1 and obj2 have different 'field' field 46 | * Returns false if either object is null/undefined 47 | */ 48 | export function ensureFieldHasChanged ( 49 | obj1: Record | null, 50 | obj2: Record | null 51 | ): (field: string) => boolean { 52 | return (obj1 == null || obj2 == null) 53 | ? () => false 54 | : field => obj1[field] !== obj2[field]; 55 | } 56 | 57 | export function ensureObjPropsValid ( 58 | obj: Record, 59 | props: string[], 60 | allowNone?: boolean 61 | ): void { 62 | const keys = Object.keys(obj); 63 | const valid = keys.every(key => props.includes(key) && typeof obj[key] === 'string'); 64 | 65 | if (!valid || (keys.length === 0 && !allowNone)) { 66 | throw new BadRequest( 67 | 'User info is not valid. (authLocalMgnt)', 68 | { errors: { $className: 'badParams' } } 69 | ); 70 | } 71 | } 72 | 73 | export { 74 | getLongToken, 75 | getShortToken, 76 | randomBytes, 77 | randomDigits 78 | } from './crypto'; 79 | 80 | export { cloneObject } from './clone-object'; 81 | export { comparePasswords } from './compare-passwords'; 82 | export { getUserData } from './get-user-data'; 83 | export { hashPassword } from './hash-password'; 84 | export { notify } from './notify'; 85 | export { sanitizeUserForClient } from './sanitize-user-for-client'; 86 | export { sanitizeUserForNotifier } from './sanitize-user-for-notifier'; 87 | -------------------------------------------------------------------------------- /src/helpers/notify.ts: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug'; 2 | import { sanitizeUserForNotifier } from './sanitize-user-for-notifier'; 3 | 4 | import type { User, Notifier, NotificationType } from '../types'; 5 | 6 | const debug = makeDebug('authLocalMgnt:notifier'); 7 | 8 | export async function notify ( 9 | notifier: Notifier, 10 | type: NotificationType, 11 | user: User, 12 | notifierOptions?: Record 13 | ): Promise { 14 | debug('notifier', type); 15 | 16 | await notifier(type, sanitizeUserForNotifier(user), notifierOptions || {}); 17 | return user; 18 | } 19 | -------------------------------------------------------------------------------- /src/helpers/random-bytes.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes as _randomBytes } from 'crypto'; 2 | 3 | export async function randomBytes ( 4 | len: number 5 | ): Promise { 6 | return await new Promise((resolve, reject) => { 7 | _randomBytes(len, (err, buf) => err ? reject(err) : resolve(buf.toString('hex'))); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/random-digits.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | 3 | export function randomDigits (len: number): string { 4 | let str = ''; 5 | 6 | while (str.length < len) { 7 | str += parseInt('0x' + randomBytes(4).toString('hex')).toString(); 8 | } 9 | 10 | return str.substr(0, len); 11 | } 12 | -------------------------------------------------------------------------------- /src/helpers/sanitize-user-for-client.ts: -------------------------------------------------------------------------------- 1 | import { cloneObject } from './clone-object'; 2 | import type { User } from '../types'; 3 | 4 | export function sanitizeUserForClient ( 5 | _user: User 6 | ): Record { 7 | const user = cloneObject(_user); 8 | 9 | delete user.password; 10 | delete user.verifyExpires; 11 | delete user.verifyToken; 12 | delete user.verifyShortToken; 13 | delete user.verifyChanges; 14 | delete user.resetExpires; 15 | delete user.resetToken; 16 | delete user.resetShortToken; 17 | 18 | return user; 19 | } 20 | -------------------------------------------------------------------------------- /src/helpers/sanitize-user-for-notifier.ts: -------------------------------------------------------------------------------- 1 | import { cloneObject } from './clone-object'; 2 | import type { User } from '../types'; 3 | 4 | export function sanitizeUserForNotifier (_user: User): Record { 5 | const user = cloneObject(_user); 6 | delete user.password; 7 | return user; 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/add-verification.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from '@feathersjs/errors'; 2 | import { checkContext } from 'feathers-hooks-common'; 3 | import { 4 | getLongToken, 5 | getShortToken, 6 | ensureFieldHasChanged 7 | } from '../helpers'; 8 | import { defaultPath } from '../options'; 9 | 10 | import type { HookContext } from '@feathersjs/feathers'; 11 | import type { AuthenticationManagementService } from '../services'; 12 | 13 | /** 14 | * 15 | * @param [path='authManagement'] the servicePath for your authManagement service 16 | * @returns 17 | */ 18 | export function addVerification ( 19 | path?: string 20 | ) { 21 | path = path || defaultPath; // default: 'authManagement' 22 | return async (context: H): Promise => { 23 | checkContext(context, 'before', ['create', 'patch', 'update']); 24 | 25 | try { 26 | const { options } = (context.app.service(path) as unknown as AuthenticationManagementService); 27 | 28 | const dataArray = (Array.isArray(context.data)) 29 | ? context.data 30 | : [context.data]; 31 | 32 | if ( 33 | (['patch', 'update'].includes(context.method)) && 34 | !!context.params.user && 35 | dataArray.some(data => { 36 | return !options.identifyUserProps.some(ensureFieldHasChanged(data, context.params.user)); 37 | }) 38 | ) { 39 | return context; 40 | } 41 | 42 | await Promise.all( 43 | dataArray.map(async data => { 44 | const [longToken, shortToken] = await Promise.all([ 45 | getLongToken(options.longTokenLen), 46 | getShortToken(options.shortTokenLen, options.shortTokenDigits) 47 | ]); 48 | 49 | data.isVerified = false; 50 | data.verifyExpires = Date.now() + options.delay; 51 | data.verifyToken = longToken; 52 | data.verifyShortToken = shortToken; 53 | data.verifyChanges = {}; 54 | }) 55 | ); 56 | 57 | return context; 58 | } catch (err) { 59 | throw new GeneralError(err); 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { addVerification } from './add-verification'; 2 | export { isVerified } from './is-verified'; 3 | export { removeVerification } from './remove-verification'; 4 | -------------------------------------------------------------------------------- /src/hooks/is-verified.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BadRequest } from '@feathersjs/errors'; 3 | import { checkContext } from 'feathers-hooks-common'; 4 | 5 | import type { HookContext } from '@feathersjs/feathers'; 6 | 7 | /** 8 | * Throws if `context.params?.user?.isVerified` is not true 9 | */ 10 | export function isVerified () { 11 | return (context: H): H => { 12 | checkContext(context, 'before'); 13 | 14 | if (!context.params?.user?.isVerified) { 15 | throw new BadRequest('User\'s email is not yet verified.'); 16 | } 17 | return context; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/remove-verification.ts: -------------------------------------------------------------------------------- 1 | import { checkContext, getItems, replaceItems } from 'feathers-hooks-common'; 2 | import type { HookContext } from '@feathersjs/feathers'; 3 | 4 | import type { User } from '../types'; 5 | 6 | /** 7 | * Sanitize users. After-hook for '/users' service. 8 | */ 9 | export function removeVerification ( 10 | ifReturnTokens?: boolean 11 | ) { 12 | return (context: H): H => { 13 | checkContext(context, 'after'); 14 | // Retrieve the items from the hook 15 | const items: User | User[] = getItems(context); 16 | if (!items) return; 17 | const isArray = Array.isArray(items); 18 | const users = (isArray ? items : [items]); 19 | 20 | users.forEach((user) => { 21 | if (!('isVerified' in user) && context.method === 'create') { 22 | /* eslint-disable no-console */ 23 | console.warn( 24 | 'Property isVerified not found in user properties. (removeVerification)' 25 | ); 26 | console.warn( 27 | "Have you added authManagement's properties to your model? (Refer to README)" 28 | ); 29 | console.warn( 30 | 'Have you added the addVerification hook on users::create?' 31 | ); 32 | /* eslint-enable */ 33 | } 34 | 35 | if (context.params.provider && user) { 36 | // noop if initiated by server 37 | delete user.verifyExpires; 38 | delete user.resetExpires; 39 | delete user.verifyChanges; 40 | if (!ifReturnTokens) { 41 | delete user.verifyToken; 42 | delete user.verifyShortToken; 43 | delete user.resetToken; 44 | delete user.resetShortToken; 45 | } 46 | } 47 | }); 48 | // Replace the items within the hook 49 | replaceItems(context, isArray ? users : users[0]); 50 | return context; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import setup from './setupAuthManagement'; 2 | 3 | import { 4 | addVerification, 5 | isVerified, 6 | removeVerification 7 | } from './hooks'; 8 | 9 | export default setup; 10 | 11 | export const hooks = { 12 | addVerification, 13 | isVerified, 14 | removeVerification 15 | }; 16 | 17 | export { addVerification }; 18 | export { isVerified }; 19 | export { removeVerification }; 20 | 21 | export { AuthenticationManagementService } from './services/AuthenticationManagementService'; 22 | export { CheckUniqueService } from './services/CheckUniqueService'; 23 | export { IdentityChangeService } from './services/IdentityChangeService'; 24 | export { PasswordChangeService } from './services/PasswordChangeService'; 25 | export { ResendVerifySignupService } from './services/ResendVerifySignupService'; 26 | export { ResetPwdLongService } from './services/ResetPwdLongService'; 27 | export { ResetPwdShortService } from './services/ResetPwdShortService'; 28 | export { SendResetPwdService } from './services/SendResetPwdService'; 29 | export { VerifySignupLongService } from './services/VerifySignupLongService'; 30 | export { VerifySignupSetPasswordLongService } from './services/VerifySignupSetPasswordLongService'; 31 | export { VerifySignupSetPasswordShortService } from './services/VerifySignupSetPasswordShortService'; 32 | export { VerifySignupShortService } from './services/VerifySignupShortService'; 33 | 34 | export * from './types'; 35 | 36 | export { default as client } from './client'; 37 | 38 | // commonjs 39 | if (typeof module !== 'undefined') { 40 | module.exports = Object.assign(setup, module.exports); 41 | } 42 | -------------------------------------------------------------------------------- /src/methods/check-unique.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors'; 2 | import makeDebug from 'debug'; 3 | 4 | import type { NullableId, Params } from '@feathersjs/feathers'; 5 | import type { 6 | CheckUniqueOptions, 7 | IdentifyUser, 8 | UsersArrayOrPaginated 9 | } from '../types'; 10 | 11 | const debug = makeDebug('authLocalMgnt:checkUnique'); 12 | 13 | /** 14 | * This module is usually called from the UI to check username, email, etc. are unique. 15 | */ 16 | export default async function checkUnique ( 17 | options: CheckUniqueOptions, 18 | identifyUser: IdentifyUser, 19 | ownId?: NullableId, 20 | meta?: { noErrMsg?: boolean}, 21 | params?: Params 22 | ): Promise { 23 | debug('checkUnique', identifyUser, ownId, meta); 24 | 25 | if (params && "query" in params) { 26 | params = Object.assign({}, params); 27 | delete params.query; 28 | } 29 | 30 | const { 31 | app, 32 | service 33 | } = options; 34 | 35 | const usersService = app.service(service); 36 | const usersServiceId = usersService.id; 37 | const errProps = []; 38 | 39 | const keys = Object.keys(identifyUser).filter( 40 | key => identifyUser[key] != null 41 | ); 42 | 43 | try { 44 | for (let i = 0, ilen = keys.length; i < ilen; i++) { 45 | const prop = keys[i]; 46 | const _params = { 47 | ...params, 48 | query: { [prop]: identifyUser[prop].trim(), $limit: 0 }, paginate: { default: 1 } 49 | }; 50 | 51 | if (ownId != null) { 52 | _params.query[usersServiceId] = { $ne: ownId }; 53 | } 54 | const users: UsersArrayOrPaginated = await usersService.find(_params); 55 | const length = Array.isArray(users) ? users.length : users.total; 56 | const isNotUnique = length > 0; 57 | 58 | if (isNotUnique) { 59 | errProps.push(prop); 60 | } 61 | } 62 | } catch (err) { 63 | throw new BadRequest( 64 | meta?.noErrMsg ? null : 'checkUnique unexpected error.', 65 | { errors: { msg: err.message, $className: 'unexpected' } } 66 | ); 67 | } 68 | 69 | if (errProps.length) { 70 | const errs = {}; 71 | errProps.forEach(prop => { errs[prop] = 'Already taken.'; }); 72 | 73 | throw new BadRequest( 74 | meta?.noErrMsg ? null : 'Values already taken.', 75 | { errors: errs } 76 | ); 77 | } 78 | 79 | return null; 80 | } 81 | -------------------------------------------------------------------------------- /src/methods/identity-change.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors'; 2 | import makeDebug from 'debug'; 3 | import { 4 | comparePasswords, 5 | ensureObjPropsValid, 6 | getLongToken, 7 | getShortToken, 8 | getUserData, 9 | notify 10 | } from '../helpers'; 11 | import type { Id, Params } from '@feathersjs/feathers'; 12 | 13 | import type { IdentifyUser, IdentityChangeOptions, SanitizedUser, NotifierOptions, User } from '../types'; 14 | 15 | const debug = makeDebug('authLocalMgnt:identityChange'); 16 | 17 | export default async function identityChange ( 18 | options: IdentityChangeOptions, 19 | identifyUser: IdentifyUser, 20 | password: string, 21 | changesIdentifyUser: Record, 22 | notifierOptions: NotifierOptions = {}, 23 | params?: Params 24 | ): Promise { 25 | // note this call does not update the authenticated user info in hooks.params.user. 26 | debug('identityChange', password, changesIdentifyUser); 27 | 28 | if (params && "query" in params) { 29 | params = Object.assign({}, params); 30 | delete params.query; 31 | } 32 | 33 | const usersService = options.app.service(options.service); 34 | const usersServiceId = usersService.id; 35 | const { 36 | delay, 37 | identifyUserProps, 38 | longTokenLen, 39 | shortTokenLen, 40 | shortTokenDigits, 41 | passwordField, 42 | notifier, 43 | sanitizeUserForClient 44 | } = options; 45 | 46 | ensureObjPropsValid(identifyUser, identifyUserProps); 47 | ensureObjPropsValid(changesIdentifyUser, identifyUserProps); 48 | 49 | const users = (await usersService.find({ 50 | ...params, 51 | query: { ...identifyUser, $limit: 2 }, 52 | paginate: false 53 | })) as User[]; 54 | const user = getUserData(users); 55 | 56 | try { 57 | await comparePasswords(password, user[passwordField] as string); 58 | } catch (err) { 59 | throw new BadRequest('Password is incorrect.', 60 | { errors: { [passwordField]: 'Password is incorrect.', $className: 'badParams' } } 61 | ); 62 | } 63 | 64 | const [verifyToken, verifyShortToken] = await Promise.all([ 65 | getLongToken(longTokenLen), 66 | getShortToken(shortTokenLen, shortTokenDigits) 67 | ]); 68 | 69 | const patchedUser = await usersService.patch(user[usersServiceId] as Id, { 70 | verifyExpires: Date.now() + delay, 71 | verifyToken, 72 | verifyShortToken, 73 | verifyChanges: changesIdentifyUser 74 | }, Object.assign({}, params)) as User; 75 | 76 | const userResult = await notify(notifier, 'identityChange', patchedUser, notifierOptions); 77 | return sanitizeUserForClient(userResult); 78 | } 79 | -------------------------------------------------------------------------------- /src/methods/password-change.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BadRequest } from '@feathersjs/errors'; 3 | import makeDebug from 'debug'; 4 | import { 5 | comparePasswords, 6 | ensureObjPropsValid, 7 | ensureValuesAreStrings, 8 | getUserData, 9 | hashPassword, 10 | notify 11 | } from '../helpers'; 12 | import type { Id, Params } from '@feathersjs/feathers'; 13 | 14 | import type { 15 | IdentifyUser, 16 | PasswordChangeOptions, 17 | SanitizedUser, 18 | NotifierOptions, 19 | User 20 | } from '../types'; 21 | 22 | const debug = makeDebug('authLocalMgnt:passwordChange'); 23 | 24 | export default async function passwordChange ( 25 | options: PasswordChangeOptions, 26 | identifyUser: IdentifyUser, 27 | oldPassword: string, 28 | password: string, 29 | notifierOptions: NotifierOptions = {}, 30 | params?: Params 31 | ): Promise { 32 | debug('passwordChange', oldPassword, password); 33 | 34 | if (params && "query" in params) { 35 | params = Object.assign({}, params); 36 | delete params.query; 37 | } 38 | 39 | const { 40 | app, 41 | identifyUserProps, 42 | passwordField, 43 | skipPasswordHash, 44 | sanitizeUserForClient, 45 | service, 46 | notifier 47 | } = options; 48 | 49 | const usersService = app.service(service); 50 | const usersServiceId = usersService.id; 51 | 52 | ensureValuesAreStrings(oldPassword, password); 53 | ensureObjPropsValid(identifyUser, identifyUserProps); 54 | 55 | const users = await usersService.find({ 56 | ...params, 57 | query: { ...identifyUser, $limit: 2 }, 58 | paginate: false, 59 | }) as User[]; 60 | const user = getUserData(users); 61 | 62 | try { 63 | await comparePasswords(oldPassword, user.password); 64 | } catch (err) { 65 | throw new BadRequest('Current password is incorrect.', { 66 | errors: { oldPassword: 'Current password is incorrect.' } 67 | }); 68 | } 69 | 70 | const patchedUser = await usersService.patch(user[usersServiceId] as Id, { 71 | password: skipPasswordHash ? password : await hashPassword(app, password, passwordField) 72 | }, Object.assign({}, params)) as User; 73 | 74 | const userResult = await notify(notifier, 'passwordChange', patchedUser, notifierOptions); 75 | return sanitizeUserForClient(userResult); 76 | } 77 | -------------------------------------------------------------------------------- /src/methods/resend-verify-signup.ts: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug'; 2 | import { 3 | ensureObjPropsValid, 4 | getLongToken, 5 | getShortToken, 6 | getUserData, 7 | notify 8 | } from '../helpers'; 9 | import type { Id, Params } from '@feathersjs/feathers'; 10 | 11 | import type { 12 | IdentifyUser, 13 | ResendVerifySignupOptions, 14 | SanitizedUser, 15 | NotifierOptions, 16 | User 17 | } from '../types'; 18 | 19 | const debug = makeDebug('authLocalMgnt:resendVerifySignup'); 20 | 21 | // {email}, {cellphone}, {verifyToken}, {verifyShortToken}, 22 | // {email, cellphone, verifyToken, verifyShortToken} 23 | export default async function resendVerifySignup ( 24 | options: ResendVerifySignupOptions, 25 | identifyUser: IdentifyUser, 26 | notifierOptions: NotifierOptions, 27 | params?: Params 28 | ): Promise { 29 | debug('identifyUser=', identifyUser); 30 | 31 | if (params && "query" in params) { 32 | params = Object.assign({}, params); 33 | delete params.query; 34 | } 35 | 36 | const { 37 | app, 38 | service, 39 | delay, 40 | identifyUserProps, 41 | longTokenLen, 42 | sanitizeUserForClient, 43 | shortTokenDigits, 44 | shortTokenLen, 45 | notifier 46 | } = options; 47 | 48 | const usersService = app.service(service); 49 | const usersServiceId = usersService.id; 50 | 51 | ensureObjPropsValid(identifyUser, 52 | identifyUserProps.concat('verifyToken', 'verifyShortToken') 53 | ); 54 | 55 | const users = await usersService.find({ 56 | ...params, 57 | query: { ...identifyUser, $limit: 2 }, 58 | paginate: false, 59 | }) as User[]; 60 | const user = getUserData(users, ['isNotVerified']); 61 | 62 | const [verifyToken, verifyShortToken] = await Promise.all([ 63 | getLongToken(longTokenLen), 64 | getShortToken(shortTokenLen, shortTokenDigits) 65 | ]); 66 | 67 | const patchedUser = await usersService.patch(user[usersServiceId] as Id, { 68 | isVerified: false, 69 | verifyExpires: Date.now() + delay, 70 | verifyToken, 71 | verifyShortToken 72 | }, Object.assign({}, params)) as User; 73 | 74 | const userResult = await notify(notifier, 'resendVerifySignup', patchedUser, notifierOptions); 75 | return sanitizeUserForClient(userResult); 76 | } 77 | -------------------------------------------------------------------------------- /src/methods/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors'; 2 | import makeDebug from 'debug'; 3 | import { 4 | comparePasswords, 5 | deconstructId, 6 | ensureObjPropsValid, 7 | ensureValuesAreStrings, 8 | getUserData, 9 | hashPassword, 10 | notify 11 | } from '../helpers'; 12 | import type { Id, Params } from '@feathersjs/feathers'; 13 | 14 | import type { 15 | IdentifyUser, 16 | ResetPasswordOptions, 17 | ResetPwdWithShortTokenOptions, 18 | SanitizedUser, 19 | Tokens, 20 | GetUserDataCheckProps, 21 | NotifierOptions, 22 | User 23 | } from '../types'; 24 | 25 | const debug = makeDebug('authLocalMgnt:resetPassword'); 26 | 27 | export async function resetPwdWithLongToken ( 28 | options: ResetPasswordOptions, 29 | resetToken: string, 30 | password: string, 31 | notifierOptions: NotifierOptions = {}, 32 | params?: Params 33 | ): Promise { 34 | ensureValuesAreStrings(resetToken, password); 35 | 36 | return await resetPassword( 37 | options, 38 | { resetToken }, 39 | { resetToken }, 40 | password, 41 | notifierOptions, 42 | params 43 | ); 44 | } 45 | 46 | export async function resetPwdWithShortToken ( 47 | options: ResetPwdWithShortTokenOptions, 48 | resetShortToken: string, 49 | identifyUser: IdentifyUser, 50 | password: string, 51 | notifierOptions: NotifierOptions = {}, 52 | params?: Params 53 | ): Promise { 54 | ensureValuesAreStrings(resetShortToken, password); 55 | ensureObjPropsValid(identifyUser, options.identifyUserProps); 56 | 57 | return await resetPassword( 58 | options, 59 | identifyUser, 60 | { resetShortToken }, 61 | password, 62 | notifierOptions, 63 | params 64 | ); 65 | } 66 | 67 | async function resetPassword ( 68 | options: ResetPasswordOptions, 69 | identifyUser: IdentifyUser, 70 | tokens: Tokens, 71 | password: string, 72 | notifierOptions: NotifierOptions = {}, 73 | params?: Params 74 | ): Promise { 75 | debug('resetPassword', identifyUser, tokens, password); 76 | 77 | if (params && "query" in params) { 78 | params = Object.assign({}, params); 79 | delete params.query; 80 | } 81 | 82 | const { 83 | app, 84 | service, 85 | skipIsVerifiedCheck, 86 | reuseResetToken, 87 | passwordField, 88 | skipPasswordHash, 89 | sanitizeUserForClient, 90 | notifier 91 | } = options; 92 | 93 | const usersService = app.service(service); 94 | const usersServiceId = usersService.id; 95 | let users; 96 | 97 | if (tokens.resetToken) { 98 | const id = deconstructId(tokens.resetToken); 99 | const user = await usersService.get(id, Object.assign({}, params)); 100 | users = [user]; 101 | } else if (tokens.resetShortToken) { 102 | users = await usersService.find({ 103 | ...params, 104 | query: { ...identifyUser, $limit: 2 }, 105 | paginate: false, 106 | }) as User[]; 107 | } else { 108 | throw new BadRequest( 109 | 'resetToken and resetShortToken are missing. (authLocalMgnt)', 110 | { errors: { $className: 'missingToken' } } 111 | ); 112 | } 113 | 114 | const checkProps: GetUserDataCheckProps = skipIsVerifiedCheck ? ['resetNotExpired'] : ['resetNotExpired', 'isVerified']; 115 | const user = getUserData(users, checkProps); 116 | 117 | // compare all tokens (hashed) 118 | const tokenChecks = Object.keys(tokens).map(async key => { 119 | if (reuseResetToken) { 120 | // Comparing token directly as reused resetToken is not hashed 121 | if (tokens[key] !== user[key]) { 122 | throw new BadRequest('Reset Token is incorrect. (authLocalMgnt)', { 123 | errors: { $className: 'incorrectToken' } 124 | }); 125 | } 126 | } else { 127 | return await comparePasswords( 128 | tokens[key], 129 | user[key] as string, 130 | () => 131 | new BadRequest( 132 | 'Reset Token is incorrect. (authLocalMgnt)', 133 | { errors: { $className: 'incorrectToken' } } 134 | ) 135 | ); 136 | } 137 | }); 138 | 139 | try { 140 | await Promise.all(tokenChecks); 141 | } catch (err) { 142 | // if token check fail, either decrease remaining attempts or cancel reset 143 | if (user.resetAttempts > 0) { 144 | await usersService.patch(user[usersServiceId], { 145 | resetAttempts: user.resetAttempts - 1 146 | }, Object.assign({}, params)); 147 | 148 | throw err; 149 | } else { 150 | await usersService.patch(user[usersServiceId], { 151 | resetToken: null, 152 | resetAttempts: null, 153 | resetShortToken: null, 154 | resetExpires: null 155 | }, Object.assign({}, params)); 156 | 157 | throw new BadRequest( 158 | 'Invalid token. Get for a new one. (authLocalMgnt)', 159 | { errors: { $className: 'invalidToken' } }); 160 | } 161 | } 162 | 163 | const patchedUser = await usersService.patch(user[usersServiceId] as Id, { 164 | [passwordField]: skipPasswordHash ? password : await hashPassword(app, password, passwordField), 165 | resetExpires: null, 166 | resetAttempts: null, 167 | resetToken: null, 168 | resetShortToken: null 169 | }, Object.assign({}, params)); 170 | 171 | const userResult = await notify(notifier, 'resetPwd', patchedUser, notifierOptions); 172 | return sanitizeUserForClient(userResult); 173 | } 174 | -------------------------------------------------------------------------------- /src/methods/send-reset-pwd.ts: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug'; 2 | 3 | import { 4 | concatIDAndHash, 5 | ensureObjPropsValid, 6 | getLongToken, 7 | getShortToken, 8 | getUserData, 9 | hashPassword, 10 | notify, 11 | isDateAfterNow 12 | } from '../helpers'; 13 | 14 | import type { Id, Params } from '@feathersjs/feathers'; 15 | import type { 16 | IdentifyUser, 17 | SanitizedUser, 18 | SendResetPwdOptions, 19 | NotifierOptions 20 | } from '../types'; 21 | 22 | const debug = makeDebug('authLocalMgnt:sendResetPwd'); 23 | 24 | export default async function sendResetPwd ( 25 | options: SendResetPwdOptions, 26 | identifyUser: IdentifyUser, 27 | notifierOptions: NotifierOptions = {}, 28 | params?: Params 29 | ): Promise { 30 | debug('sendResetPwd'); 31 | 32 | if (params && "query" in params) { 33 | params = Object.assign({}, params); 34 | delete params.query; 35 | } 36 | 37 | const { 38 | app, 39 | identifyUserProps, 40 | longTokenLen, 41 | passwordField, 42 | resetAttempts, 43 | resetDelay, 44 | reuseResetToken, 45 | sanitizeUserForClient, 46 | service, 47 | shortTokenDigits, 48 | shortTokenLen, 49 | skipIsVerifiedCheck, 50 | notifier 51 | } = options; 52 | 53 | const usersService = app.service(service); 54 | const usersServiceId = usersService.id; 55 | 56 | ensureObjPropsValid(identifyUser, identifyUserProps); 57 | 58 | const users = await usersService.find({ 59 | ...params, 60 | query: { ...identifyUser, $limit: 2 }, 61 | paginate: false, 62 | }); 63 | const user = getUserData(users, skipIsVerifiedCheck ? [] : ['isVerified']); 64 | 65 | if ( 66 | // Use existing token when it's not hashed, 67 | // and remaining time exceeds half of resetDelay 68 | reuseResetToken && user.resetToken && user.resetToken.includes('___') && 69 | isDateAfterNow(user.resetExpires, resetDelay / 2) 70 | ) { 71 | await notify(notifier, 'sendResetPwd', user, notifierOptions); 72 | return sanitizeUserForClient(user); 73 | } 74 | 75 | const [resetToken, resetShortToken] = await Promise.all([ 76 | getLongToken(longTokenLen), 77 | getShortToken(shortTokenLen, shortTokenDigits) 78 | ]); 79 | 80 | Object.assign(user, { 81 | resetExpires: Date.now() + resetDelay, 82 | resetAttempts: resetAttempts, 83 | resetToken: concatIDAndHash(user[usersServiceId] as Id, resetToken), 84 | resetShortToken: resetShortToken 85 | }); 86 | 87 | await notify(options.notifier, 'sendResetPwd', user, notifierOptions); 88 | 89 | const [resetToken3, resetShortToken3] = await Promise.all([ 90 | reuseResetToken ? user.resetToken : hashPassword(app, user.resetToken, passwordField), 91 | reuseResetToken ? user.resetShortToken : hashPassword(app, user.resetShortToken, passwordField) 92 | ]); 93 | 94 | const patchedUser = await usersService.patch(user[usersServiceId] as Id, { 95 | resetExpires: user.resetExpires, 96 | resetAttempts: user.resetAttempts, 97 | resetToken: resetToken3, 98 | resetShortToken: resetShortToken3 99 | }, Object.assign({}, params)); 100 | 101 | return sanitizeUserForClient(patchedUser); 102 | } 103 | -------------------------------------------------------------------------------- /src/methods/verify-signup-set-password.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors'; 2 | import makeDebug from 'debug'; 3 | import { 4 | ensureObjPropsValid, 5 | ensureValuesAreStrings, 6 | getUserData, 7 | hashPassword, 8 | isDateAfterNow, 9 | notify 10 | } from '../helpers'; 11 | import type { Id, Params } from '@feathersjs/feathers'; 12 | import type { VerifyChanges } from '..'; 13 | 14 | import type { 15 | IdentifyUser, 16 | SanitizedUser, 17 | Tokens, 18 | User, 19 | VerifySignupSetPasswordOptions, 20 | VerifySignupSetPasswordWithShortTokenOptions, 21 | NotifierOptions 22 | } from '../types'; 23 | 24 | const debug = makeDebug('authLocalMgnt:verifySignupSetPassword'); 25 | 26 | export async function verifySignupSetPasswordWithLongToken ( 27 | options: VerifySignupSetPasswordOptions, 28 | verifyToken: string, 29 | password: string, 30 | notifierOptions: NotifierOptions = {}, 31 | params?: Params 32 | ): Promise { 33 | ensureValuesAreStrings(verifyToken, password); 34 | 35 | const result = await verifySignupSetPassword( 36 | options, 37 | { verifyToken }, 38 | { verifyToken }, 39 | password, 40 | notifierOptions, 41 | params 42 | ); 43 | return result; 44 | } 45 | 46 | export async function verifySignupSetPasswordWithShortToken ( 47 | options: VerifySignupSetPasswordWithShortTokenOptions, 48 | verifyShortToken: string, 49 | identifyUser: IdentifyUser, 50 | password: string, 51 | notifierOptions: NotifierOptions = {}, 52 | params?: Params 53 | ): Promise { 54 | ensureValuesAreStrings(verifyShortToken, password); 55 | ensureObjPropsValid(identifyUser, options.identifyUserProps); 56 | 57 | const result = await verifySignupSetPassword( 58 | options, 59 | identifyUser, 60 | { 61 | verifyShortToken 62 | }, 63 | password, 64 | notifierOptions, 65 | params 66 | ); 67 | return result; 68 | } 69 | 70 | async function verifySignupSetPassword ( 71 | options: VerifySignupSetPasswordOptions, 72 | identifyUser: IdentifyUser, 73 | tokens: Tokens, 74 | password: string, 75 | notifierOptions: NotifierOptions = {}, 76 | params?: Params 77 | ): Promise { 78 | debug('verifySignupSetPassword', identifyUser, tokens, password); 79 | 80 | if (params && "query" in params) { 81 | params = Object.assign({}, params); 82 | delete params.query; 83 | } 84 | 85 | const { 86 | app, 87 | passwordField, 88 | skipPasswordHash, 89 | sanitizeUserForClient, 90 | service, 91 | notifier 92 | } = options; 93 | 94 | const usersService = app.service(service); 95 | const usersServiceId = usersService.id; 96 | 97 | const users = await usersService.find({ 98 | ...params, 99 | query: { ...identifyUser, $limit: 2 }, 100 | paginate: false, 101 | }) as User[]; 102 | const user = getUserData(users, [ 103 | 'isNotVerifiedOrHasVerifyChanges', 104 | 'verifyNotExpired' 105 | ]); 106 | 107 | if (!Object.keys(tokens).every((key) => tokens[key] === user[key])) { 108 | await eraseVerifyProps(user, user.isVerified, params); 109 | 110 | throw new BadRequest( 111 | 'Invalid token. Get for a new one. (authLocalMgnt)', 112 | { errors: { $className: 'badParam' } } 113 | ); 114 | } 115 | 116 | const userErasedVerify = await eraseVerifyPropsSetPassword( 117 | user, 118 | isDateAfterNow(user.verifyExpires), 119 | user.verifyChanges || {}, 120 | password, 121 | skipPasswordHash, 122 | params 123 | ); 124 | 125 | const userResult = await notify(notifier, 'verifySignupSetPassword', userErasedVerify, notifierOptions); 126 | return sanitizeUserForClient(userResult); 127 | 128 | async function eraseVerifyProps ( 129 | user: User, 130 | isVerified: boolean, 131 | params?: Params 132 | ): Promise { 133 | const patchData = Object.assign({}, { 134 | isVerified, 135 | verifyToken: null, 136 | verifyShortToken: null, 137 | verifyExpires: null, 138 | verifyChanges: {}, 139 | }); 140 | 141 | const result = await usersService.patch( 142 | user[usersServiceId] as Id, 143 | patchData, 144 | Object.assign({}, params) 145 | ) as User; 146 | return result; 147 | } 148 | 149 | async function eraseVerifyPropsSetPassword ( 150 | user: User, 151 | isVerified: boolean, 152 | verifyChanges: VerifyChanges, 153 | password: string, 154 | skipPasswordHash: boolean, 155 | params?: Params 156 | ): Promise { 157 | 158 | const patchData = Object.assign({}, verifyChanges || {}, { 159 | isVerified, 160 | verifyToken: null, 161 | verifyShortToken: null, 162 | verifyExpires: null, 163 | verifyChanges: {}, 164 | [passwordField]: skipPasswordHash ? password : await hashPassword(app, password, passwordField) 165 | }); 166 | 167 | const result = await usersService.patch( 168 | user[usersServiceId] as Id, 169 | patchData, 170 | Object.assign({}, params) 171 | ) as User; 172 | return result; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/methods/verify-signup.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors'; 2 | import makeDebug from 'debug'; 3 | import { 4 | ensureObjPropsValid, 5 | ensureValuesAreStrings, 6 | getUserData, 7 | notify, 8 | isDateAfterNow 9 | } from '../helpers'; 10 | import type { Id, Params } from '@feathersjs/feathers'; 11 | 12 | import type { 13 | SanitizedUser, 14 | VerifySignupOptions, 15 | VerifySignupWithShortTokenOptions, 16 | Tokens, 17 | User, 18 | IdentifyUser, 19 | NotifierOptions, 20 | VerifyChanges 21 | } from '../types'; 22 | 23 | const debug = makeDebug('authLocalMgnt:verifySignup'); 24 | 25 | export async function verifySignupWithLongToken ( 26 | options: VerifySignupOptions, 27 | verifyToken: string, 28 | notifierOptions: NotifierOptions = {}, 29 | params?: Params 30 | ): Promise { 31 | ensureValuesAreStrings(verifyToken); 32 | 33 | const result = await verifySignup( 34 | options, 35 | { verifyToken }, 36 | { verifyToken }, 37 | notifierOptions, 38 | params 39 | ); 40 | return result; 41 | } 42 | 43 | export async function verifySignupWithShortToken ( 44 | options: VerifySignupWithShortTokenOptions, 45 | verifyShortToken: string, 46 | identifyUser: IdentifyUser, 47 | notifierOptions: NotifierOptions = {}, 48 | params?: Params 49 | ): Promise { 50 | ensureValuesAreStrings(verifyShortToken); 51 | ensureObjPropsValid(identifyUser, options.identifyUserProps); 52 | 53 | const result = await verifySignup( 54 | options, 55 | identifyUser, 56 | { verifyShortToken }, 57 | notifierOptions, 58 | params 59 | ); 60 | return result; 61 | } 62 | 63 | async function verifySignup ( 64 | options: VerifySignupOptions, 65 | identifyUser: IdentifyUser, 66 | tokens: Tokens, 67 | notifierOptions: NotifierOptions = {}, 68 | params?: Params 69 | ): Promise { 70 | debug('verifySignup', identifyUser, tokens); 71 | 72 | if (params && "query" in params) { 73 | params = Object.assign({}, params); 74 | delete params.query; 75 | } 76 | 77 | const { 78 | app, 79 | sanitizeUserForClient, 80 | service, 81 | notifier 82 | } = options; 83 | 84 | const usersService = app.service(service); 85 | const usersServiceId = usersService.id; 86 | 87 | const users = await usersService.find( 88 | { 89 | ...params, 90 | query: { ...identifyUser, $limit: 2 }, 91 | paginate: false, 92 | } 93 | ); 94 | const user = getUserData(users, [ 95 | 'isNotVerifiedOrHasVerifyChanges', 96 | 'verifyNotExpired' 97 | ]); 98 | 99 | let userErasedVerify: User; 100 | 101 | if (!Object.keys(tokens).every(key => tokens[key] === user[key])) { 102 | userErasedVerify = await eraseVerifyProps(user, user.isVerified); 103 | 104 | throw new BadRequest( 105 | 'Invalid token. Get for a new one. (authLocalMgnt)', 106 | { errors: { $className: 'badParam' } } 107 | ); 108 | } else { 109 | userErasedVerify = await eraseVerifyProps( 110 | user, 111 | isDateAfterNow(user.verifyExpires), 112 | user.verifyChanges || {}, 113 | params 114 | ); 115 | } 116 | 117 | const userResult = await notify(notifier, 'verifySignup', userErasedVerify, notifierOptions); 118 | return sanitizeUserForClient(userResult); 119 | 120 | async function eraseVerifyProps ( 121 | user: User, 122 | isVerified: boolean, 123 | verifyChanges?: VerifyChanges, 124 | params?: Params 125 | ): Promise { 126 | const patchData = Object.assign({}, verifyChanges || {}, { 127 | isVerified, 128 | verifyToken: null, 129 | verifyShortToken: null, 130 | verifyExpires: null, 131 | verifyChanges: {} 132 | }); 133 | 134 | const result = await usersService.patch( 135 | user[usersServiceId] as Id, 136 | patchData, 137 | Object.assign({}, params) 138 | ); 139 | return result; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import _cloneDeep from 'lodash/cloneDeep'; 2 | import _pick from 'lodash/pick'; 3 | 4 | import { sanitizeUserForClient } from './helpers/sanitize-user-for-client'; 5 | 6 | import type { 7 | NotificationType, 8 | User, 9 | AuthenticationManagementServiceOptions 10 | } from './types'; 11 | 12 | export const defaultPath = 'authManagement'; 13 | 14 | export const optionsDefault: AuthenticationManagementServiceOptions = { 15 | service: '/users', // need exactly this for test suite 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 17 | notifier: async (type: NotificationType, user: User, notifierOptions) => {}, 18 | longTokenLen: 15, // token's length will be twice this 19 | shortTokenLen: 6, 20 | shortTokenDigits: true, 21 | resetDelay: 1000 * 60 * 60 * 2, // 2 hours 22 | delay: 1000 * 60 * 60 * 24 * 5, // 5 days 23 | resetAttempts: 0, 24 | reuseResetToken: false, 25 | identifyUserProps: ['email'], 26 | sanitizeUserForClient, 27 | skipIsVerifiedCheck: false, 28 | passwordField: 'password', 29 | skipPasswordHash: false, 30 | passParams: undefined 31 | }; 32 | 33 | export function makeDefaultOptions ( 34 | keys?: K[] 35 | ): Pick { 36 | const options = _cloneDeep(optionsDefault); 37 | if (!keys) { return options; } 38 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 39 | return _pick(options, keys); 40 | } 41 | -------------------------------------------------------------------------------- /src/services/AuthenticationManagementBase.ts: -------------------------------------------------------------------------------- 1 | import { MethodNotAllowed } from '@feathersjs/errors'; 2 | import type { Application, Params } from '@feathersjs/feathers'; 3 | 4 | export abstract class AuthenticationManagementBase { 5 | publish: unknown; 6 | app: Application; 7 | options: O; 8 | 9 | abstract _create (data: T, params?: Params): Promise; 10 | 11 | constructor (app: Application) { 12 | if (!app) { 13 | throw new Error("Service from 'feathers-authentication-management' needs the 'app' as first constructor argument"); 14 | } 15 | 16 | this.app = app; 17 | } 18 | 19 | async create (data: T, params?: Params): Promise { 20 | if (Array.isArray(data)) { 21 | return await Promise.reject( 22 | new MethodNotAllowed('authManagement does not handle multiple entries') 23 | ); 24 | } 25 | 26 | return await this._create(data, params); 27 | } 28 | 29 | protected get optionsWithApp (): O & { app: Application } { 30 | return Object.assign({ app: this.app }, this.options); 31 | } 32 | 33 | async setup (): Promise { 34 | if (typeof this.publish === 'function') { 35 | this.publish(() => null); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/services/CheckUniqueService.ts: -------------------------------------------------------------------------------- 1 | import checkUnique from '../methods/check-unique'; 2 | import { makeDefaultOptions } from '../options'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | import type { Application, Params } from '@feathersjs/feathers'; 5 | 6 | import type { 7 | DataCheckUnique, 8 | CheckUniqueServiceOptions 9 | } from '../types'; 10 | 11 | export class CheckUniqueService 12 | extends AuthenticationManagementBase { 13 | constructor (app: Application, options?: Partial) { 14 | super(app); 15 | 16 | const defaultOptions: CheckUniqueServiceOptions = makeDefaultOptions(['service', 'passParams']); 17 | this.options = Object.assign(defaultOptions, options); 18 | } 19 | 20 | async _create (data: DataCheckUnique, params?: Params): Promise { 21 | const passedParams = this.options.passParams && await this.options.passParams(params); 22 | 23 | return await checkUnique( 24 | this.optionsWithApp, 25 | data.user, 26 | data.ownId, 27 | data.meta, 28 | passedParams 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/services/IdentityChangeService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import identityChange from '../methods/identity-change'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | 5 | import type { 6 | SanitizedUser, 7 | DataIdentityChange, 8 | IdentityChangeServiceOptions 9 | } from '../types'; 10 | import type { Application, Params } from '@feathersjs/feathers'; 11 | 12 | export class IdentityChangeService 13 | extends AuthenticationManagementBase { 14 | constructor (app: Application, options?: Partial) { 15 | super(app); 16 | 17 | const defaultOptions: IdentityChangeServiceOptions = makeDefaultOptions([ 18 | 'service', 19 | 'notifier', 20 | 'longTokenLen', 21 | 'shortTokenLen', 22 | 'shortTokenDigits', 23 | 'delay', 24 | 'identifyUserProps', 25 | 'sanitizeUserForClient', 26 | 'passwordField', 27 | 'passParams' 28 | ]); 29 | 30 | this.options = Object.assign(defaultOptions, options); 31 | } 32 | 33 | async _create (data: DataIdentityChange, params?: Params): Promise { 34 | const passedParams = this.options.passParams && await this.options.passParams(params); 35 | 36 | return await identityChange( 37 | this.optionsWithApp, 38 | data.user, 39 | data.password, 40 | data.changes, 41 | data.notifierOptions, 42 | passedParams 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/services/PasswordChangeService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import passwordChange from '../methods/password-change'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | 5 | import type { 6 | SanitizedUser, 7 | DataPasswordChange, 8 | PasswordChangeOptions, 9 | PasswordChangeServiceOptions 10 | } from '../types'; 11 | import type { Application, Params } from '@feathersjs/feathers'; 12 | 13 | export class PasswordChangeService 14 | extends AuthenticationManagementBase { 15 | constructor (app: Application, options?: Partial) { 16 | super(app); 17 | 18 | const defaultOptions = makeDefaultOptions([ 19 | 'service', 20 | 'notifier', 21 | 'identifyUserProps', 22 | 'sanitizeUserForClient', 23 | 'passwordField', 24 | 'skipPasswordHash', 25 | 'passParams' 26 | ]); 27 | this.options = Object.assign(defaultOptions, options); 28 | } 29 | 30 | async _create (data: DataPasswordChange, params?: Params): Promise { 31 | const passedParams = this.options.passParams && await this.options.passParams(params); 32 | 33 | return await passwordChange( 34 | this.optionsWithApp, 35 | data.user, 36 | data.oldPassword, 37 | data.password, 38 | data.notifierOptions, 39 | passedParams 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/services/ResendVerifySignupService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import resendVerifySignup from '../methods/resend-verify-signup'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | 5 | import type { 6 | SanitizedUser, 7 | DataResendVerifySignup, 8 | ResendVerifySignupServiceOptions 9 | } from '../types'; 10 | import type { Application, Params } from '@feathersjs/feathers'; 11 | 12 | export class ResendVerifySignupService 13 | extends AuthenticationManagementBase { 14 | constructor (app: Application, options?: Partial) { 15 | super(app); 16 | 17 | const defaultOptions: ResendVerifySignupServiceOptions = makeDefaultOptions([ 18 | 'service', 19 | 'notifier', 20 | 'longTokenLen', 21 | 'shortTokenLen', 22 | 'shortTokenDigits', 23 | 'delay', 24 | 'identifyUserProps', 25 | 'sanitizeUserForClient', 26 | 'passParams' 27 | ]); 28 | 29 | this.options = Object.assign(defaultOptions, options); 30 | } 31 | 32 | async _create (data: DataResendVerifySignup, params?: Params): Promise { 33 | const passedParams = this.options.passParams && await this.options.passParams(params); 34 | 35 | return await resendVerifySignup( 36 | this.optionsWithApp, 37 | data.user, 38 | data.notifierOptions, 39 | passedParams 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/services/ResetPwdLongService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import { resetPwdWithLongToken } from '../methods/reset-password'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | 5 | import type { 6 | SanitizedUser, 7 | DataResetPwdLong, 8 | ResetPasswordServiceOptions 9 | } from '../types'; 10 | import type { Application, Params } from '@feathersjs/feathers'; 11 | 12 | export class ResetPwdLongService 13 | extends AuthenticationManagementBase { 14 | constructor (app: Application, options?: Partial) { 15 | super(app); 16 | 17 | const defaultOptions: ResetPasswordServiceOptions = makeDefaultOptions([ 18 | 'service', 19 | 'skipIsVerifiedCheck', 20 | 'notifier', 21 | 'reuseResetToken', 22 | 'sanitizeUserForClient', 23 | 'passwordField', 24 | 'skipPasswordHash', 25 | 'passParams' 26 | ]); 27 | this.options = Object.assign(defaultOptions, options); 28 | } 29 | 30 | async _create (data: DataResetPwdLong, params?: Params): Promise { 31 | const passedParams = this.options.passParams && await this.options.passParams(params); 32 | 33 | return await resetPwdWithLongToken( 34 | this.optionsWithApp, 35 | data.token, 36 | data.password, 37 | data.notifierOptions, 38 | passedParams 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/services/ResetPwdShortService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import { resetPwdWithShortToken } from '../methods/reset-password'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | 5 | import type { 6 | DataResetPwdShort, 7 | SanitizedUser, 8 | ResetPwdWithShortServiceOptions 9 | } from '../types'; 10 | import type { Application, Params } from '@feathersjs/feathers'; 11 | 12 | export class ResetPwdShortService 13 | extends AuthenticationManagementBase { 14 | constructor (app: Application, options?: Partial) { 15 | super(app); 16 | 17 | const defaultOptions: ResetPwdWithShortServiceOptions = makeDefaultOptions([ 18 | 'service', 19 | 'skipIsVerifiedCheck', 20 | 'reuseResetToken', 21 | 'notifier', 22 | 'reuseResetToken', 23 | 'sanitizeUserForClient', 24 | 'passwordField', 25 | 'skipPasswordHash', 26 | 'identifyUserProps', 27 | 'passParams' 28 | ]); 29 | this.options = Object.assign(defaultOptions, options); 30 | } 31 | 32 | async _create (data: DataResetPwdShort, params?: Params): Promise { 33 | const passedParams = this.options.passParams && await this.options.passParams(params); 34 | 35 | return await resetPwdWithShortToken( 36 | this.optionsWithApp, 37 | data.token, 38 | data.user, 39 | data.password, 40 | data.notifierOptions, 41 | passedParams 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/services/SendResetPwdService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import sendResetPwd from '../methods/send-reset-pwd'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | 5 | import type { 6 | DataSendResetPwd, 7 | SanitizedUser, 8 | SendResetPwdServiceOptions 9 | } from '../types'; 10 | import type { Application, Params } from '@feathersjs/feathers'; 11 | 12 | export class SendResetPwdService 13 | extends AuthenticationManagementBase { 14 | constructor (app: Application, options?: Partial) { 15 | super(app); 16 | 17 | const defaultOptions: SendResetPwdServiceOptions = makeDefaultOptions([ 18 | 'service', 19 | 'identifyUserProps', 20 | 'skipIsVerifiedCheck', 21 | 'reuseResetToken', 22 | 'resetDelay', 23 | 'sanitizeUserForClient', 24 | 'resetAttempts', 25 | 'shortTokenLen', 26 | 'longTokenLen', 27 | 'shortTokenDigits', 28 | 'notifier', 29 | 'passwordField', 30 | 'passParams' 31 | ]); 32 | this.options = Object.assign(defaultOptions, options); 33 | } 34 | 35 | async _create (data: DataSendResetPwd, params?: Params): Promise { 36 | const passedParams = this.options.passParams && await this.options.passParams(params); 37 | 38 | return await sendResetPwd( 39 | this.optionsWithApp, 40 | data.user, 41 | data.notifierOptions, 42 | passedParams 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/services/VerifySignupLongService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | 3 | import { verifySignupWithLongToken } from '../methods/verify-signup'; 4 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 5 | import type { Application, Params } from '@feathersjs/feathers'; 6 | 7 | import type { 8 | DataVerifySignupLong, 9 | SanitizedUser, 10 | VerifySignupLongServiceOptions 11 | } from '../types'; 12 | 13 | export class VerifySignupLongService 14 | extends AuthenticationManagementBase { 15 | constructor (app: Application, options?: Partial) { 16 | super(app); 17 | 18 | const defaultOptions: VerifySignupLongServiceOptions = makeDefaultOptions([ 19 | 'service', 20 | 'notifier', 21 | 'sanitizeUserForClient', 22 | 'passParams' 23 | ]); 24 | this.options = Object.assign(defaultOptions, options); 25 | } 26 | 27 | async _create (data: DataVerifySignupLong, params?: Params): Promise { 28 | const passedParams = this.options.passParams && await this.options.passParams(params); 29 | 30 | return await verifySignupWithLongToken( 31 | this.optionsWithApp, 32 | data.token, 33 | data.notifierOptions, 34 | passedParams 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/services/VerifySignupSetPasswordLongService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import { verifySignupSetPasswordWithLongToken } from '../methods/verify-signup-set-password'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | import type { 5 | DataVerifySignupSetPasswordLong, 6 | SanitizedUser, 7 | VerifySignupSetPasswordLongServiceOptions 8 | } from '../types'; 9 | import type { Application, Params } from '@feathersjs/feathers'; 10 | 11 | export class VerifySignupSetPasswordLongService 12 | extends AuthenticationManagementBase { 13 | constructor (app: Application, options?: Partial) { 14 | super(app); 15 | 16 | const defaultOptions: VerifySignupSetPasswordLongServiceOptions = makeDefaultOptions([ 17 | 'service', 18 | 'notifier', 19 | 'sanitizeUserForClient', 20 | 'passwordField', 21 | 'skipPasswordHash', 22 | 'passParams' 23 | ]); 24 | this.options = Object.assign(defaultOptions, options); 25 | } 26 | 27 | async _create (data: DataVerifySignupSetPasswordLong, params?: Params): Promise { 28 | const passedParams = this.options.passParams && await this.options.passParams(params); 29 | 30 | return await verifySignupSetPasswordWithLongToken( 31 | this.optionsWithApp, 32 | data.token, 33 | data.password, 34 | data.notifierOptions, 35 | passedParams 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/services/VerifySignupSetPasswordShortService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import { verifySignupSetPasswordWithShortToken } from '../methods/verify-signup-set-password'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | 5 | import type { 6 | DataVerifySignupSetPasswordShort, 7 | SanitizedUser, 8 | VerifySignupSetPasswordShortServiceOptions 9 | } from '../types'; 10 | import type { Application, Params } from '@feathersjs/feathers'; 11 | 12 | export class VerifySignupSetPasswordShortService 13 | extends AuthenticationManagementBase { 14 | constructor (app: Application, options?: Partial) { 15 | super(app); 16 | 17 | const defaultOptions: VerifySignupSetPasswordShortServiceOptions = makeDefaultOptions([ 18 | 'service', 19 | 'notifier', 20 | 'sanitizeUserForClient', 21 | 'passwordField', 22 | 'skipPasswordHash', 23 | 'identifyUserProps', 24 | 'passParams' 25 | ]); 26 | this.options = Object.assign(defaultOptions, options); 27 | } 28 | 29 | async _create (data: DataVerifySignupSetPasswordShort, params?: Params): Promise { 30 | const passedParams = this.options.passParams && await this.options.passParams(params); 31 | 32 | return await verifySignupSetPasswordWithShortToken( 33 | this.optionsWithApp, 34 | data.token, 35 | data.user, 36 | data.password, 37 | data.notifierOptions, 38 | passedParams 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/services/VerifySignupShortService.ts: -------------------------------------------------------------------------------- 1 | import { makeDefaultOptions } from '../options'; 2 | import { verifySignupWithShortToken } from '../methods/verify-signup'; 3 | import { AuthenticationManagementBase } from './AuthenticationManagementBase'; 4 | import type { 5 | DataVerifySignupShort, 6 | SanitizedUser, 7 | VerifySignupShortServiceOptions 8 | } from '../types'; 9 | 10 | import type { Application, Params } from '@feathersjs/feathers'; 11 | 12 | export class VerifySignupShortService 13 | extends AuthenticationManagementBase { 14 | constructor (app: Application, options?: Partial) { 15 | super(app); 16 | 17 | const defaultOptions: VerifySignupShortServiceOptions = makeDefaultOptions([ 18 | 'service', 19 | 'notifier', 20 | 'sanitizeUserForClient', 21 | 'passwordField', 22 | 'identifyUserProps', 23 | 'passParams' 24 | ]); 25 | this.options = Object.assign(defaultOptions, options); 26 | } 27 | 28 | async _create (data: DataVerifySignupShort, params?: Params): Promise { 29 | const passedParams = this.options.passParams && await this.options.passParams(params); 30 | 31 | return await verifySignupWithShortToken( 32 | this.optionsWithApp, 33 | data.token, 34 | data.user, 35 | data.notifierOptions, 36 | passedParams 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthenticationManagementService } from './AuthenticationManagementService'; 2 | export { CheckUniqueService } from './CheckUniqueService'; 3 | export { IdentityChangeService } from './IdentityChangeService'; 4 | export { PasswordChangeService } from './PasswordChangeService'; 5 | export { ResendVerifySignupService } from './ResendVerifySignupService'; 6 | export { ResetPwdLongService } from './ResetPwdLongService'; 7 | export { ResetPwdShortService } from './ResetPwdShortService'; 8 | export { SendResetPwdService } from './SendResetPwdService'; 9 | export { VerifySignupLongService } from './VerifySignupLongService'; 10 | export { VerifySignupSetPasswordLongService } from './VerifySignupSetPasswordLongService'; 11 | export { VerifySignupSetPasswordShortService } from './VerifySignupSetPasswordShortService'; 12 | export { VerifySignupShortService } from './VerifySignupShortService'; 13 | -------------------------------------------------------------------------------- /src/setupAuthManagement.ts: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug'; 2 | 3 | import { AuthenticationManagementService } from './services/AuthenticationManagementService'; 4 | import { defaultPath, makeDefaultOptions } from './options'; 5 | import type { AuthenticationManagementSetupOptions } from './types'; 6 | import type { Application } from '@feathersjs/feathers'; 7 | 8 | const debug = makeDebug('authLocalMgnt:service'); 9 | 10 | export default function authenticationLocalManagement ( 11 | _options?: Partial, 12 | docs?: Record 13 | ): (app: Application) => void { 14 | debug('service being configured.'); 15 | 16 | return function (app) { 17 | const defaultOptions = makeDefaultOptions(); 18 | const options = Object.assign( 19 | {}, 20 | defaultOptions, 21 | _options 22 | ); 23 | 24 | const path = _options?.path || defaultPath; 25 | 26 | const service = new AuthenticationManagementService(app, options); 27 | 28 | if (docs) { 29 | // @ts-expect-error service does not have docs 30 | service.docs = docs; 31 | } 32 | 33 | app.use(path, service); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /test/client.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Application, feathers } from '@feathersjs/feathers'; 3 | import AuthManagement from '../src/client'; 4 | import { AuthenticationManagementClient } from '../src/types'; 5 | 6 | // users DB 7 | const usersDb = [ 8 | { _id: 'a', email: 'bad', password: 'aa', isVerified: false }, 9 | { _id: 'b', email: 'ok', password: 'bb', isVerified: true } 10 | ]; 11 | 12 | let spyData = null; 13 | let spyParams = null; 14 | let spyAuthenticateEmail; 15 | let spyAuthenticatePassword; 16 | 17 | // Fake for authManagementService service 18 | const authLocalMgntFake = function () { 19 | return function authManagement () { // 'function' needed as we use 'this' 20 | const app = this; 21 | const path = 'authManagement'; 22 | 23 | app.use(path, { 24 | create (data, params1, cb) { 25 | spyData = data; 26 | spyParams = params1; 27 | 28 | return cb ? cb(null) : Promise.resolve(); 29 | } 30 | }); 31 | 32 | app.authenticate = (obj) => { 33 | spyAuthenticateEmail = obj.email; 34 | spyAuthenticatePassword = obj.password; 35 | 36 | const index = usersDb[0].email === obj.email ? 0 : 1; 37 | 38 | return Promise.resolve({ data: usersDb[index] }); 39 | }; 40 | 41 | app.log = () => {}; 42 | }; 43 | }; 44 | 45 | // Tests 46 | describe('client.test.ts', () => { 47 | describe('instantiate', () => { 48 | it('exists', () => { 49 | assert.strictEqual(typeof AuthManagement, 'function'); 50 | }); 51 | }); 52 | 53 | describe('methods', () => { 54 | let app: Application; 55 | let authManagement: AuthenticationManagementClient; 56 | 57 | beforeEach(() => { 58 | app = feathers(); 59 | app.configure(authLocalMgntFake()); 60 | app.setup(); 61 | authManagement = AuthManagement(app); 62 | }); 63 | 64 | it('checkUnique', async () => { 65 | await authManagement.checkUnique({ username: 'john a' }, null, true); 66 | 67 | assert.deepStrictEqual(spyParams, {}); 68 | assert.deepStrictEqual(spyData, { 69 | action: 'checkUnique', value: { username: 'john a' }, ownId: null, meta: { noErrMsg: true } 70 | }); 71 | }); 72 | 73 | it('resendVerify', async () => { 74 | await authManagement.resendVerifySignup({ email: 'a@a.com'}, { b: 'b' }); 75 | 76 | assert.deepStrictEqual(spyParams, {}); 77 | assert.deepStrictEqual(spyData, { 78 | action: 'resendVerifySignup', 79 | value: { email: 'a@a.com' }, 80 | notifierOptions: { b: 'b' } 81 | }); 82 | }); 83 | 84 | it('verifySignupLong', async () => { 85 | await authManagement.verifySignupLong('000'); 86 | 87 | assert.deepStrictEqual(spyParams, {}); 88 | assert.deepStrictEqual(spyData, { action: 'verifySignupLong', value: '000' }); 89 | }); 90 | 91 | it('verifySignupShort', async () => { 92 | await authManagement.verifySignupShort('000', { email: 'a@a.com' }); 93 | 94 | assert.deepStrictEqual(spyParams, {}); 95 | assert.deepStrictEqual(spyData, { 96 | action: 'verifySignupShort', 97 | value: { token: '000', user: { email: 'a@a.com' } } 98 | }); 99 | }); 100 | 101 | it('sendResetPwd', async () => { 102 | await authManagement.sendResetPwd({ email: 'a@a.com'}, { b: 'b' }); 103 | 104 | assert.deepStrictEqual(spyParams, {}); 105 | assert.deepStrictEqual(spyData, { 106 | action: 'sendResetPwd', 107 | value: { email: 'a@a.com' }, 108 | notifierOptions: { b: 'b' } 109 | }); 110 | }); 111 | 112 | it('resetPwdLong', async () => { 113 | await authManagement.resetPwdLong('000', '12345678'); 114 | 115 | assert.deepStrictEqual(spyParams, {}); 116 | assert.deepStrictEqual(spyData, { 117 | action: 'resetPwdLong', 118 | value: { token: '000', password: '12345678' } 119 | }); 120 | }); 121 | 122 | it('resetPwdShort', async () => { 123 | await authManagement.resetPwdShort('000', { email: 'a@a.com' }, '12345678'); 124 | 125 | assert.deepStrictEqual(spyParams, {}); 126 | assert.deepStrictEqual(spyData, { 127 | action: 'resetPwdShort', 128 | value: { token: '000', user: { email: 'a@a.com' }, password: '12345678' } 129 | }); 130 | }); 131 | 132 | it('passwordChange', async () => { 133 | await authManagement.passwordChange('12345678', 'password', { email: 'a' }); 134 | 135 | assert.deepStrictEqual(spyData, { 136 | action: 'passwordChange', value: { user: { email: 'a' }, oldPassword: '12345678', password: 'password' } 137 | }); 138 | }); 139 | 140 | it('identityChange', async () => { 141 | await authManagement.identityChange('12345678', { email: 'b@b.com' }, { username: 'q' }); 142 | 143 | assert.deepStrictEqual(spyData, { 144 | action: 'identityChange', 145 | value: { user: { username: 'q' }, 146 | password: '12345678', 147 | changes: { email: 'b@b.com' } } 148 | }); 149 | }); 150 | 151 | it('authenticate is verified', async () => { 152 | const result = await authManagement.authenticate('ok', 'bb'); 153 | 154 | assert.strictEqual(spyAuthenticateEmail, 'ok'); 155 | assert.strictEqual(spyAuthenticatePassword, 'bb'); 156 | assert.deepStrictEqual(result, usersDb[1]); 157 | }); 158 | 159 | it('authenticate is not verified', async () => { 160 | try { 161 | await authManagement.authenticate('bad', '12345678'); 162 | 163 | assert.fail('unexpected succeeded.'); 164 | } catch (err) { 165 | assert.strictEqual(spyAuthenticateEmail, 'bad'); 166 | assert.strictEqual(spyAuthenticatePassword, '12345678'); 167 | assert.notEqual(err, null); 168 | } 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/errors-async-await.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { BadRequest } from '@feathersjs/errors'; 3 | 4 | describe('errors-async-await.test.ts', () => { 5 | describe('1 deep', () => { 6 | describe('call as async function', () => { 7 | it('successful', async () => { 8 | try { 9 | const result = await service('ok'); 10 | assert.strictEqual(result, 'service ok'); 11 | } catch (err) { 12 | assert.fail(`unexpected error: ${err.message}`); 13 | } 14 | }); 15 | 16 | it('throw', async () => { 17 | try { 18 | const result = await service('throw'); 19 | assert.strictEqual(result, 'service ok'); 20 | } catch (err) { 21 | assert.strictEqual(err.message, 'service throw'); 22 | } 23 | }); 24 | }); 25 | 26 | describe('call expecting Promise', () => { 27 | it('successful', () => { 28 | return service('ok') 29 | .then(result => { 30 | assert.strictEqual(result, 'service ok'); 31 | }) 32 | .catch(err => { 33 | assert.fail(`unexpected error: ${err.message}`); 34 | }); 35 | }); 36 | 37 | it('throw', () => { 38 | return service('throw') 39 | .then(result => { 40 | assert.fail(`unexpectedly succeeded`); 41 | }) 42 | .catch(err => { 43 | assert.strictEqual(err.message, 'service throw'); 44 | }); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('2 deep', () => { 50 | describe('call as async function', () => { 51 | it('successful', async () => { 52 | try { 53 | const result = await service('passwordChange', 'ok'); 54 | assert.strictEqual(result, 'passwordChange ok'); 55 | } catch (err) { 56 | assert.fail(`unexpected error: ${err.message}`); 57 | } 58 | }); 59 | 60 | it('throw', async () => { 61 | try { 62 | const result = await service('passwordChange', 'throw'); 63 | assert.strictEqual(result, 'service ok'); 64 | } catch (err) { 65 | assert.strictEqual(err.message, 'passwordChange throw'); 66 | } 67 | }); 68 | }); 69 | 70 | describe('call expecting Promise', () => { 71 | it('successful', () => { 72 | return service('passwordChange', 'ok') 73 | .then(result => { 74 | assert.strictEqual(result, 'passwordChange ok'); 75 | }) 76 | .catch(err => { 77 | assert.fail(`unexpected error: ${err.message}`); 78 | }); 79 | }); 80 | 81 | it('throw', () => { 82 | return service('passwordChange', 'throw') 83 | .then(result => { 84 | assert.fail(`unexpectedly succeeded`); 85 | }) 86 | .catch(err => { 87 | assert.strictEqual(err.message, 'passwordChange throw'); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('3 deep', () => { 94 | describe('call as async function', () => { 95 | it('successful', async () => { 96 | try { 97 | const result = await service('passwordChange', 'ensureValuesAreStrings', 'ok'); 98 | assert.strictEqual(result, 'ensureValuesAreStrings ok'); 99 | } catch (err) { 100 | assert.fail(`unexpected error: ${err.message}`); 101 | } 102 | }); 103 | 104 | it('throw', async () => { 105 | try { 106 | const result = await service('passwordChange', 'ensureValuesAreStrings', 'throw'); 107 | assert.strictEqual(result, 'service ok'); 108 | } catch (err) { 109 | assert.strictEqual(err.message, 'ensureValuesAreStrings throw'); 110 | } 111 | }); 112 | }); 113 | 114 | describe('call expecting Promise', () => { 115 | it('successful', () => { 116 | return service('passwordChange', 'ensureValuesAreStrings', 'ok') 117 | .then(result => { 118 | assert.strictEqual(result, 'ensureValuesAreStrings ok'); 119 | }) 120 | .catch(err => { 121 | assert.fail(`unexpected error: ${err.message}`); 122 | }); 123 | }); 124 | 125 | it('throw', () => { 126 | return service('passwordChange', 'ensureValuesAreStrings', 'throw') 127 | .then(result => { 128 | assert.fail(`unexpectedly succeeded`); 129 | }) 130 | .catch(err => { 131 | assert.strictEqual(err.message, 'ensureValuesAreStrings throw'); 132 | }); 133 | }); 134 | }); 135 | }); 136 | }); 137 | 138 | async function service (action, param1?, param2?) { 139 | switch (action) { 140 | case 'ok': 141 | return 'service ok'; 142 | case 'passwordChange': 143 | try { 144 | return await passwordChange(param1, param2); 145 | } catch (err) { 146 | return Promise.reject(err); 147 | } 148 | case 'throw': 149 | throw new BadRequest('service throw'); 150 | default: 151 | throw new BadRequest('service throw default'); 152 | } 153 | } 154 | 155 | async function passwordChange (param1, param2) { 156 | switch (param1) { 157 | case 'ok': 158 | return 'passwordChange ok'; 159 | case 'throw': 160 | throw new BadRequest('passwordChange throw'); 161 | case 'ensureValuesAreStrings': 162 | return await ensureValuesAreStrings(param2); 163 | default: 164 | throw new BadRequest('passwordChange throw default'); 165 | } 166 | } 167 | 168 | async function ensureValuesAreStrings (param2) { 169 | switch (param2) { 170 | case 'ok': 171 | return 'ensureValuesAreStrings ok'; 172 | case 'throw': 173 | throw new BadRequest('ensureValuesAreStrings throw'); 174 | default: 175 | throw new BadRequest('ensureValuesAreStrings throw default'); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /test/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { feathers } from '@feathersjs/feathers'; 3 | import { MemoryService } from '@feathersjs/memory'; 4 | import authLocalMgnt from '../src/index'; 5 | import { 6 | deconstructId, 7 | ensureFieldHasChanged, 8 | ensureValuesAreStrings, 9 | sanitizeUserForNotifier, 10 | getUserData, 11 | hashPassword, 12 | isDateAfterNow, 13 | sanitizeUserForClient 14 | } from '../src/helpers'; 15 | import { User } from '../src/types'; 16 | import { addSeconds } from "date-fns" 17 | 18 | const makeUsersService = options => 19 | function (app) { 20 | Object.assign(options, { multi: true }); 21 | app.use('/users', new MemoryService(options)); 22 | }; 23 | 24 | const users_Id = [ 25 | { _id: 'a', email: 'a', username: 'john a', sensitiveData: 'some secret' } 26 | ]; 27 | 28 | describe('helpers.test.ts', () => { 29 | describe("sanitization", () => { 30 | it('allows to stringify sanitized user object', () => { 31 | const user = { 32 | id: 1, 33 | email: 'test@test.test', 34 | password: '0000000000', 35 | resetToken: 'aaa' 36 | } as any as User; 37 | 38 | const result1 = sanitizeUserForClient(user); 39 | const result2 = sanitizeUserForNotifier(user); 40 | 41 | assert.doesNotThrow(() => JSON.stringify(result1)); 42 | assert.doesNotThrow(() => JSON.stringify(result2)); 43 | }); 44 | 45 | it('throws error when stringifying sanitized object with circular reference', () => { 46 | const user = { 47 | id: 1, 48 | email: 'test@test.test', 49 | password: '0000000000', 50 | resetToken: 'aaa' 51 | } as any as User 52 | 53 | user.self = user; 54 | 55 | const result1 = sanitizeUserForClient(user); 56 | const result2 = sanitizeUserForNotifier(user); 57 | 58 | assert.throws(() => JSON.stringify(result1), TypeError); 59 | assert.throws(() => JSON.stringify(result2), TypeError); 60 | }); 61 | 62 | it('allows to stringify sanitized object with circular reference and custom toJSON()', () => { 63 | const user = { 64 | id: 1, 65 | email: 'test@test.test', 66 | password: '0000000000', 67 | resetToken: 'aaa', 68 | toJSON: function () { 69 | return Object.assign({}, this, { self: undefined }); 70 | } 71 | } as any as User; 72 | 73 | user.self = user; 74 | 75 | const result1 = sanitizeUserForClient(user); 76 | const result2 = sanitizeUserForNotifier(user); 77 | 78 | assert.doesNotThrow(() => JSON.stringify(result1)); 79 | assert.doesNotThrow(() => JSON.stringify(result2)); 80 | }); 81 | 82 | it('allows to stringify sanitized object with circular reference and custom toObject()', () => { 83 | const user = { 84 | id: 1, 85 | email: 'test@test.test', 86 | password: '0000000000', 87 | resetToken: 'aaa', 88 | toObject: function () { 89 | return Object.assign({}, this, { self: undefined }); 90 | } 91 | } as any as User; 92 | 93 | user.self = user; 94 | 95 | const result1 = sanitizeUserForClient(user); 96 | const result2 = sanitizeUserForNotifier(user); 97 | 98 | assert.doesNotThrow(() => JSON.stringify(result1)); 99 | assert.doesNotThrow(() => JSON.stringify(result2)); 100 | }); 101 | 102 | it('allows for customized sanitize function', async () => { 103 | const app = feathers(); 104 | app.configure(makeUsersService({ id: '_id' })); 105 | app.configure( 106 | authLocalMgnt({ 107 | sanitizeUserForClient: customSanitizeUserForClient 108 | }) 109 | ); 110 | app.setup(); 111 | const authManagement = app.service('authManagement'); 112 | 113 | const usersService = app.service('users'); 114 | await usersService.remove(null); 115 | await usersService.create(users_Id); 116 | 117 | const result = await authManagement.create({ 118 | action: 'resendVerifySignup', 119 | value: { email: 'a' } 120 | }); 121 | 122 | assert.strictEqual(result.sensitiveData, undefined); 123 | }); 124 | }) 125 | 126 | it("deconstructId", () => { 127 | const id = deconstructId("123___456"); 128 | assert.strictEqual(id, "123", "extracts id"); 129 | 130 | assert.throws( 131 | () => deconstructId("this is__a test") 132 | ) 133 | }); 134 | 135 | it("ensureFieldHasChanges", () => { 136 | const ensureForNulls = ensureFieldHasChanged(null, null); 137 | assert.ok(!ensureForNulls('password'), "returns false for nulls"); 138 | 139 | const ensureForChanged = ensureFieldHasChanged({ password: "1" }, { password: "2" }); 140 | assert.ok(ensureForChanged('password'), "changed password"); 141 | 142 | const ensureForUnchanged = ensureFieldHasChanged({ password: "1" }, { password: "1" }); 143 | assert.ok(ensureForChanged('password'), "password did not change") 144 | }); 145 | 146 | it("ensureValuesAreStrings", () => { 147 | assert.doesNotThrow( 148 | () => ensureValuesAreStrings(), 149 | "does not throw on empty array" 150 | ) 151 | 152 | assert.doesNotThrow( 153 | () => ensureValuesAreStrings("1", "2", "3"), 154 | "does not throw on string array" 155 | ) 156 | 157 | assert.throws( 158 | // @ts-expect-error anything other than string is not allowed 159 | () => ensureValuesAreStrings("1", "2", 3), 160 | "throws on mixed array" 161 | ) 162 | }) 163 | 164 | describe("getUserData", () => { 165 | it("throws with no users", () => { 166 | assert.throws( 167 | () => getUserData([]) 168 | ); 169 | 170 | assert.throws( 171 | () => getUserData({ data: [], limit: 10, skip: 0, total: 0 }) 172 | ) 173 | }) 174 | 175 | it("throws with users > 1", () => { 176 | assert.throws( 177 | () => getUserData([ 178 | // @ts-expect-error some props missing 179 | { id: 1, email: "test@test.de" }, 180 | // @ts-expect-error some props missing 181 | { id: 2, email: "test2@test.de" } 182 | ]) 183 | ) 184 | 185 | assert.throws( 186 | () => getUserData({ 187 | data: [ 188 | // @ts-expect-error some props missing 189 | { id: 1, email: "test@test.de" }, 190 | // @ts-expect-error some props missing 191 | { id: 2, email: "test2@test.de" } 192 | ], 193 | limit: 10, 194 | skip: 0, 195 | total: 2 196 | }) 197 | ) 198 | }); 199 | }); 200 | 201 | it("hashPassword", async () => { 202 | let rejected = false; 203 | try { 204 | // @ts-expect-error third argument is not provided 205 | await hashPassword(feathers(), "123") 206 | } catch { 207 | rejected = true; 208 | } 209 | 210 | assert.ok(rejected, "rejected"); 211 | }) 212 | 213 | it("isDateAfterNow", () => { 214 | const now = new Date(); 215 | const nowPlus1 = addSeconds(now, 1); 216 | 217 | assert.ok(isDateAfterNow(nowPlus1.getTime()), "unix is after now"); 218 | assert.ok(isDateAfterNow(nowPlus1), "date obj is after now"); 219 | 220 | assert.ok(!isDateAfterNow(now.getTime()), "now as unix returns false") 221 | assert.ok(!isDateAfterNow(now), "now as date obj returns false") 222 | 223 | assert.ok(isDateAfterNow(now.getTime(), -1000), "now as unix with delay returns true"); 224 | assert.ok(isDateAfterNow(now, -1000), "now as date obj with delay returns true"); 225 | }) 226 | }); 227 | 228 | function customSanitizeUserForClient (user) { 229 | const user1 = sanitizeUserForClient(user); 230 | delete user1.sensitiveData; 231 | return user1; 232 | } 233 | -------------------------------------------------------------------------------- /test/helpers/date-or-number-to-number.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "console"; 2 | import { dateOrNumberToNumber } from "../../src/helpers/date-or-number-to-number"; 3 | 4 | describe('date-or-number-to-number', () => { 5 | it('should return 0 for undefined', () => { 6 | assert(dateOrNumberToNumber(undefined) === 0); 7 | }); 8 | 9 | it('should return 0 for null', () => { 10 | assert(dateOrNumberToNumber(null) === 0); 11 | }); 12 | 13 | it('should return default for undefined', () => { 14 | assert(dateOrNumberToNumber(undefined, 1) === 1); 15 | }); 16 | 17 | it('should return default for null', () => { 18 | assert(dateOrNumberToNumber(null, 1) === 1); 19 | }); 20 | 21 | it('should return 0 for 0', () => { 22 | assert(dateOrNumberToNumber(0) === 0); 23 | }); 24 | 25 | it('should return 0 for "0"', () => { 26 | assert(dateOrNumberToNumber('0') === 0); 27 | }); 28 | 29 | it('should return number for a date', () => { 30 | assert(dateOrNumberToNumber(new Date(0)) === 0); 31 | }); 32 | 33 | it('should return a number for a dateISOString', () => { 34 | assert(dateOrNumberToNumber(new Date(0).toISOString()) === 0); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/hooks/is-verified.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { isVerified } from '../../src/hooks'; 3 | import { timeoutEachTest } from '../test-helpers/config'; 4 | 5 | describe('is-verified.test.ts', function () { 6 | this.timeout(timeoutEachTest); 7 | let context; 8 | 9 | beforeEach(() => { 10 | context = { 11 | type: 'before', 12 | method: 'create', 13 | params: { user: { email: 'a@a.com', password: '0000000000' } } 14 | }; 15 | }); 16 | 17 | it('throws if not before', () => { 18 | context.type = 'after'; 19 | assert.throws(() => { isVerified()(context); }, undefined, undefined); 20 | }); 21 | 22 | it('throws if not create, update or patch', () => { 23 | context.method = 'find'; 24 | assert.throws(() => isVerified()(context), undefined, undefined); 25 | 26 | context.method = 'get'; 27 | assert.throws(() => isVerified()(context), undefined, undefined); 28 | 29 | context.method = 'remove'; 30 | assert.throws(() => isVerified()(context), undefined, undefined); 31 | }); 32 | 33 | it('works with verified used', () => { 34 | context.params.user.isVerified = true; 35 | assert.doesNotThrow(() => { isVerified()(context); }); 36 | }); 37 | 38 | it('throws with unverified user', () => { 39 | context.params.user.isVerified = false; 40 | assert.throws(() => { isVerified()(context); }); 41 | }); 42 | 43 | it('throws if addVerification not run', () => { 44 | assert.throws(() => { isVerified()(context); }); 45 | }); 46 | 47 | it('throws if populate not run', () => { 48 | delete context.params.user; 49 | assert.throws(() => { isVerified()(context); }); 50 | }); 51 | 52 | it('throws with damaged hook', () => { 53 | delete context.params; 54 | assert.throws(() => { isVerified()(context); }); 55 | }); 56 | 57 | it('throws if not before', () => { 58 | context.type = 'after'; 59 | assert.throws(() => { isVerified()(context); }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/hooks/remove-verification.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { 3 | removeVerification 4 | } from '../../src/hooks'; 5 | 6 | let context; 7 | 8 | describe('remove-verification.test.ts', () => { 9 | beforeEach(() => { 10 | context = { 11 | type: 'after', 12 | method: 'create', 13 | params: { provider: 'socketio' }, 14 | result: { 15 | email: 'a@a.com', 16 | password: '0000000000', 17 | isVerified: true, 18 | verifyToken: '000', 19 | verifyExpires: Date.now(), 20 | verifyChanges: {}, 21 | resetToken: '000', 22 | resetExpires: Date.now() 23 | } 24 | }; 25 | }); 26 | 27 | it('works with verified user', () => { 28 | assert.doesNotThrow(() => { removeVerification()(context); }); 29 | 30 | const user = context.result; 31 | assert.ok(user.isVerified); 32 | assert.strictEqual(user.isVerified, true); 33 | assert.strictEqual(user.verifyToken, undefined); 34 | assert.strictEqual(user.verifyExpires, undefined); 35 | assert.strictEqual(user.resetToken, undefined); 36 | assert.strictEqual(user.resetExpires, undefined); 37 | assert.strictEqual(user.verifyChanges, undefined); 38 | }); 39 | 40 | it('works with unverified user', () => { 41 | context.result.isVerified = false; 42 | 43 | assert.doesNotThrow(() => { removeVerification()(context); }); 44 | 45 | const user = context.result; 46 | assert.strictEqual(user.isVerified, false); 47 | assert.strictEqual(user.isVerified, false); 48 | assert.strictEqual(user.verifyToken, undefined); 49 | assert.strictEqual(user.verifyExpires, undefined); 50 | assert.strictEqual(user.resetToken, undefined); 51 | assert.strictEqual(user.resetExpires, undefined); 52 | assert.strictEqual(user.verifyChanges, undefined); 53 | }); 54 | 55 | it('works if addVerification not run', () => { 56 | context.result = {}; 57 | 58 | assert.doesNotThrow(() => { removeVerification()(context); }); 59 | }); 60 | 61 | it('noop if server initiated', () => { 62 | context.params.provider = undefined; 63 | assert.doesNotThrow( 64 | () => { removeVerification()(context); } 65 | ); 66 | 67 | const user = context.result; 68 | assert.ok(user.isVerified); 69 | assert.strictEqual(user.isVerified, true); 70 | assert.ok(user.verifyToken); 71 | assert.ok(user.verifyExpires); 72 | assert.ok(user.resetToken); 73 | assert.ok(user.resetExpires); 74 | assert.ok(user.verifyChanges); 75 | }); 76 | 77 | it('works with multiple verified user', () => { 78 | context.result = [context.result, context.result]; 79 | assert.doesNotThrow(() => { removeVerification()(context); }); 80 | 81 | context.result.forEach(user => { 82 | assert.ok(user.isVerified); 83 | assert.strictEqual(user.isVerified, true); 84 | assert.strictEqual(user.verifyToken, undefined); 85 | assert.strictEqual(user.verifyExpires, undefined); 86 | assert.strictEqual(user.resetToken, undefined); 87 | assert.strictEqual(user.resetExpires, undefined); 88 | assert.strictEqual(user.verifyChanges, undefined); 89 | }); 90 | }); 91 | 92 | it('does not throw with damaged hook', () => { 93 | delete context.result; 94 | 95 | assert.doesNotThrow(() => { removeVerification()(context); }); 96 | }); 97 | 98 | it('throws if not after', () => { 99 | context.type = 'before'; 100 | 101 | assert.throws(() => { removeVerification()(context); }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/methods/check-unique.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { feathers } from '@feathersjs/feathers'; 3 | import { MemoryService, type MemoryServiceOptions } from '@feathersjs/memory'; 4 | import authLocalMgnt, { 5 | DataCheckUnique, 6 | DataCheckUniqueWithAction, 7 | CheckUniqueService, 8 | } from '../../src/index'; 9 | import { timeoutEachTest } from '../test-helpers/config'; 10 | import { HookContextTest, ParamsTest, Application } from '../types' 11 | 12 | 13 | const withAction = ( 14 | data: DataCheckUnique 15 | ): DataCheckUniqueWithAction => { 16 | return { 17 | action: "checkUnique", 18 | meta: data.meta, 19 | value: data.user, 20 | ownId: data.ownId 21 | } 22 | } 23 | 24 | ['_id', 'id'].forEach(idType => { 25 | const users = [ 26 | { [idType]: 'a', email: 'a', username: 'john a' }, 27 | { [idType]: 'b', email: 'b', username: 'john b' }, 28 | { [idType]: 'c', email: 'c', username: 'john b' } 29 | ]; 30 | 31 | ['paginated', 'non-paginated'].forEach(pagination => { 32 | [{ 33 | name: "authManagement.create", 34 | callMethod: (app: Application, data: DataCheckUnique, params?: ParamsTest) => { 35 | return app.service("authManagement").create(withAction(data), params); 36 | } 37 | }, { 38 | name: "authManagement.checkUnique", 39 | callMethod: (app: Application, data: DataCheckUnique, params?: ParamsTest) => { 40 | return app.service("authManagement").checkUnique(data, params); 41 | } 42 | }, { 43 | name: "authManagement/check-unique", 44 | callMethod: (app: Application, data: DataCheckUnique, params?: ParamsTest) => { 45 | return app.service("authManagement/check-unique").create(data, params); 46 | } 47 | }].forEach(({ name, callMethod }) => { 48 | describe(`check-unique.test.ts ${idType} ${pagination} ${name}`, function () { 49 | this.timeout(timeoutEachTest); 50 | 51 | describe('standard', () => { 52 | let app: Application; 53 | let usersService; 54 | 55 | beforeEach(async () => { 56 | app = feathers(); 57 | app.configure(authLocalMgnt({ 58 | passParams: params => params 59 | })); 60 | app.use("authManagement/check-unique", new CheckUniqueService(app, { 61 | passParams: params => params 62 | })); 63 | const optionsUsers: Partial = { 64 | multi: true, 65 | id: idType 66 | }; 67 | if (pagination === "paginated") { 68 | optionsUsers.paginate = { default: 10, max: 50 }; 69 | } 70 | app.use("users", new MemoryService(optionsUsers)) 71 | 72 | app.service("users").hooks({ 73 | before: { 74 | all: [ 75 | (context: HookContextTest) => { 76 | if ((context.params as any)?.call && "count" in (context.params as any).call) { 77 | (context.params as any).call.count++; 78 | } 79 | } 80 | ] 81 | } 82 | }) 83 | 84 | usersService = app.service('users'); 85 | await usersService.remove(null); 86 | await usersService.create( 87 | clone(users) 88 | ); 89 | }); 90 | 91 | it('returns a promise', async () => { 92 | let res = callMethod(app, { user: { username: 'john a' }}) 93 | 94 | assert.ok(res instanceof Promise, `no promise returned`); 95 | }); 96 | 97 | it('handles empty query', async () => { 98 | await callMethod(app, { user: {} }); 99 | }); 100 | 101 | it('handles empty query returning nothing', async () => { 102 | await callMethod(app, { user: { username: 'hjhjhj' }}); 103 | }); 104 | 105 | it('finds single query on single item', async () => { 106 | try { 107 | await callMethod(app, { user: { username: 'john a' }}); 108 | 109 | assert.fail(`test unexpectedly succeeded`); 110 | } catch (err) { 111 | assert.strictEqual(err.message, 'Values already taken.'); 112 | assert.strictEqual(err.errors.username, 'Already taken.'); 113 | } 114 | }); 115 | 116 | it('handles noErrMsg option', async () => { 117 | try { 118 | await callMethod(app, { 119 | user: { username: 'john a' }, 120 | meta: { noErrMsg: true } 121 | }); 122 | 123 | assert.fail(`${name}: test unexpectedly succeeded`); 124 | } catch (err) { 125 | assert.strictEqual(err.message, 'Error', `${name}: Error`); // feathers default for no error message 126 | assert.strictEqual(err.errors.username, 'Already taken.', `${name}: Already taken.`); 127 | } 128 | }); 129 | 130 | it('finds single query on multiple items', async () => { 131 | try { 132 | await callMethod(app, { user: { username: 'john b' }}); 133 | 134 | assert.fail(`${name}: test unexpectedly succeeded`); 135 | } catch (err) { 136 | assert.strictEqual(err.message, 'Values already taken.', `${name}: Values already taken.`); 137 | assert.strictEqual(err.errors.username, 'Already taken.', `${name}: Already taken.`); 138 | } 139 | }); 140 | 141 | it('finds multiple queries on same item', async () => { 142 | try { 143 | await callMethod(app, { user: { 144 | username: 'john a', email: 'a' 145 | }}); 146 | 147 | assert.fail(`${name}: test unexpectedly succeeded`); 148 | } catch (err) { 149 | assert.strictEqual(err.message, 'Values already taken.', `${name}: Values already taken.`); 150 | assert.strictEqual(err.errors.username, 'Already taken.', `${name}: Already taken.`); 151 | } 152 | }); 153 | 154 | it('finds multiple queries on different item', async () => { 155 | try { 156 | await callMethod(app, { user: { 157 | username: 'john a', 158 | email: 'b' 159 | }}); 160 | 161 | assert.fail(`${name}: test unexpectedly succeeded`); 162 | } catch (err) { 163 | assert.strictEqual(err.message, 'Values already taken.', `${name}: Values already taken.`); 164 | assert.strictEqual(err.errors.username, 'Already taken.', `${name}: Already taken.`); 165 | } 166 | }); 167 | 168 | it('ignores null & undefined queries', async () => { 169 | await callMethod(app, { user: { 170 | username: undefined, 171 | email: null 172 | }}); 173 | }); 174 | 175 | it('ignores current user on single item', async () => { 176 | await callMethod(app, { 177 | user: { username: 'john a' }, 178 | ownId: 'a' 179 | }); 180 | }); 181 | 182 | it('can use "passParams"', async () => { 183 | try { 184 | await callMethod(app, { 185 | user: { username: 'john b' }, 186 | ownId: 'b' 187 | }); 188 | 189 | assert.fail(`${name}: test unexpectedly succeeded`); 190 | } catch (err) { 191 | assert.strictEqual(err.message, 'Values already taken.'); 192 | assert.strictEqual(err.errors.username, 'Already taken.'); 193 | } 194 | }); 195 | 196 | it('handles empty query returning nothing', async () => { 197 | const params = { call: { count: 0 } }; 198 | await callMethod(app, { user: { username: 'hjhjhj' }}, params); 199 | assert.ok(params.call.count > 0, `${name}: count not incremented`); 200 | }); 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | // Helpers 208 | 209 | function clone (obj) { 210 | return JSON.parse(JSON.stringify(obj)); 211 | } 212 | -------------------------------------------------------------------------------- /test/randoms.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { 3 | randomDigits, 4 | randomBytes 5 | } from '../src/helpers'; 6 | 7 | describe('randoms.test.ts', () => { 8 | describe('randomDigits', () => { 9 | it('correct length', () => { 10 | const str = randomDigits(6); 11 | assert.strictEqual(str.length, 6); 12 | }); 13 | 14 | it('returns different values', () => { 15 | const str1 = randomDigits(6); 16 | const str2 = randomDigits(6); 17 | 18 | assert.strictEqual(str1.length, 6); 19 | assert.strictEqual(str2.length, 6); 20 | 21 | assert.notEqual(str1, str2); 22 | }); 23 | }); 24 | 25 | describe('randomBytes', () => { 26 | it('correct length', async () => { 27 | const str = await randomBytes(10); 28 | 29 | assert.strictEqual(str.length, 20); 30 | }); 31 | 32 | it('returns different values', async () => { 33 | const str1 = await randomBytes(10); 34 | const str2 = await randomBytes(10); 35 | 36 | assert.strictEqual(str1.length, 20); 37 | assert.strictEqual(str2.length, 20); 38 | assert.notEqual(str1, str2); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/spy-on.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import assert from 'assert'; 3 | import { SpyOn } from './test-helpers'; 4 | 5 | describe('spy-on.test.ts', () => { 6 | it('works with functions without callbacks', () => { 7 | const spy = SpyOn(test); 8 | spy.callWith(1, 2, 3); 9 | spy.callWith(4, 5, 6); 10 | 11 | assert.deepStrictEqual(spy.result(), [ 12 | { args: [1, 2, 3], result: ['y', false, [1, 2, 3]] }, 13 | { args: [4, 5, 6], result: ['y', false, [4, 5, 6]] } 14 | ]); 15 | 16 | function test (a, b, c) { return ['y', false, [a, b, c]]; } 17 | }); 18 | 19 | it('works with functions with a callback', (done) => { 20 | const spy = SpyOn(testCb); 21 | spy.callWithCb(1, 2, 3, (x, y, z) => { 22 | assert.strictEqual(x, 'a'); 23 | assert.strictEqual(y, true); 24 | assert.deepStrictEqual(z, [1, 2, 3]); 25 | 26 | spy.callWithCb(8, 9, 0, (x, y, z) => { 27 | assert.strictEqual(x, 'a'); 28 | assert.strictEqual(y, true); 29 | assert.deepStrictEqual(z, [8, 9, 0]); 30 | 31 | assert.deepStrictEqual(spy.result(), [ 32 | { args: [1, 2, 3], result: ['a', true, [1, 2, 3]] }, 33 | { args: [8, 9, 0], result: ['a', true, [8, 9, 0]] } 34 | ]); 35 | done(); 36 | }); 37 | }); 38 | 39 | function testCb (a, b, c, cb) { 40 | setTimeout(() => (cb('a', true, [a, b, c])), 0); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/test-helpers/about-equal-date-time.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | function aboutEqualDateTime ( 4 | time1: number | Date, 5 | time2: number | Date, 6 | msg?: string, 7 | delta?: number 8 | ) { 9 | delta = delta || 600; 10 | //@ts-ignore 11 | const diff = Math.abs(time1 - time2); 12 | assert.ok(diff <= delta, msg || `times differ by ${diff}ms`); 13 | } 14 | 15 | export default aboutEqualDateTime; 16 | -------------------------------------------------------------------------------- /test/test-helpers/authenticationService.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationService } from '@feathersjs/authentication'; 2 | import { LocalStrategy } from '@feathersjs/authentication-local'; 3 | import { Application } from '@feathersjs/feathers'; 4 | import { authentication as config } from './config'; 5 | 6 | export default (app: Application, options?: any) => { 7 | if (options) { 8 | app.set('authentication', options); 9 | } else { 10 | app.set('authentication', config); 11 | } 12 | const authService = new AuthenticationService(app); 13 | 14 | authService.register('local', new LocalStrategy()); 15 | return authService; 16 | }; 17 | -------------------------------------------------------------------------------- /test/test-helpers/basic-spy.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Create a light weight spy on functions. 4 | * 5 | * @param {Function} fcn - to spy on 6 | * @returns {Object} spy. Call fcn with spy.callWith(...). Get params and results with spy.result(). 7 | * @class 8 | * 9 | * (1) To test a function without a callback: 10 | * 11 | * function test(a, b, c) { return ['y', false, [a, b, c]]; } 12 | * const spyTest = new feathersStub.SpyOn(test); 13 | * spyTest.callWith(1, 2, 3); 14 | * spyTest.callWith(4, 5, 6); 15 | * 16 | * spyTest.result(); 17 | * // [ { args: [1, 2, 3], result: ['y', false, [1, 2, 3]] }, 18 | * // { args: [4, 5, 6], result: ['y', false, [4, 5, 6]] } ] 19 | * 20 | * (2) To test a function with a callback as the last param: 21 | * 22 | * function testCb(a, b, c, cb) { setTimeout(() => { return cb('a', true, [a, b, c]); }, 0); } 23 | * const spyTestCb = SpyOn(testCb); 24 | * spyTestCb.callWithCb(1, 2, 3, (x, y, z) => { 25 | * spyTestCb.callWithCb(8, 9, 0, (x, y, z) => { 26 | * 27 | * spyTestCb.result() 28 | * // [ { args: [1, 2, 3], result: ['a', true, [1, 2, 3]] }, 29 | * // { args: [8, 9, 0], result: ['a', true, [8, 9, 0]] } ] 30 | * }); 31 | * }); 32 | */ 33 | 34 | function SpyOn (fcn) { 35 | const stack = []; 36 | return { 37 | // spy on function without a callback 38 | // not being part of prototype chain allows callers to set 'this' 39 | callWith: function(...args) { 40 | const myStackOffset = stack.length; 41 | stack.push({ args: clone(args) }); 42 | const result = fcn.apply(this, args); 43 | stack[myStackOffset].result = result; // can handle recursion 44 | 45 | return result; 46 | }, 47 | // spy on function with a callback 48 | // not being part of prototype chain allows callers to set 'this' 49 | callWithCb: function (...args) { 50 | const myStackOffset = stack.length; 51 | stack.push({ args: args.slice(0, -1) }); 52 | 53 | args[args.length - 1] = cbWrapper(args[args.length - 1]); 54 | fcn.apply(this, args); 55 | 56 | function cbWrapper (fcnCb) { 57 | return function cbWrapperInner (...args1) { 58 | stack[myStackOffset].result = args1; 59 | 60 | fcnCb.apply(this, args1); 61 | }; 62 | } 63 | }, 64 | // return spy info 65 | result: function () { 66 | return stack; 67 | } 68 | }; 69 | } 70 | 71 | export default SpyOn; 72 | 73 | function clone (obj) { 74 | return JSON.parse(JSON.stringify(obj)); 75 | } 76 | -------------------------------------------------------------------------------- /test/test-helpers/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | timeoutEachTest: 40000, 3 | maxTimeAllTests: 1000 * 60 * 60 * 2, // 2 hours 4 | defaultVerifyDelay: 1000 * 60 * 60 * 24 * 5, // 5 days 5 | authentication: { 6 | entity: 'user', 7 | service: 'users', 8 | secret: 'QM7Pn9fttvOOGI+VJym3KSJnUSoaspodkapsokdpaoskdpokasdopkaspokdpaosk=', 9 | authStrategies: ['local'], 10 | local: { 11 | usernameField: 'email', 12 | passwordField: 'password' 13 | } 14 | } 15 | }; 16 | 17 | export default config; 18 | 19 | export const timeoutEachTest = config.timeoutEachTest; 20 | export const maxTimeAllTests = config.maxTimeAllTests; 21 | export const defaultVerifyDelay = config.defaultVerifyDelay; 22 | export const authentication = config.authentication; 23 | -------------------------------------------------------------------------------- /test/test-helpers/index.ts: -------------------------------------------------------------------------------- 1 | import aboutEqualDateTime from "./about-equal-date-time"; 2 | import authenticationService from "./authenticationService"; 3 | import SpyOn from "./basic-spy"; 4 | import config from "./config"; 5 | import makeDateTime from "./make-date-time"; 6 | 7 | export { 8 | aboutEqualDateTime, 9 | authenticationService, 10 | SpyOn, 11 | config, 12 | makeDateTime 13 | } 14 | -------------------------------------------------------------------------------- /test/test-helpers/make-date-time.ts: -------------------------------------------------------------------------------- 1 | import { defaultVerifyDelay } from './config'; 2 | 3 | function makeDateTime ( 4 | options?: { delay?: number } 5 | ): number { 6 | options = options || {}; 7 | return Date.now() + (options.delay || defaultVerifyDelay); 8 | } 9 | 10 | export default makeDateTime; 11 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationService } from "@feathersjs/authentication/lib"; 2 | import { Application as FeathersApplication, HookContext, Params, Query } from "@feathersjs/feathers"; 3 | import { AuthenticationManagementService, CheckUniqueService, IdentityChangeService, PasswordChangeService, ResendVerifySignupService, ResetPwdLongService, ResetPwdShortService, SendResetPwdService, User, VerifySignupLongService, VerifySignupSetPasswordLongService, VerifySignupSetPasswordShortService, VerifySignupShortService } from "../src"; 4 | import { type MemoryService } from '@feathersjs/memory'; 5 | 6 | /** 7 | * These typings are for test purpose only 8 | */ 9 | 10 | export interface ParamsTest extends Params { 11 | call: { 12 | count: number 13 | } 14 | } 15 | 16 | interface ServiceTypes { 17 | authentication: AuthenticationService 18 | authManagement: AuthenticationManagementService; 19 | "authManagement/check-unique": CheckUniqueService 20 | "authManagement/identity-change": IdentityChangeService 21 | "authManagement/password-change": PasswordChangeService 22 | "authManagement/resend-verify-signup": ResendVerifySignupService 23 | "authManagement/reset-password-long": ResetPwdLongService 24 | "authManagement/reset-password-short": ResetPwdShortService 25 | "authManagement/send-reset-password": SendResetPwdService 26 | "authManagement/verify-signup-long": VerifySignupLongService 27 | "authManagement/verify-signup-short": VerifySignupShortService 28 | "authManagement/verify-signup-set-password-long": VerifySignupSetPasswordLongService 29 | "authManagement/verify-signup-set-password-short": VerifySignupSetPasswordShortService 30 | users: MemoryService 31 | } 32 | 33 | export type Application = FeathersApplication 34 | 35 | export interface HookContextTest extends HookContext {} 36 | 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "target": "ES2015", 8 | "module": "commonjs", 9 | "downlevelIteration": true, 10 | "sourceMap": false, 11 | "declaration": true, 12 | "strictNullChecks": false, 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "**/*.test.*", 20 | ".eslintrc.js", 21 | "examples/**", 22 | "docs/**/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "target": "es6", 8 | "sourceMap": true, 9 | "allowJs": true 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": [ 13 | "node_modules", 14 | "**/*.test.ts", 15 | "examples/**" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------