diff --git a/extensions/authentication-jwt/.npmrc b/extensions/authentication-jwt/.npmrc new file mode 100644 index 000000000000..cafe685a112d --- /dev/null +++ b/extensions/authentication-jwt/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/extensions/authentication-jwt/LICENSE b/extensions/authentication-jwt/LICENSE new file mode 100644 index 000000000000..4b3ed4fd4f0e --- /dev/null +++ b/extensions/authentication-jwt/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2020. All Rights Reserved. +Node module: @loopback/extension-authentication-jwt +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/extensions/authentication-jwt/README.md b/extensions/authentication-jwt/README.md new file mode 100644 index 000000000000..a9339eee27ff --- /dev/null +++ b/extensions/authentication-jwt/README.md @@ -0,0 +1,330 @@ +# @loopback/extension-authentication-jwt + +This module exports the jwt authentication strategy and its corresponding token +and user service as a component. You can mount the component to get a prototype +token based authentication system in your LoopBack 4 application. + +## Usage + +To use this component, you need to have an existing LoopBack 4 application and a +datasource in it for persistency. + +- create app: run `lb4 app` +- create datasource: run `lb4 datasource` + +Next enable the jwt authentication system in your application: + +- add authenticate action + +
+Check The Code +

+ +```ts +import { + AuthenticateFn, + AuthenticationBindings, + AUTHENTICATION_STRATEGY_NOT_FOUND, + USER_PROFILE_NOT_FOUND, +} from '@loopback/authentication'; +export class MySequence implements SequenceHandler { + constructor( + // - enable jwt auth - + // inject the auth action + @inject(AuthenticationBindings.AUTH_ACTION) + protected authenticateRequest: AuthenticateFn, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + // - enable jwt auth - + // call authentication action + await this.authenticateRequest(request); + + const args = await this.parseParams(request, route); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (error) { + // - enable jwt auth - + // improve the error check + if ( + error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || + error.code === USER_PROFILE_NOT_FOUND + ) { + Object.assign(error, {statusCode: 401 /* Unauthorized */}); + } + this.reject(context, error); + } + } +} +``` + +

+
+ +- mount jwt component in application + +
+Check The Code +

+ +```ts +import {AuthenticationComponent} from '@loopback/authentication'; +import { + JWTAuthenticationComponent, + SECURITY_SCHEME_SPEC, +} from '@loopback/extension-authentication-jwt'; + +export class TestApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options: ApplicationConfig = {}) { + super(options); + + // Set up the custom sequence + this.sequence(MySequence); + + // Set up default home page + this.static('/', path.join(__dirname, '../public')); + + // - enable jwt auth - + // Add security spec (Future work: refactor it to an enhancer) + this.addSecuritySpec(); + // Mount authentication system + this.component(AuthenticationComponent); + // Mount jwt component + this.component(JWTAuthenticationComponent); + // Bind datasource + this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME); + + this.component(RestExplorerComponent); + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = {}; + } + + // - enable jwt auth - + // Currently there is an extra function to + // merge the security spec into the application. + // This will be improved with a coming enhancer. + // See section [Future Work](#future-work) + addSecuritySpec(): void { + this.api({ + openapi: '3.0.0', + info: { + title: 'test application', + version: '1.0.0', + }, + paths: {}, + components: {securitySchemes: SECURITY_SCHEME_SPEC}, + security: [ + { + // secure all endpoints with 'jwt' + jwt: [], + }, + ], + servers: [{url: '/'}], + }); + } +} +``` + +

+
+ +_All the jwt authentication related code are marked with comment "- enable jwt +auth -", you can search for it to find all the related code you need to enable +the entire jwt authentication in a LoopBack 4 application._ + +## Adding Endpoint in Controller + +After mounting the component, you can call token and user services to perform +login, then decorate endpoints with `@authentication('jwt')` to inject the +logged in user's profile. + +This module contains an example application in the `fixtures` folder. It has a +controller with endpoints `/login` and `/whoAmI`. + +The code snippet for login function: + +```ts +async login( + @requestBody(CredentialsRequestBody) credentials: Credentials, + ): Promise<{token: string}> { + // ensure the user exists, and the password is correct + const user = await this.userService.verifyCredentials(credentials); + + // convert a User object into a UserProfile object (reduced set of properties) + const userProfile = this.userService.convertToUserProfile(user); + + // create a JSON Web Token based on the user profile + const token = await this.jwtService.generateToken(userProfile); + + return {token}; + } +``` + +The code snippet for whoAmI function: + +```ts +@authenticate('jwt') + async whoAmI(): Promise { + return this.user[securityId]; + } +``` + +The complete file is in +[user.controller.ts](https://github.com/strongloop/loopback-next/tree/master/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts) + +## Customization + +As a prototype implementation this module provides basic functionalities in each +service. You can customize and re-bind any element provided in the +[component](https://github.com/strongloop/loopback-next/tree/master/extensions/authentication-jwt/src/jwt-authentication-component.ts) +with your own one. + +Replacing the `User` model is a bit more complicated because it's not injected +but imported directly in related files. The sub-section covers the steps to +provide your own `User` model and repository. + +### Customizing User + +1. Create your own user model and repository by running the `lb4 model` and + `lb4 repository` commands. + +2. The user service requires the user model and repository, to provide your own + ones, you can create a custom `UserService` and bind it to + `UserServiceBindings.USER_SERVICE`. Take a look at + [the default user service](https://github.com/strongloop/loopback-next/blob/master/extensions/authentication-jwt/src/services/user.service.ts) + for an example of `UserService` implementation. + + For convenience, here is the code in `user.service.ts`. You can replace the + `User` and `UserRepository` with `MyUser`, `MyUserRepository`: + +
+ Check The Code +

+ + ```ts + import {UserService} from '@loopback/authentication'; + import {repository} from '@loopback/repository'; + import {HttpErrors} from '@loopback/rest'; + import {securityId, UserProfile} from '@loopback/security'; + import {compare} from 'bcryptjs'; + // User --> MyUser + import {MyUser} from '../models'; + // UserRepository --> MyUserRepository + import {MyUserRepository} from '../repositories'; + + export type Credentials = { + email: string; + password: string; + }; + + // User --> MyUser + export class CustomUserService implements UserService { + constructor( + // UserRepository --> MyUserRepository + @repository(MyUserRepository) public userRepository: MyUserRepository, + ) {} + + // User --> MyUser + async verifyCredentials(credentials: Credentials): Promise { + const invalidCredentialsError = 'Invalid email or password.'; + + const foundUser = await this.userRepository.findOne({ + where: {email: credentials.email}, + }); + if (!foundUser) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + const credentialsFound = await this.userRepository.findCredentials( + foundUser.id, + ); + if (!credentialsFound) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + const passwordMatched = await compare( + credentials.password, + credentialsFound.password, + ); + + if (!passwordMatched) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + return foundUser; + } + + // User --> MyUser + convertToUserProfile(user: MyUser): UserProfile { + return { + [securityId]: user.id.toString(), + name: user.username, + id: user.id, + email: user.email, + }; + } + } + ``` + +

+
+ +3. Bind `MyUserRepository` (and `MyUserCredentialsRepository` if you create your + own as well) to the corresponding key in your `application.ts`: + + ```ts + import {CustomUserService} from './services/custom-user-service'; + import {MyUserRepository, MyUserCredentialsRepository} from './repositories'; + import {UserServiceBindings} from '@loopback/extension-authentication-jwt'; + + export class TestApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), + ) { + constructor(options: ApplicationConfig = {}) { + super(options); + // ...other setup + this.component(JWTAuthenticationComponent); + // Bind datasource + this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME); + // Bind user service + this.bind(UserServiceBindings.USER_SERVICE).toClass(CustomUserService), + // Bind user and credentials repository + this.bind(UserServiceBindings.USER_REPOSITORY).toClass( + UserRepository, + ), + this.bind(UserServiceBindings.USER_CREDENTIALS_REPOSITORY).toClass( + UserCredentialsRepository, + ), + } + } + ``` + +## Future Work + +The security specification is currently manually added in the application file. +The next step is to create an enhancer in the component to automatically bind +the spec when app starts. + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/extensions/authentication-jwt/index.d.ts b/extensions/authentication-jwt/index.d.ts new file mode 100644 index 000000000000..7506e77c9fe6 --- /dev/null +++ b/extensions/authentication-jwt/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/extensions/authentication-jwt/index.js b/extensions/authentication-jwt/index.js new file mode 100644 index 000000000000..8b6848eee96b --- /dev/null +++ b/extensions/authentication-jwt/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/extensions/authentication-jwt/index.ts b/extensions/authentication-jwt/index.ts new file mode 100644 index 000000000000..c7799dce1b24 --- /dev/null +++ b/extensions/authentication-jwt/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/extensions/authentication-jwt/package-lock.json b/extensions/authentication-jwt/package-lock.json new file mode 100644 index 000000000000..23c7c76c6e83 --- /dev/null +++ b/extensions/authentication-jwt/package-lock.json @@ -0,0 +1,1351 @@ +{ + "name": "@loopback/extension-authentication-jwt", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", + "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", + "dev": true + }, + "@types/node": { + "version": "10.17.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.18.tgz", + "integrity": "sha512-DQ2hl/Jl3g33KuAUOcMrcAOtsbzb+y/ufakzAdeK9z/H/xsvkpbETZZbPNMIiQuk24f5ZRMCcZIViAwyFIiKmg==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.26.0.tgz", + "integrity": "sha512-4yUnLv40bzfzsXcTAtZyTjbiGUXMrcIJcIMioI22tSOyAxpdXiZ4r7YQUU8Jj6XXrLz9d5aMHPQf5JFR7h27Nw==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.26.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz", + "integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.26.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.26.0.tgz", + "integrity": "sha512-+Xj5fucDtdKEVGSh9353wcnseMRkPpEAOY96EEenN7kJVrLqy/EVwtIh3mxcUz8lsFXW1mT5nN5vvEam/a5HiQ==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.26.0", + "@typescript-eslint/typescript-estree": "2.26.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz", + "integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz", + "integrity": "sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-plugin-eslint-plugin": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.2.1.tgz", + "integrity": "sha512-nvmoefIqdFX+skyCt/dN9HaeSNyL8A9UvEtCqCFfJBjKpAR0uRL3SGPLlvDsnfXWtN72G/viowvpA33VjQkGCg==", + "dev": true + }, + "eslint-plugin-mocha": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.3.0.tgz", + "integrity": "sha512-Cd2roo8caAyG21oKaaNTj7cqeYRWW1I2B5SfpKRp0Ip1gkfwoR1Ow0IGlPWnNjzywdF4n+kHL8/9vM6zCJUxdg==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "ramda": "^0.27.0" + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.2.0.tgz", + "integrity": "sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==", + "dev": true, + "requires": { + "estraverse": "^5.0.0" + }, + "dependencies": { + "estraverse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.0.0.tgz", + "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "ramda": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", + "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==", + "dev": true + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } +} diff --git a/extensions/authentication-jwt/package.json b/extensions/authentication-jwt/package.json new file mode 100644 index 000000000000..1b5b16d26e5c --- /dev/null +++ b/extensions/authentication-jwt/package.json @@ -0,0 +1,68 @@ +{ + "name": "@loopback/extension-authentication-jwt", + "version": "0.0.0", + "description": "Extension for the prototype of JWT authentication", + "engines": { + "node": ">=10" + }, + "scripts": { + "build": "lb-tsc", + "clean": "lb-clean loopback-extension-authentication-jwt*.tgz dist tsconfig.build.tsbuildinfo package", + "pretest": "npm run build", + "test": "lb-mocha \"dist/__tests__/unit/*.js\" \"dist/__tests__/acceptance/*.js\"", + "verify": "npm pack && tar xf loopback-extension-authentication-jwt*.tgz && tree package && npm run clean" + }, + "author": "IBM Corp.", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "@loopback/authentication": "^4.1.1", + "@loopback/core": "^2.2.0", + "@loopback/openapi-v3": "^3.1.1", + "@loopback/rest": "^3.1.0", + "@loopback/rest-explorer": "^2.0.2", + "@loopback/security": "^0.2.2", + "@loopback/service-proxy": "^2.0.2", + "@types/bcryptjs": "2.4.2", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^8.5.1" + }, + "devDependencies": { + "@loopback/build": "^5.0.0", + "@loopback/boot": "^2.0.2", + "@loopback/repository": "^2.0.2", + "@loopback/eslint-config": "^6.0.2", + "@loopback/testlab": "^2.0.2", + "@types/lodash": "^4.14.149", + "@types/node": "^10.17.18", + "@typescript-eslint/eslint-plugin": "^2.26.0", + "@typescript-eslint/parser": "^2.26.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.1", + "eslint-plugin-eslint-plugin": "^2.2.1", + "eslint-plugin-mocha": "^6.2.2", + "lodash": "^4.17.15", + "typescript": "~3.8.3" + }, + "keywords": [ + "LoopBack", + "Authentication", + "jsonwebtoken" + ], + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist", + "src", + "!*/__tests__" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git", + "directory": "extensions/health" + } +} diff --git a/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts b/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts new file mode 100644 index 000000000000..90f2959d204d --- /dev/null +++ b/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts @@ -0,0 +1,101 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Client, + createRestAppClient, + expect, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {genSalt, hash} from 'bcryptjs'; +import * as _ from 'lodash'; +import {UserServiceBindings} from '../..'; +import {UserRepository} from '../../repositories'; +import {TestApplication} from '../fixtures/application'; + +describe('jwt authentication', () => { + let app: TestApplication; + let client: Client; + let token: string; + let userRepo: UserRepository; + + before(givenRunningApplication); + before(() => { + client = createRestAppClient(app); + }); + after(async () => { + await app.stop(); + }); + + it(`user login successfully`, async () => { + const credentials = {email: 'jane@doe.com', password: 'opensesame'}; + const res = await client.post('/users/login').send(credentials).expect(200); + token = res.body.token; + }); + + it('whoAmI returns the login user id', async () => { + const res = await client + .get('/whoAmI') + .set('Authorization', 'Bearer ' + token) + .expect(200); + expect(res.text).to.equal('2'); + }); + + /* + ============================================================================ + TEST HELPERS + ============================================================================ + */ + + async function givenRunningApplication() { + app = new TestApplication({ + rest: givenHttpServerConfig(), + }); + + await app.boot(); + userRepo = await app.get(UserServiceBindings.USER_REPOSITORY); + await createUsers(); + await app.start(); + } + + async function createUsers(): Promise { + const hashedPassword = await hashPassword('opensesame', 10); + const users = [ + { + id: 1, + username: 'John', + email: 'john@doe.com', + password: hashedPassword, + }, + { + id: 2, + username: 'Jane', + email: 'jane@doe.com', + password: hashedPassword, + }, + { + id: 3, + username: 'Bob', + email: 'bob@projects.com', + password: hashedPassword, + }, + ]; + + for (const u of users) { + await userRepo.create(_.pick(u, ['id', 'email', 'username'])); + await userRepo + .userCredentials(u.id) + .create({password: u.password, userId: u.id}); + } + } + + async function hashPassword( + password: string, + rounds: number, + ): Promise { + const salt = await genSalt(rounds); + return hash(password, salt); + } +}); diff --git a/extensions/authentication-jwt/src/__tests__/fixtures/application.ts b/extensions/authentication-jwt/src/__tests__/fixtures/application.ts new file mode 100644 index 000000000000..6c7322956980 --- /dev/null +++ b/extensions/authentication-jwt/src/__tests__/fixtures/application.ts @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {AuthenticationComponent} from '@loopback/authentication'; +import {BootMixin} from '@loopback/boot'; +import {ApplicationConfig} from '@loopback/core'; +import {RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; +import {RestExplorerComponent} from '@loopback/rest-explorer'; +import {ServiceMixin} from '@loopback/service-proxy'; +import path from 'path'; +import { + JWTAuthenticationComponent, + SECURITY_SCHEME_SPEC, + UserServiceBindings, +} from '../../'; +import {DbDataSource} from './datasources/db.datasource'; +import {MySequence} from './sequence'; + +export class TestApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options: ApplicationConfig = {}) { + super(options); + + // Set up the custom sequence + this.sequence(MySequence); + + // Set up default home page + this.static('/', path.join(__dirname, '../public')); + + // - enable jwt auth - + // Add security spec (To be done: refactor it to an enhancer) + this.addSecuritySpec(); + // Mount authentication system + this.component(AuthenticationComponent); + // Mount jwt component + this.component(JWTAuthenticationComponent); + // Bind datasource + this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME); + + this.component(RestExplorerComponent); + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = { + controllers: { + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, + }, + repositories: { + dirs: ['repositories'], + extensions: ['.repository.js'], + nested: true, + }, + }; + } + + // - enable jwt auth - + addSecuritySpec(): void { + this.api({ + openapi: '3.0.0', + info: { + title: 'test application', + version: '1.0.0', + }, + paths: {}, + components: {securitySchemes: SECURITY_SCHEME_SPEC}, + security: [ + { + // secure all endpoints with 'jwt' + jwt: [], + }, + ], + servers: [{url: '/'}], + }); + } +} diff --git a/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts b/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts new file mode 100644 index 000000000000..6caeba411819 --- /dev/null +++ b/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts @@ -0,0 +1,97 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT +import { + authenticate, + TokenService, + UserService, +} from '@loopback/authentication'; +import {inject} from '@loopback/core'; +import {get, post, requestBody} from '@loopback/rest'; +import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; +import {TokenServiceBindings, User, UserServiceBindings} from '../../../'; +import {Credentials} from '../../../services/user.service'; + +const CredentialsSchema = { + type: 'object', + required: ['email', 'password'], + properties: { + email: { + type: 'string', + format: 'email', + }, + password: { + type: 'string', + minLength: 8, + }, + }, +}; + +export const CredentialsRequestBody = { + description: 'The input of login function', + required: true, + content: { + 'application/json': {schema: CredentialsSchema}, + }, +}; + +export class UserController { + constructor( + @inject(TokenServiceBindings.TOKEN_SERVICE) + public jwtService: TokenService, + @inject(UserServiceBindings.USER_SERVICE) + public userService: UserService, + @inject(SecurityBindings.USER, {optional: true}) + private user: UserProfile, + ) {} + + @post('/users/login', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }) + async login( + @requestBody(CredentialsRequestBody) credentials: Credentials, + ): Promise<{token: string}> { + // ensure the user exists, and the password is correct + const user = await this.userService.verifyCredentials(credentials); + + // convert a User object into a UserProfile object (reduced set of properties) + const userProfile = this.userService.convertToUserProfile(user); + + // create a JSON Web Token based on the user profile + const token = await this.jwtService.generateToken(userProfile); + + return {token}; + } + + @authenticate('jwt') + @get('/whoAmI', { + responses: { + '200': { + description: '', + schema: { + type: 'string', + }, + }, + }, + }) + async whoAmI(): Promise { + return this.user[securityId]; + } +} diff --git a/extensions/authentication-jwt/src/__tests__/fixtures/datasources/db.datasource.config.json b/extensions/authentication-jwt/src/__tests__/fixtures/datasources/db.datasource.config.json new file mode 100644 index 000000000000..43202b0a3aa8 --- /dev/null +++ b/extensions/authentication-jwt/src/__tests__/fixtures/datasources/db.datasource.config.json @@ -0,0 +1,4 @@ +{ + "name": "db", + "connector": "memory" +} diff --git a/extensions/authentication-jwt/src/__tests__/fixtures/datasources/db.datasource.ts b/extensions/authentication-jwt/src/__tests__/fixtures/datasources/db.datasource.ts new file mode 100644 index 000000000000..99ed459a8402 --- /dev/null +++ b/extensions/authentication-jwt/src/__tests__/fixtures/datasources/db.datasource.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/example-todo +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/core'; +import {juggler} from '@loopback/repository'; +import config from './db.datasource.config.json'; + +export class DbDataSource extends juggler.DataSource { + static dataSourceName = 'db'; + + constructor( + @inject('datasources.config.db', {optional: true}) + dsConfig: object = config, + ) { + super(dsConfig); + } +} diff --git a/extensions/authentication-jwt/src/__tests__/fixtures/sequence.ts b/extensions/authentication-jwt/src/__tests__/fixtures/sequence.ts new file mode 100644 index 000000000000..ef79e59873d7 --- /dev/null +++ b/extensions/authentication-jwt/src/__tests__/fixtures/sequence.ts @@ -0,0 +1,62 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + AuthenticateFn, + AuthenticationBindings, + AUTHENTICATION_STRATEGY_NOT_FOUND, + USER_PROFILE_NOT_FOUND, +} from '@loopback/authentication'; +import {Context, inject} from '@loopback/core'; +import { + FindRoute, + InvokeMethod, + ParseParams, + Reject, + RequestContext, + RestBindings, + Send, + SequenceHandler, +} from '@loopback/rest'; + +const SequenceActions = RestBindings.SequenceActions; + +export class MySequence implements SequenceHandler { + constructor( + @inject(RestBindings.Http.CONTEXT) public ctx: Context, + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) public send: Send, + @inject(SequenceActions.REJECT) public reject: Reject, + // - enable jwt auth - + // inject the auth action + @inject(AuthenticationBindings.AUTH_ACTION) + protected authenticateRequest: AuthenticateFn, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + + // - enable jwt auth - + // call authentication action + await this.authenticateRequest(request); + + const args = await this.parseParams(request, route); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (error) { + if ( + error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || + error.code === USER_PROFILE_NOT_FOUND + ) { + Object.assign(error, {statusCode: 401 /* Unauthorized */}); + } + this.reject(context, error); + } + } +} diff --git a/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts b/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts new file mode 100644 index 000000000000..5d40e0adad34 --- /dev/null +++ b/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {HttpErrors} from '@loopback/rest'; +import {securityId} from '@loopback/security'; +import {expect} from '@loopback/testlab'; +import {JWTService} from '../../'; + +describe('token service', () => { + const USER_PROFILE = { + email: 'test@email.com', + [securityId]: '1', + name: 'test', + }; + // the jwt service only preserves field 'id' and 'name' + const DECODED_USER_PROFILE = { + id: '1', + name: 'test', + }; + const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; + const TOKEN_EXPIRES_IN_VALUE = '60'; + const jwtService = new JWTService(TOKEN_SECRET_VALUE, TOKEN_EXPIRES_IN_VALUE); + + it('token service generateToken() succeeds', async () => { + const token = await jwtService.generateToken(USER_PROFILE); + expect(token).to.not.be.empty(); + }); + + it('token service verifyToken() succeeds', async () => { + const token = await jwtService.generateToken(USER_PROFILE); + const userProfileFromToken = await jwtService.verifyToken(token); + + expect(userProfileFromToken).to.deepEqual(DECODED_USER_PROFILE); + }); + + it('token service verifyToken() fails', async () => { + const expectedError = new HttpErrors.Unauthorized( + `Error verifying token : invalid token`, + ); + const invalidToken = 'aaa.bbb.ccc'; + await expect(jwtService.verifyToken(invalidToken)).to.be.rejectedWith( + expectedError, + ); + }); +}); diff --git a/extensions/authentication-jwt/src/index.ts b/extensions/authentication-jwt/src/index.ts new file mode 100644 index 000000000000..8e07d02f8d24 --- /dev/null +++ b/extensions/authentication-jwt/src/index.ts @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './jwt-authentication-component'; +export * from './keys'; +export * from './models'; +export * from './repositories'; +export * from './services'; diff --git a/extensions/authentication-jwt/src/jwt-authentication-component.ts b/extensions/authentication-jwt/src/jwt-authentication-component.ts new file mode 100644 index 000000000000..6d795ab61bd5 --- /dev/null +++ b/extensions/authentication-jwt/src/jwt-authentication-component.ts @@ -0,0 +1,45 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {registerAuthenticationStrategy} from '@loopback/authentication'; +import { + Application, + Binding, + Component, + CoreBindings, + inject, +} from '@loopback/core'; +import { + TokenServiceBindings, + TokenServiceConstants, + UserServiceBindings, +} from './keys'; +import {UserCredentialsRepository, UserRepository} from './repositories'; +import {MyUserService} from './services'; +import {JWTAuthenticationStrategy} from './services/jwt.auth.strategy'; +import {JWTService} from './services/jwt.service'; + +export class JWTAuthenticationComponent implements Component { + bindings = [ + // token bindings + Binding.bind(TokenServiceBindings.TOKEN_SECRET).to( + TokenServiceConstants.TOKEN_SECRET_VALUE, + ), + Binding.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to( + TokenServiceConstants.TOKEN_EXPIRES_IN_VALUE, + ), + Binding.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService), + + // user bindings + Binding.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService), + Binding.bind(UserServiceBindings.USER_REPOSITORY).toClass(UserRepository), + Binding.bind(UserServiceBindings.USER_CREDENTIALS_REPOSITORY).toClass( + UserCredentialsRepository, + ), + ]; + constructor(@inject(CoreBindings.APPLICATION_INSTANCE) app: Application) { + registerAuthenticationStrategy(app, JWTAuthenticationStrategy); + } +} diff --git a/extensions/authentication-jwt/src/keys.ts b/extensions/authentication-jwt/src/keys.ts new file mode 100644 index 000000000000..ff23c61dee0b --- /dev/null +++ b/extensions/authentication-jwt/src/keys.ts @@ -0,0 +1,36 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {TokenService, UserService} from '@loopback/authentication'; +import {BindingKey} from '@loopback/core'; +import {User} from './models'; +import {Credentials} from './services/user.service'; + +export namespace TokenServiceConstants { + export const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; + export const TOKEN_EXPIRES_IN_VALUE = '21600'; +} + +export namespace TokenServiceBindings { + export const TOKEN_SECRET = BindingKey.create( + 'authentication.jwt.secret', + ); + export const TOKEN_EXPIRES_IN = BindingKey.create( + 'authentication.jwt.expires.in.seconds', + ); + export const TOKEN_SERVICE = BindingKey.create( + 'services.authentication.jwt.tokenservice', + ); +} + +export namespace UserServiceBindings { + export const USER_SERVICE = BindingKey.create>( + 'services.user.service', + ); + export const DATASOURCE_NAME = 'jwtdb'; + export const USER_REPOSITORY = 'repositories.UserRepository'; + export const USER_CREDENTIALS_REPOSITORY = + 'repositories.UserCredentialsRepository'; +} diff --git a/extensions/authentication-jwt/src/models/index.ts b/extensions/authentication-jwt/src/models/index.ts new file mode 100644 index 000000000000..87f489e88cfa --- /dev/null +++ b/extensions/authentication-jwt/src/models/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './user-credentials.model'; +export * from './user.model'; diff --git a/extensions/authentication-jwt/src/models/user-credentials.model.ts b/extensions/authentication-jwt/src/models/user-credentials.model.ts new file mode 100644 index 000000000000..53b26a31160e --- /dev/null +++ b/extensions/authentication-jwt/src/models/user-credentials.model.ts @@ -0,0 +1,44 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property} from '@loopback/repository'; + +@model({settings: {strict: false}}) +export class UserCredentials extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @property({ + type: 'string', + required: true, + }) + password: string; + + @property({ + type: 'number', + required: true, + }) + userId: number; + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface UserCredentialsRelations { + // describe navigational properties here +} + +export type UserCredentialsWithRelations = UserCredentials & + UserCredentialsRelations; diff --git a/extensions/authentication-jwt/src/models/user.model.ts b/extensions/authentication-jwt/src/models/user.model.ts new file mode 100644 index 000000000000..219595a68d5b --- /dev/null +++ b/extensions/authentication-jwt/src/models/user.model.ts @@ -0,0 +1,70 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, hasOne, model, property} from '@loopback/repository'; +import {UserCredentials} from './user-credentials.model'; + +@model({ + settings: { + strict: false, + }, +}) +export class User extends Entity { + // must keep it + @property({ + type: 'number', + id: 1, + generated: false, + updateOnly: true, + }) + id: number; + + @property({ + type: 'string', + }) + realm?: string; + + // must keep it + @property({ + type: 'string', + }) + username?: string; + + // must keep it + @property({ + type: 'string', + required: true, + }) + email: string; + + @property({ + type: 'boolean', + }) + emailVerified?: boolean; + + @property({ + type: 'string', + }) + verificationToken?: string; + + @hasOne(() => UserCredentials) + userCredentials: UserCredentials; + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface UserRelations { + // describe navigational properties here +} + +export type UserWithRelations = User & UserRelations; diff --git a/extensions/authentication-jwt/src/repositories/index.ts b/extensions/authentication-jwt/src/repositories/index.ts new file mode 100644 index 000000000000..ec84dbc609f2 --- /dev/null +++ b/extensions/authentication-jwt/src/repositories/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './user-credentials.repository'; +export * from './user.repository'; diff --git a/extensions/authentication-jwt/src/repositories/user-credentials.repository.ts b/extensions/authentication-jwt/src/repositories/user-credentials.repository.ts new file mode 100644 index 000000000000..5785f95ac55b --- /dev/null +++ b/extensions/authentication-jwt/src/repositories/user-credentials.repository.ts @@ -0,0 +1,22 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/core'; +import {DefaultCrudRepository, juggler} from '@loopback/repository'; +import {UserServiceBindings} from '../keys'; +import {UserCredentials, UserCredentialsRelations} from '../models'; + +export class UserCredentialsRepository extends DefaultCrudRepository< + UserCredentials, + typeof UserCredentials.prototype.id, + UserCredentialsRelations +> { + constructor( + @inject(`datasources.${UserServiceBindings.DATASOURCE_NAME}`) + dataSource: juggler.DataSource, + ) { + super(UserCredentials, dataSource); + } +} diff --git a/extensions/authentication-jwt/src/repositories/user.repository.ts b/extensions/authentication-jwt/src/repositories/user.repository.ts new file mode 100644 index 000000000000..d6c19a39e80f --- /dev/null +++ b/extensions/authentication-jwt/src/repositories/user.repository.ts @@ -0,0 +1,58 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter, inject} from '@loopback/core'; +import { + DefaultCrudRepository, + HasOneRepositoryFactory, + juggler, + repository, +} from '@loopback/repository'; +import {UserServiceBindings} from '../keys'; +import {User, UserCredentials, UserRelations} from '../models'; +import {UserCredentialsRepository} from './user-credentials.repository'; + +export class UserRepository extends DefaultCrudRepository< + User, + typeof User.prototype.id, + UserRelations +> { + public readonly userCredentials: HasOneRepositoryFactory< + UserCredentials, + typeof User.prototype.id + >; + + constructor( + @inject(`datasources.${UserServiceBindings.DATASOURCE_NAME}`) + dataSource: juggler.DataSource, + @repository.getter('UserCredentialsRepository') + protected userCredentialsRepositoryGetter: Getter< + UserCredentialsRepository + >, + ) { + super(User, dataSource); + this.userCredentials = this.createHasOneRepositoryFactoryFor( + 'userCredentials', + userCredentialsRepositoryGetter, + ); + this.registerInclusionResolver( + 'userCredentials', + this.userCredentials.inclusionResolver, + ); + } + + async findCredentials( + userId: typeof User.prototype.id, + ): Promise { + try { + return await this.userCredentials(userId).get(); + } catch (err) { + if (err.code === 'ENTITY_NOT_FOUND') { + return undefined; + } + throw err; + } + } +} diff --git a/extensions/authentication-jwt/src/services/index.ts b/extensions/authentication-jwt/src/services/index.ts new file mode 100644 index 000000000000..9586e8f1ae37 --- /dev/null +++ b/extensions/authentication-jwt/src/services/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './jwt.auth.strategy'; +export * from './jwt.service'; +export * from './security.spec'; +export * from './user.service'; diff --git a/extensions/authentication-jwt/src/services/jwt.auth.strategy.ts b/extensions/authentication-jwt/src/services/jwt.auth.strategy.ts new file mode 100644 index 000000000000..071a5d541a1d --- /dev/null +++ b/extensions/authentication-jwt/src/services/jwt.auth.strategy.ts @@ -0,0 +1,50 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {AuthenticationStrategy, TokenService} from '@loopback/authentication'; +import {inject} from '@loopback/core'; +import {HttpErrors, Request} from '@loopback/rest'; +import {UserProfile} from '@loopback/security'; +import {TokenServiceBindings} from '../keys'; + +export class JWTAuthenticationStrategy implements AuthenticationStrategy { + name = 'jwt'; + + constructor( + @inject(TokenServiceBindings.TOKEN_SERVICE) + public tokenService: TokenService, + ) {} + + async authenticate(request: Request): Promise { + const token: string = this.extractCredentials(request); + const userProfile: UserProfile = await this.tokenService.verifyToken(token); + return userProfile; + } + + extractCredentials(request: Request): string { + if (!request.headers.authorization) { + throw new HttpErrors.Unauthorized(`Authorization header not found.`); + } + + // for example : Bearer xxx.yyy.zzz + const authHeaderValue = request.headers.authorization; + + if (!authHeaderValue.startsWith('Bearer')) { + throw new HttpErrors.Unauthorized( + `Authorization header is not of type 'Bearer'.`, + ); + } + + //split the string into 2 parts : 'Bearer ' and the `xxx.yyy.zzz` + const parts = authHeaderValue.split(' '); + if (parts.length !== 2) + throw new HttpErrors.Unauthorized( + `Authorization header value has too many parts. It must follow the pattern: 'Bearer xx.yy.zz' where xx.yy.zz is a valid JWT token.`, + ); + const token = parts[1]; + + return token; + } +} diff --git a/extensions/authentication-jwt/src/services/jwt.service.ts b/extensions/authentication-jwt/src/services/jwt.service.ts new file mode 100644 index 000000000000..41b6705e23c5 --- /dev/null +++ b/extensions/authentication-jwt/src/services/jwt.service.ts @@ -0,0 +1,77 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {TokenService} from '@loopback/authentication'; +import {inject} from '@loopback/core'; +import {HttpErrors} from '@loopback/rest'; +import {securityId, UserProfile} from '@loopback/security'; +import {promisify} from 'util'; +import {TokenServiceBindings} from '../keys'; + +const jwt = require('jsonwebtoken'); +const signAsync = promisify(jwt.sign); +const verifyAsync = promisify(jwt.verify); + +export class JWTService implements TokenService { + constructor( + @inject(TokenServiceBindings.TOKEN_SECRET) + private jwtSecret: string, + @inject(TokenServiceBindings.TOKEN_EXPIRES_IN) + private jwtExpiresIn: string, + ) {} + + async verifyToken(token: string): Promise { + if (!token) { + throw new HttpErrors.Unauthorized( + `Error verifying token : 'token' is null`, + ); + } + + let userProfile: UserProfile; + + try { + // decode user profile from token + const decodedToken = await verifyAsync(token, this.jwtSecret); + // don't copy over token field 'iat' and 'exp', nor 'email' to user profile + userProfile = Object.assign( + {[securityId]: '', name: ''}, + { + [securityId]: decodedToken.id, + name: decodedToken.name, + id: decodedToken.id, + }, + ); + } catch (error) { + throw new HttpErrors.Unauthorized( + `Error verifying token : ${error.message}`, + ); + } + return userProfile; + } + + async generateToken(userProfile: UserProfile): Promise { + if (!userProfile) { + throw new HttpErrors.Unauthorized( + 'Error generating token : userProfile is null', + ); + } + const userInfoForToken = { + id: userProfile[securityId], + name: userProfile.name, + email: userProfile.email, + }; + // Generate a JSON Web Token + let token: string; + try { + token = await signAsync(userInfoForToken, this.jwtSecret, { + expiresIn: Number(this.jwtExpiresIn), + }); + } catch (error) { + throw new HttpErrors.Unauthorized(`Error encoding token : ${error}`); + } + + return token; + } +} diff --git a/extensions/authentication-jwt/src/services/security.spec.ts b/extensions/authentication-jwt/src/services/security.spec.ts new file mode 100644 index 000000000000..978a965cca3a --- /dev/null +++ b/extensions/authentication-jwt/src/services/security.spec.ts @@ -0,0 +1,18 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ReferenceObject, SecuritySchemeObject} from '@loopback/openapi-v3'; + +export const OPERATION_SECURITY_SPEC = [{jwt: []}]; +export type SecuritySchemeObjects = { + [securityScheme: string]: SecuritySchemeObject | ReferenceObject; +}; +export const SECURITY_SCHEME_SPEC: SecuritySchemeObjects = { + jwt: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, +}; diff --git a/extensions/authentication-jwt/src/services/user.service.ts b/extensions/authentication-jwt/src/services/user.service.ts new file mode 100644 index 000000000000..362782c1a899 --- /dev/null +++ b/extensions/authentication-jwt/src/services/user.service.ts @@ -0,0 +1,65 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/extension-authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {UserService} from '@loopback/authentication'; +import {repository} from '@loopback/repository'; +import {HttpErrors} from '@loopback/rest'; +import {securityId, UserProfile} from '@loopback/security'; +import {compare} from 'bcryptjs'; +import {User} from '../models'; +import {UserRepository} from '../repositories'; + +/** + * A pre-defined type for user credentials. It assumes a user logs in + * using the email and password. You can modify it if your app has different credential fields + */ +export type Credentials = { + email: string; + password: string; +}; + +export class MyUserService implements UserService { + constructor( + @repository(UserRepository) public userRepository: UserRepository, + ) {} + + async verifyCredentials(credentials: Credentials): Promise { + const invalidCredentialsError = 'Invalid email or password.'; + + const foundUser = await this.userRepository.findOne({ + where: {email: credentials.email}, + }); + if (!foundUser) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + const credentialsFound = await this.userRepository.findCredentials( + foundUser.id, + ); + if (!credentialsFound) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + const passwordMatched = await compare( + credentials.password, + credentialsFound.password, + ); + + if (!passwordMatched) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + return foundUser; + } + + convertToUserProfile(user: User): UserProfile { + return { + [securityId]: user.id.toString(), + name: user.username, + id: user.id, + email: user.email, + }; + } +} diff --git a/extensions/authentication-jwt/tsconfig.build.json b/extensions/authentication-jwt/tsconfig.build.json new file mode 100644 index 000000000000..c7b8e49eaca5 --- /dev/null +++ b/extensions/authentication-jwt/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +}