Skip to content

Commit

Permalink
feat: jwt auth
Browse files Browse the repository at this point in the history
Signed-off-by: Nora <[email protected]>
  • Loading branch information
jannyHou committed Dec 19, 2018
1 parent b64982c commit 0747517
Show file tree
Hide file tree
Showing 9 changed files with 674 additions and 156 deletions.
709 changes: 555 additions & 154 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,21 @@
"src"
],
"dependencies": {
"@loopback/authentication": "^1.0.6",
"@loopback/boot": "^1.0.3",
"@loopback/context": "^1.0.1",
"@loopback/core": "^1.0.1",
"@loopback/openapi-v3": "^1.1.0",
"@loopback/repository": "^1.0.3",
"@loopback/rest": "^1.2.0",
"@loopback/service-proxy": "^1.0.1",
"@types/jsonwebtoken": "^8.3.0",
"@types/passport": "^0.4.7",
"bcryptjs": "^2.4.3",
"debug": "^4.1.0",
"express": "^4.16.4",
"isemail": "^3.2.0",
"jsonwebtoken": "^8.4.0",
"lodash": "^4.17.11",
"loopback-connector-kv-redis": "^3.0.0",
"loopback-connector-mongodb": "^3.9.2",
Expand All @@ -78,7 +82,7 @@
"@types/express": "^4.16.0",
"@types/lodash": "^4.14.118",
"@types/mocha": "^5.0.0",
"@types/node": "^10.12.3",
"@types/node": "^10.12.12",
"commitizen": "^3.0.4",
"concurrently": "^4.0.1",
"cz-conventional-changelog": "^2.1.0",
Expand Down
10 changes: 10 additions & 0 deletions src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import {RestApplication} from '@loopback/rest';
import {ServiceMixin} from '@loopback/service-proxy';
import {MySequence} from './sequence';
import * as path from 'path';
import {
AuthenticationBindings,
AuthenticationComponent,
} from '@loopback/authentication';
import {JWTProvider} from './providers';

/**
* Information from package.json
Expand All @@ -29,16 +34,21 @@ export class ShoppingApplication extends BootMixin(
constructor(options?: ApplicationConfig) {
super(options);

this.bind(AuthenticationBindings.STRATEGY).toProvider(JWTProvider);

// Bind package.json to the application context
this.bind(PackageKey).to(pkg);

this.component(AuthenticationComponent);

// Set up the custom sequence
this.sequence(MySequence);

// Set up default home page
this.static('/', path.join(__dirname, '../../public'));

this.projectRoot = __dirname;

// Customize @loopback/boot Booter Conventions here
this.bootOptions = {
controllers: {
Expand Down
4 changes: 4 additions & 0 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {promisify} from 'util';
import * as isemail from 'isemail';
import {RecommenderService} from '../services/recommender.service';
import {inject} from '@loopback/core';
import {
authenticate,
} from '@loopback/authentication';

const hashAsync = promisify(hash);

Expand Down Expand Up @@ -59,6 +62,7 @@ export class UserController {
},
},
})
@authenticate('jwt')
async findById(@param.path.string('userId') userId: string): Promise<User> {
return this.userRepository.findById(userId, {
fields: {password: false},
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export {ShoppingApplication, PackageInfo, PackageKey} from './application';
export async function main(options?: ApplicationConfig) {
const app = new ShoppingApplication(options);

// app.component(AuthenticationComponent);

await app.boot();
await app.start();

Expand Down
66 changes: 66 additions & 0 deletions src/providers/JWT.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {Provider, ValueOrPromise} from '@loopback/context';
const jwt = require('jsonwebtoken');
import {promisify} from 'util';
import {Request} from '@loopback/rest';
import {
AuthenticateFn,
UserProfile,
AuthenticationMetadata,
AuthenticationBindings,
} from '@loopback/authentication';
import {inject} from '@loopback/context';

const signAsync = promisify(jwt.sign);
const verifyAsync = promisify(jwt.verify);
// Consider turn it to a binding
const SECRET = 'secretforjwt';

// NOTE: any improvement to not using undefined?

export class JWTProvider implements Provider<AuthenticateFn | undefined> {
constructor(
@inject(AuthenticationBindings.METADATA)
private metadata: AuthenticationMetadata,
) {}
value(): ValueOrPromise<AuthenticateFn | undefined> {
// NOTE: there should be a function that maps the metadata.strategy to the corresponding provider
// the logic below shouldn't happen in the provider's value()
if (!this.metadata) {
return undefined;
}

const name = this.metadata.strategy;
if (name === 'jwt') {
return req => this.verify(req);
} else {
return Promise.reject(`The strategy ${name} is not available.`);
}
}
async verify(request: Request): Promise<UserProfile | undefined> {
// process.nextTick(() => {
// users.find(username, password, cb);
// });

// A mock for sign in
const payload = {admin: true};
await signAsync(payload, SECRET, {expiresInMinutes: 5});
// const token =
// request.body.token ||
// request.query.token ||
// request.headers['x-access-token'];
const token = 'not the right token';

if (token) {
try {
await verifyAsync(token, SECRET);
} catch (err) {
if (err) return Promise.reject('Authentication failed!');
}
}
// should we return some meaningful message?
return;
}
}
// server
// .bind(AuthenticationBindings.STRATEGY)
// .toProvider(MyPassportStrategyProvider);
1 change: 1 addition & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './JWT.provider';
4 changes: 4 additions & 0 deletions src/sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Send,
SequenceHandler,
} from '@loopback/rest';
import {AuthenticationBindings, AuthenticateFn} from '@loopback/authentication';

const SequenceActions = RestBindings.SequenceActions;

Expand All @@ -24,12 +25,15 @@ export class MySequence implements SequenceHandler {
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
@inject(AuthenticationBindings.AUTH_ACTION)
protected authenticateRequest: AuthenticateFn,
) {}

async handle(context: RequestContext) {
try {
const {request, response} = context;
const route = this.findRoute(request);
await this.authenticateRequest(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
Expand Down
28 changes: 27 additions & 1 deletion test/acceptance/user.controller.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
// License text available at https://opensource.org/licenses/MIT

import {Client, expect} from '@loopback/testlab';
import {Response} from 'supertest';
import {ShoppingApplication} from '../..';
import {UserRepository, OrderRepository} from '../../src/repositories';
import {MongoDataSource} from '../../src/datasources';
import {setupApplication} from './helper';
import {createRecommendationServer} from '../../recommender';
import {Server} from 'http';
import {Server, ServerResponse} from 'http';
import {authenticate} from '@loopback/authentication';
const recommendations = require('../../recommender/recommendations.json');

describe('UserController', () => {
Expand Down Expand Up @@ -110,6 +112,30 @@ describe('UserController', () => {
await client.get(`/users/${newUser.id}`).expect(200, newUser.toJSON());
});

it.skip('returns a user with given id only when that user logins in', async () => {
const newUser = await userRepo.create(user);
const auth = {} as {token: string};
delete newUser.password;
delete newUser.orders;
// MongoDB returns an id object we need to convert to string
// since the REST API returns a string for the id property.
newUser.id = newUser.id.toString();
await client
.post('/Users/login')
.send({username: 'the-username', password: 'the-password'})
.expect(200)
.end(onResponse);

await client
.get(`/users/${newUser.id}`)
.set('Authorization', 'bearer ' + auth.token)
.expect(200, newUser.toJSON());

function onResponse(err: Error, res: Response) {
auth.token = res.body.token;
}
});

describe('user product recommendation (service) api', () => {
// tslint:disable-next-line:no-any
let recommendationService: Server;
Expand Down

0 comments on commit 0747517

Please sign in to comment.