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: enable swagger-ui token #267

Closed
wants to merge 4 commits into from
Closed
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
123 changes: 115 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,6 @@ This app has five services:
5. `services/jwt-service` - responsible for generating and verifying JSON Web
Token.

## Authentication

_Note: This app contains a `login` endpoint for the purpose of spike and demo,
the authentication for the CRUD operations and navigational endpoints of model
User is still in progress._

### Login

The endpoint for logging in a user is a `POST` request to `/users/login`.
Expand All @@ -110,13 +104,126 @@ possible by the use of the `UserService` service provided by
You can see the details in
[`packages/shopping/src/controllers/user.controller.ts`](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts).

### Tutorial
## Authentication

### Enabling Auth Token with Swagger-ui

`swagger-ui` module is built with authorization component, which will show up by
adding the security schema and operation security spec in the OpenAPI spec.

You can check the OpenAPI
[doc](https://swagger.io/docs/specification/authentication/bearer-authentication/)
to learn how to add it, see section "Describing Bearer Authentication".

As a demo, the security related specs are hardcoded and merged into the
application's OpenAPI spec in the main file in this spike.

### Setting token

_Should be moved to
https://loopback.io/doc/en/lb4/Authentication-Tutorial.html#try-it-out_

After creating a user, you can login with `email`and `password`:

![login](/imgs/login.png)

A JWT token will be generated and returned in the response body, you can copy
the token for setting the bearer header in the next step:

![get-token.png](/imgs/get-token.png)

Then set the token for every request's Bearer header. You will find a green
button called "Authorize" on the right corner of the explorer:

![authorize-button](/imgs/authorize-button.png)

Click it and the token set dialog will be prompted:

![set-token](/imgs/set-token.png)

Paste the token you just copied in the field, then click "Authorize". The token
will be hidden:

![after-set-token](/imgs/after-set-token.png)

Now you can try endpoint like `GET/users/me` to verify that the logged in user
is injected in the request:

![me](/imgs/me.png)

### Operation Level Security Policy

You can also specify security policy for a single endpoint, by adding the
security spec in the operation decorator, like:

```ts
// Define your operation level security policy
const SECURITY_SPEC_OPERATION = {
[{basicAuth: []}];
}

// Also add the corresponding security schema into `components.securitySchemas`
const SECURITY_SCHEMA_SPEC = {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
basicAuth: {
type: 'http',
scheme: 'basic',
},
},
};

// Add security policy in the operation decorator
@get('/users/{userId}', {
security: SECURITY_SPEC_OPERATION,
responses: RESPONSE_SPEC,
})
async findById(@param.path.string('userId') userId: string): Promise<User> {
// Print out the header, you will see the Basic auth string in the header
console.log(`header ${inspect(this.request.headers)}`);
// business logic
}
```

The "Authorize" dialog will show up your new added entry as:

![operation-level-security](/imgs/operation-level-security.png)

Provide the username and password to login:

![set-basic-auth](/imgs/set-basic-auth.png)

Try endpoint `GET users/{userId}`, the printed header contains the basic auth
string:

![basic-auth-header](/imgs/basic-auth-header.png)

### Follow-up Stories

As you could find in the `security-spec.ts` file, security related spec is
hardcoded now and is manually merged into the openapi spec in the main file
`index.ts`, to enable the token set more automatically, there are 3 things we
could improve:

- The security schema could be contributed by the authentication strategy, see
[story#3669](https://github.com/strongloop/loopback-next/issues/3669)
- This spike sets a global security policy for all the endpoints, while the
policy spec can be set on the operation level, it's also documented in
[swagger/authentication/bearer-authentication](https://swagger.io/docs/specification/authentication/bearer-authentication/).
This can be achieved by using `@api()`(controller class level) or
`@authenticate()`(controller method level)

## Tutorial

There is a tutorial which shows how to apply the JWT strategy to secure your
endpoint with `@loopback/[email protected]`. You can check more details in
https://loopback.io/doc/en/lb4/Authentication-Tutorial.html

### Trying It Out
## Trying It Out

Please check the
[try it out](https://loopback.io/doc/en/lb4/Authentication-Tutorial.html#try-it-out)
Expand Down
Binary file added imgs/after-set-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/authorize-button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/basic-auth-header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/get-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/me.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/operation-level-security.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/set-basic-auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/set-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion packages/shopping/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import {BootMixin} from '@loopback/boot';
import {ApplicationConfig, BindingKey} from '@loopback/core';
import {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {RestApplication, RestBindings} from '@loopback/rest';
import {ServiceMixin} from '@loopback/service-proxy';
import {MyAuthenticationSequence} from './sequence';
import {
Expand All @@ -29,6 +29,7 @@ import {
import {PasswordHasherBindings} from './keys';
import {BcryptHasher} from './services/hash.password.bcryptjs';
import {JWTAuthenticationStrategy} from './authentication-strategies/jwt-strategy';
import {SECURITY_SPEC, SECURITY_SCHEME_SPEC} from './utils/security-spec';

/**
* Information from package.json
Expand All @@ -47,6 +48,13 @@ export class ShoppingApplication extends BootMixin(
) {
constructor(options?: ApplicationConfig) {
super(options);
this.api({
openapi: '3.0.0',
info: {title: pkg.name, version: pkg.version},
paths: {},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

servers: [{url: '/'}]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

components: {securitySchemes: SECURITY_SCHEME_SPEC},
security: SECURITY_SPEC,
});

this.setUpBindings();

Expand Down Expand Up @@ -99,4 +107,8 @@ export class ShoppingApplication extends BootMixin(

this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService);
}

async start(): Promise<void> {
await super.start();
}
}
18 changes: 16 additions & 2 deletions packages/shopping/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@

import {repository} from '@loopback/repository';
import {validateCredentials} from '../services/validator';
import {post, param, get, requestBody, HttpErrors} from '@loopback/rest';
import {
post,
param,
get,
requestBody,
HttpErrors,
RestBindings,
Request,
} from '@loopback/rest';
import {User, Product} from '../models';
import {UserRepository} from '../repositories';
import {RecommenderService} from '../services/recommender.service';
import {inject} from '@loopback/core';
import {inject, Context} from '@loopback/core';
import {
authenticate,
UserProfile,
Expand All @@ -23,13 +31,15 @@ import {
} from './specs/user-controller.specs';
import {Credentials} from '../repositories/user.repository';
import {PasswordHasher} from '../services/hash.password.bcryptjs';
import {inspect} from 'util';

import {
TokenServiceBindings,
PasswordHasherBindings,
UserServiceBindings,
} from '../keys';
import * as _ from 'lodash';
import {SECURITY_SPEC_OPERATION} from '../utils/security-spec';

export class UserController {
constructor(
Expand All @@ -42,6 +52,8 @@ export class UserController {
public jwtService: TokenService,
@inject(UserServiceBindings.USER_SERVICE)
public userService: UserService<User, Credentials>,
@inject(RestBindings.Http.CONTEXT) public ctx: Context,
@inject(RestBindings.Http.REQUEST) private request: Request,
) {}

@post('/users', {
Expand Down Expand Up @@ -83,6 +95,7 @@ export class UserController {
}

@get('/users/{userId}', {
security: SECURITY_SPEC_OPERATION,
Copy link
Member

Choose a reason for hiding this comment

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

IIUC, this is configuring basicAuth schema. Is that intentional? I thought that we use bearerAuth schema everywhere. Or is this just for the spike?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bajtos Oh this is just a demo, for spike only, I just want to verify that a operation level security policy(which is different than the global bearerAuth) works.

I thought that we use bearerAuth schema everywhere

Exactly 💯

responses: {
'200': {
description: 'User',
Expand All @@ -97,6 +110,7 @@ export class UserController {
},
})
async findById(@param.path.string('userId') userId: string): Promise<User> {
console.log(`header ${inspect(this.request.headers)}`);
return this.userRepository.findById(userId, {
fields: {password: false},
});
Expand Down
1 change: 1 addition & 0 deletions packages/shopping/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import {ShoppingApplication} from './application';
import {ApplicationConfig} from '@loopback/core';
import {RestBindings} from '@loopback/rest';
export {ShoppingApplication, PackageInfo, PackageKey} from './application';

export async function main(options?: ApplicationConfig) {
Expand Down
18 changes: 18 additions & 0 deletions packages/shopping/src/utils/security-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SecuritySchemeObject, ReferenceObject } from "@loopback/openapi-v3";

export const SECURITY_SPEC = [{bearerAuth: []}];
export const SECURITY_SPEC_OPERATION = [{basicAuth: []}];
export type SecuritySchemeObjects = {
[securityScheme: string]: SecuritySchemeObject | ReferenceObject;
}
export const SECURITY_SCHEME_SPEC: SecuritySchemeObjects = {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
basicAuth: {
type: 'http',
scheme: 'basic',
},
};