Skip to content

Commit

Permalink
feat: extract model and repo
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Apr 17, 2020
1 parent a692c58 commit 9db549a
Show file tree
Hide file tree
Showing 23 changed files with 381 additions and 70 deletions.
4 changes: 2 additions & 2 deletions extensions/authentication-jwt/LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Copyright (c) IBM Corp. 2019. All Rights Reserved.
Node module: @loopback/extension-health
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.

--------
Expand Down
300 changes: 279 additions & 21 deletions extensions/authentication-jwt/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,298 @@
# @loopback/extension-authentication-jwt

This module exports the jwt authentication strategy and its corresponding token
service as a component. You can mount the component to get a prototype token
based authentication system in your LoopBack 4 application.
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

This module contains an example LoopBack 4 application in folder `fixtures`. It
contains:
To use this component, you need to have an existing LoopBack 4 application and a
datasource in it for persistency.

- a `User` and a `UserCredentials` model.
- a user service that contains user utility functions like validating
credentials.
- tip: the user service is not extracted into the jwt component because it
couples with model `User` and `UserCredentials`.
- a controller with endpoints `/login` and `/whoAmI`.
- create app: run `lb4 app`
- create datasource: run `lb4 datasource`

To mount the jwt component in the example, you can add this code in file
`application.ts`:
Next enable the jwt authentication system in your application:

- add authenticate action

<details>
<summary><strong>Check The Code</strong></summary>
<p>

```ts
// Assume you already enabled the authentication system
// in the sequence and application file.
this.component(JWTAuthenticationComponent);

// You will find the current example has an extra function to
// merge the security spec into the application.
// This will be improved with a coming enhancer.
// See section [To Be Done](#to-be-done)
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);
}
}
}
```

</p>
</details>

- mount jwt component in application

<details>
<summary><strong>Check The Code</strong></summary>
<p>

```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: '/'}],
});
}
}
```

</p>
</details>

_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._

## To Be Done
## Add 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<string> {
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)

## Customize Model

1. Create your own user model and repository by `lb4 model` and
`lb4 repository`.

2. The user service requires the user model, to provide your own one, you need
to copy the
[`user.service.ts`](https://github.com/strongloop/loopback-next/blob/master/extensions/authentication-jwt/src/services/user.service.ts)
file into your application, e.g. copy to file
`myapp/src/services/custom.user.service.ts`, and replace the default `User`,
`UserRepository` with `MyUser`, `MyUserRepository`:

<details>
<summary><strong>Check The Code</strong></summary>
<p>

```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<MyUser, Credentials> {
constructor(
// UserRepository --> MyUserRepository
@repository(MyUserRepository) public userRepository: MyUserRepository,
) {}
// User --> MyUser
async verifyCredentials(credentials: Credentials): Promise<MyUser> {
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,
};
}
}
```

</p>
</details>

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
Expand Down
4 changes: 2 additions & 2 deletions extensions/authentication-jwt/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/extension-health
// 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

Expand Down
4 changes: 2 additions & 2 deletions extensions/authentication-jwt/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/extension-health
// 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

Expand Down
4 changes: 2 additions & 2 deletions extensions/authentication-jwt/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/extension-health
// 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
} from '@loopback/testlab';
import {genSalt, hash} from 'bcryptjs';
import * as _ from 'lodash';
import {UserServiceBindings} from '../..';
import {UserRepository} from '../../repositories';
import {TestApplication} from '../fixtures/application';
import {UserRepository} from '../fixtures/repositories';

describe('jwt authentication', () => {
let app: TestApplication;
Expand Down Expand Up @@ -54,7 +55,7 @@ describe('jwt authentication', () => {
});

await app.boot();
userRepo = await app.get('repositories.UserRepository');
userRepo = await app.get(UserServiceBindings.USER_REPOSITORY);
await createUsers();
await app.start();
}
Expand Down
Loading

0 comments on commit 9db549a

Please sign in to comment.