Skip to content

Commit

Permalink
feat: create jwt auth service
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Jan 25, 2019
1 parent 3cbb0a8 commit b7c5bd0
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 151 deletions.
89 changes: 1 addition & 88 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
15 changes: 7 additions & 8 deletions src/authentication-strategies/JWT.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserProfile | undefined> {
let token = request.query.access_token || request.headers['authorization'];
if (!token) throw new HttpErrors.Unauthorized('No access token found!');
Expand All @@ -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, {
Expand Down
Empty file.
11 changes: 5 additions & 6 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<UserProfile>,
) {}
Expand Down Expand Up @@ -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};
}
}
9 changes: 9 additions & 0 deletions src/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {BindingKey} from '@loopback/context';
import {JWTAuthenticationService} from './services/JWT.authentication.service';

export namespace JWTAuthenticationBindings {
export const SECRET = BindingKey.create<string>('JWT.authentication.secret');
export const SERVICE = BindingKey.create<JWTAuthenticationService>(
'JWT.authentication.service',
);
}
80 changes: 80 additions & 0 deletions src/services/JWT.authentication.service.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<UserProfile> {
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<JWTAuthenticationService>() {
constructor(
@repository(UserRepository) public userRepository: UserRepository,
@inject('JWT.authentication.secret') protected jwt_secret: string,
) {}
value(): ValueOrPromise<JWTAuthenticationService> {
return new JWTAuthenticationService(this.userRepository, this.jwt_secret);
}
}
Loading

0 comments on commit b7c5bd0

Please sign in to comment.