Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jwt auth extension #5046

Merged
merged 1 commit into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/authentication-jwt/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=true
25 changes: 25 additions & 0 deletions extensions/authentication-jwt/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
330 changes: 330 additions & 0 deletions extensions/authentication-jwt/README.md
Original file line number Diff line number Diff line change
@@ -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

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

```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);
}
}
}
```

</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);
jannyHou marked this conversation as resolved.
Show resolved Hide resolved
// Mount jwt component
this.component(JWTAuthenticationComponent);
jannyHou marked this conversation as resolved.
Show resolved Hide resolved
// 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},
jannyHou marked this conversation as resolved.
Show resolved Hide resolved
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._

## 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<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)

## 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`:

<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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a code comment that the value of TokenServiceBindings can be changed too if required. This is something many will definitely want to change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hacksparrow I added a new section called "customization" and it has "customizing model" as the sub section, see https://github.com/strongloop/loopback-next/pull/5046/files#diff-b7edcb305a3224321ad8aea0c9573fb8R181

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
6 changes: 6 additions & 0 deletions extensions/authentication-jwt/index.d.ts
Original file line number Diff line number Diff line change
@@ -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';
6 changes: 6 additions & 0 deletions extensions/authentication-jwt/index.js
Original file line number Diff line number Diff line change
@@ -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');
8 changes: 8 additions & 0 deletions extensions/authentication-jwt/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading