diff --git a/package-lock.json b/package-lock.json index ef7bb64fa..af0e5cfc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,23 +72,6 @@ "integrity": "sha512-WXKf5K5HT6X0kKiCOezJZFljsfxKV1FpU8Tf1A7ZpGvyd/Q4hlrJm2EwoH2onaUq3O4tLDp+4gk0hHPsMyxmOg==", "dev": true }, - "@babel/runtime": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.1.5.tgz", - "integrity": "sha512-xKnPpXG/pvK1B90JkwwxSGii90rQGKtzcMt2gI5G6+M0REXaq6rOHsGC2ay6/d0Uje7zzvSzjEzfR3ENhFlrfA==", - "dev": true, - "requires": { - "regenerator-runtime": "0.12.1" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", - "dev": true - } - } - }, "@babel/template": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", @@ -630,7 +613,7 @@ "@types/supertest": "2.0.7", "express": "4.16.4", "fs-extra": "7.0.1", - "oas-validator": "2.0.2", + "oas-validator": "2.0.1", "shot": "4.0.7", "should": "13.2.3", "sinon": "7.2.2", @@ -687,7 +670,6 @@ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "dev": true, "requires": { - "co": "4.6.0", "fast-deep-equal": "1.1.0", "fast-json-stable-stringify": "2.0.0", "json-schema-traverse": "0.3.1" @@ -806,7 +788,6 @@ "dev": true, "requires": { "js-yaml": "3.12.0", - "node-fetch-h2": "2.3.0", "oas-kit-common": "1.0.6", "reftools": "1.0.4", "yargs": "12.0.5" @@ -825,7 +806,6 @@ "dev": true, "requires": { "ajv": "5.5.2", - "better-ajv-errors": "0.5.7", "js-yaml": "3.12.0", "oas-kit-common": "1.0.6", "oas-linter": "2.0.1", @@ -1421,34 +1401,6 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, - "better-ajv-errors": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.5.7.tgz", - "integrity": "sha512-O7tpXektKWVwYCH5g6Vs3lKD+sJs7JHh5guapmGJd+RTwxhFZEf4FwvbHBURUnoXsTeFaMvGuhTTmEGiHpNi6w==", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0", - "@babel/runtime": "7.1.5", - "chalk": "2.4.1", - "core-js": "2.5.7", - "json-to-ast": "2.0.5", - "jsonpointer": "4.0.1", - "leven": "2.1.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.5.0" - } - } - } - }, "bl": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.0.1.tgz", @@ -1692,12 +1644,6 @@ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz", "integrity": "sha512-21O0kGmvED5OJ7ZTdqQ5lQQ+sjuez33R+d35jZKLwqUb5mqcPHUsxOSzj61+LHVtxGZd1kShbQM3MjB/gBJkVg==" }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2948,12 +2894,6 @@ "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.3.1.tgz", "integrity": "sha512-PcI9NUm6EUOhHlaxYABCqDQQWS7IgoBZ/PmPkhuzj+oR01ffjv3EJfKnnWJZcUhILtUh6/NdJi1Zs/mIr6v8DA==" }, - "http2-client": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.1.tgz", - "integrity": "sha512-3oWAo1MOb8VlQ26wxydOP7cD4Kt0KAtMZhTrl2HWjBtiLCQ80zgr1mvt6iNyKiVTN2wHwOP290b8KfErNcVZlg==", - "dev": true - }, "husky": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/husky/-/husky-1.1.3.tgz", @@ -3558,12 +3498,6 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, - "json-to-ast": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.0.5.tgz", - "integrity": "sha512-rKTfvT5WWXCjzs1MThg54ExXauw02FthhNofb9YJFXc/MkYgE865wqMYJmLBLMqQqdWPRDvU2bSOb81uIMMWOg==", - "dev": true - }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -3584,12 +3518,6 @@ "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.18.0.tgz", "integrity": "sha512-YCLSH4SkHbNCNfJ/IeBGmKwPaiFxTZU011F5MV0UcC+O+te7fiwOQMU2ZYFCkqkH1VkhFRKxftoODbRd7YafeA==" }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, "jsonwebtoken": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz", @@ -3653,12 +3581,6 @@ "invert-kv": "2.0.0" } }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, "linkify-it": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", @@ -4399,15 +4321,6 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, - "node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "dev": true, - "requires": { - "http2-client": "1.3.1" - } - }, "nopt": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz", diff --git a/src/application.ts b/src/application.ts index f683d4d85..1e51076e6 100644 --- a/src/application.ts +++ b/src/application.ts @@ -14,8 +14,10 @@ import { AuthenticationBindings, AuthenticationComponent, } from '@loopback/authentication'; +import {JWTAuthenticationBindings} from './keys'; import {StrategyResolverProvider} from './providers/strategy.resolver.provider'; import {AuthenticateActionProvider} from './providers/custom.authentication.provider'; +import {JWTAuthenticationServiceProvider} from './services/JWT.authentication.service'; /** * Information from package.json @@ -39,12 +41,18 @@ export class ShoppingApplication extends BootMixin( this.bind(PackageKey).to(pkg); this.component(AuthenticationComponent); + + // The following bindings is an imply this.bind(AuthenticationBindings.AUTH_ACTION).toProvider( AuthenticateActionProvider, ); this.bind(AuthenticationBindings.STRATEGY).toProvider( StrategyResolverProvider, ); + this.bind(JWTAuthenticationBindings.SECRET).to('secretforjwt'); + this.bind(JWTAuthenticationBindings.SERVICE).toProvider( + JWTAuthenticationServiceProvider, + ); // Set up the custom sequence this.sequence(MySequence); diff --git a/src/authentication-strategies/JWT.strategy.ts b/src/authentication-strategies/JWT.strategy.ts index 4aca7f3dd..b33649ad8 100644 --- a/src/authentication-strategies/JWT.strategy.ts +++ b/src/authentication-strategies/JWT.strategy.ts @@ -3,17 +3,19 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -const jwt = require('jsonwebtoken'); -import {promisify} from 'util'; -const verifyAsync = promisify(jwt.verify); // Consider turn it to a binding const SECRET = 'secretforjwt'; import {Request, HttpErrors} from '@loopback/rest'; import {UserProfile} from '@loopback/authentication'; -import * as _ from 'lodash'; import {AuthenticationStrategy} from './authentication.strategy'; +import {inject} from '@loopback/core'; +import {JWTAuthenticationService} from '../services/JWT.authentication.service'; export class JWTStrategy implements AuthenticationStrategy { + constructor( + @inject('JWT.authentication.service') + public jwtAuthService: JWTAuthenticationService, + ) {} async authenticate(request: Request): Promise { let token = request.query.access_token || request.headers['authorization']; if (!token) throw new HttpErrors.Unauthorized('No access token found!'); @@ -23,10 +25,7 @@ export class JWTStrategy implements AuthenticationStrategy { } try { - const decoded = await verifyAsync(token, SECRET); - let user = _.pick(decoded, ['id', 'email', 'firstName']); - (user as UserProfile).name = user.firstName; - delete user.firstName; + const user = await this.jwtAuthService.decodeAccessToken(token); return user; } catch (err) { Object.assign(err, { diff --git a/src/authentication-strategies/keys.ts b/src/authentication-strategies/keys.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 7989cc149..961a65d5b 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -17,10 +17,7 @@ import { AuthenticationBindings, } from '@loopback/authentication'; import {Credentials} from '../repositories/user.repository'; -import { - validateCredentials, - getAccessTokenForUser, -} from '../utils/user.authentication'; +import {JWTAuthenticationService} from '../services/JWT.authentication.service'; import * as isemail from 'isemail'; const hashAsync = promisify(hash); @@ -41,6 +38,8 @@ export class UserController { @repository(UserRepository) public userRepository: UserRepository, @inject('services.RecommenderService') public recommender: RecommenderService, + @inject('JWT.authentication.service') + public jwtAuthService: JWTAuthenticationService, @inject.setter(AuthenticationBindings.CURRENT_USER) public setCurrentUser: Setter, ) {} @@ -157,8 +156,8 @@ export class UserController { async login( @requestBody() credentials: Credentials, ): Promise<{token: string}> { - validateCredentials(credentials); - const token = await getAccessTokenForUser(this.userRepository, credentials); + this.jwtAuthService.validateCredentials(credentials); + const token = await this.jwtAuthService.getAccessTokenForUser(credentials); return {token}; } } diff --git a/src/keys.ts b/src/keys.ts new file mode 100644 index 000000000..a5ffcf51b --- /dev/null +++ b/src/keys.ts @@ -0,0 +1,9 @@ +import {BindingKey} from '@loopback/context'; +import {JWTAuthenticationService} from './services/JWT.authentication.service'; + +export namespace JWTAuthenticationBindings { + export const SECRET = BindingKey.create('JWT.authentication.secret'); + export const SERVICE = BindingKey.create( + 'JWT.authentication.service', + ); +} diff --git a/src/services/JWT.authentication.service.ts b/src/services/JWT.authentication.service.ts new file mode 100644 index 000000000..272c394de --- /dev/null +++ b/src/services/JWT.authentication.service.ts @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2018, 2019. All Rights Reserved. +// Node module: @loopback4-example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as _ from 'lodash'; +import {Credentials, UserRepository} from '../repositories/user.repository'; +import {toJSON} from '@loopback/testlab'; +import {promisify} from 'util'; +import * as isemail from 'isemail'; +import {HttpErrors} from '@loopback/rest'; +import {UserProfile} from '@loopback/authentication'; +import {inject, ValueOrPromise, Provider} from '@loopback/core'; +import {repository} from '@loopback/repository'; +const jwt = require('jsonwebtoken'); +const signAsync = promisify(jwt.sign); +const verifyAsync = promisify(jwt.verify); + +export class JWTAuthenticationService { + constructor( + public userRepository: UserRepository, + protected jwt_secret: string, + ) {} + + async getAccessTokenForUser(credentials: Credentials): Promise { + const foundUser = await this.userRepository.findOne({ + where: {email: credentials.email, password: credentials.password}, + }); + if (!foundUser) { + throw new HttpErrors.Unauthorized('Wrong credentials!'); + } + + const currentUser = _.pick(toJSON(foundUser), ['id', 'email', 'firstName']); + + // Generate user token using JWT + const token = await signAsync(currentUser, this.jwt_secret, { + expiresIn: 300, + }); + + return token; + } + + validateCredentials(credentials: Credentials) { + // Validate Email + if (!isemail.validate(credentials.email)) { + throw new HttpErrors.UnprocessableEntity('invalid email'); + } + + // Validate Password Length + if (credentials.password.length < 8) { + throw new HttpErrors.UnprocessableEntity( + 'password must be minimum 8 characters', + ); + } + } + + async decodeAccessToken(token: string): Promise { + const decoded = await verifyAsync(token, this.jwt_secret); + let user = _.pick(decoded, ['id', 'email', 'firstName']); + (user as UserProfile).name = user.firstName; + delete user.firstName; + return user; + } +} + +// Error: +// A class can only implement an identifier/qualified-name with +// optional type arguments. [2500] + +// Does service have to be a provider? +export class JWTAuthenticationServiceProvider + implements Provider() { + constructor( + @repository(UserRepository) public userRepository: UserRepository, + @inject('JWT.authentication.secret') protected jwt_secret: string, + ) {} + value(): ValueOrPromise { + return new JWTAuthenticationService(this.userRepository, this.jwt_secret); + } +} diff --git a/src/utils/user.authentication.ts b/src/utils/user.authentication.ts deleted file mode 100644 index 8c5f8e7e2..000000000 --- a/src/utils/user.authentication.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright IBM Corp. 2018, 2019. All Rights Reserved. -// Node module: @loopback4-example-shopping -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import * as _ from 'lodash'; -import {Credentials, UserRepository} from '../repositories/user.repository'; -import {toJSON} from '@loopback/testlab'; -import {promisify} from 'util'; -import * as isemail from 'isemail'; -import {HttpErrors} from '@loopback/rest'; -const jwt = require('jsonwebtoken'); -const signAsync = promisify(jwt.sign); - -export async function getAccessTokenForUser( - userRepository: UserRepository, - credentials: Credentials, -): Promise { - const foundUser = await userRepository.findOne({ - where: {email: credentials.email, password: credentials.password}, - }); - if (!foundUser) { - throw new HttpErrors.Unauthorized('Wrong credentials!'); - } - - const currentUser = _.pick(toJSON(foundUser), ['id', 'email', 'firstName']); - - // Generate user token using JWT - const token = await signAsync(currentUser, 'secretforjwt', { - expiresIn: 300, - }); - - return token; -} - -export function validateCredentials(credentials: Credentials) { - // Validate Email - if (!isemail.validate(credentials.email)) { - throw new HttpErrors.UnprocessableEntity('invalid email'); - } - - // Validate Password Length - if (credentials.password.length < 8) { - throw new HttpErrors.UnprocessableEntity( - 'password must be minimum 8 characters', - ); - } -} diff --git a/tsconfig.json b/tsconfig.json index 1dd28f8c4..ff1c4e955 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,10 @@ "**/*.d.ts" ], "compilerOptions": { - "lib": ["es2018", "dom", "esnext.asynciterable"] + "lib": ["es2018", "dom", "esnext.asynciterable"], + "compilorOptions": { + "experimentalDecorators": true, + "allowJs": true + } } }