Skip to content

Commit

Permalink
feat(core): add component to authenticate swagger ui access (sourcefu…
Browse files Browse the repository at this point in the history
  • Loading branch information
akshatdubeysf authored Jan 28, 2022
1 parent b46815e commit 40f669f
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@loopback/openapi-v3": "^7.0.0",
"@loopback/repository": "^4.0.0",
"@loopback/rest": "^11.0.0",
"@loopback/rest-explorer": "^4.0.1",
"@loopback/service-proxy": "^4.0.0",
"casbin": "^5.11.5",
"i18n": "^0.13.3",
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {configure} from 'i18n';

import {LocaleKey} from './enums';
import {SFCoreBindings, OASBindings} from './keys';
import {LoggerExtensionComponent} from './components';
import {
LoggerExtensionComponent,
SwaggerAuthenticationComponent,
} from './components';
import {CoreConfig} from './types';
import {Loopback4HelmetComponent} from 'loopback4-helmet';
import {RateLimiterComponent} from 'loopback4-ratelimiter';
Expand Down Expand Up @@ -57,6 +60,10 @@ export class CoreComponent implements Component {
middlewares.push(swStatsMiddleware);
}

if (this.coreConfig?.authenticateSwaggerUI) {
this.application.component(SwaggerAuthenticationComponent);
}

// Configure locale provider
if (this.coreConfig?.configObject) {
configure({...this.coreConfig.configObject, register: this.localeObj});
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './bearer-verifier';
export * from './logger-extension';
export * from './swagger-authentication';
31 changes: 31 additions & 0 deletions packages/core/src/components/swagger-authentication/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# SwaggerAuthentication Component

## Overview

A Loopback Component that adds an authenticating middleware for Rest Explorer

### Installation

```bash

npm i @sourceloop/authentication-service

```

### Usage

- Create a new Loopback4 Application (If you don't have one already)
`lb4 testapp`
- Install the authentication service
`npm i @sourceloop/core`
- Configure `@sourceloop/core` component to include `SwaggerAuthenticateComponent` -
```typescript
this.bind(SFCoreBindings.config).to({
authenticateSwaggerUI: true,
swaggerUsername: '<username>',
swaggerPassword: '<password>',
});
```
- Bind the `HttpAuthenticationVerifier` to override the basic authentication logic provided by [default](/providers/http-authentication.verifier.ts).
- Start the application
`npm start`
29 changes: 29 additions & 0 deletions packages/core/src/components/swagger-authentication/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
Binding,
Component,
CoreBindings,
inject,
ProviderMap,
} from '@loopback/core';
import {Class, Model, Repository} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {SwaggerAuthenticationBindings} from './keys';
import {AuthenticateSwaggerMiddlewareInterceptor} from './middlewares';
import {HttpAuthenticationVerifierProvider} from './providers/http-authentication.verifier';

export class SwaggerAuthenticationComponent implements Component {
providers?: ProviderMap;
bindings: Binding[] = [];
repositories?: Class<Repository<Model>>[];
models?: Class<Model>[];
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
private readonly application: RestApplication,
) {
this.providers = {
[SwaggerAuthenticationBindings.VERIFIER.key]:
HttpAuthenticationVerifierProvider,
};
this.application.middleware(AuthenticateSwaggerMiddlewareInterceptor);
}
}
5 changes: 5 additions & 0 deletions packages/core/src/components/swagger-authentication/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './component';
export * from './keys';
export * from './types';
export * from './providers';
export * from './middlewares';
8 changes: 8 additions & 0 deletions packages/core/src/components/swagger-authentication/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {BindingKey} from '@loopback/core';
import {BINDING_PREFIX} from '../../constants';
import {HttpAuthenticationVerifier} from './types';
export namespace SwaggerAuthenticationBindings {
export const VERIFIER = BindingKey.create<HttpAuthenticationVerifier>(
`${BINDING_PREFIX}.bearer-verfier.config`,
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
Context,
globalInterceptor,
inject,
InvocationResult,
Provider,
ValueOrPromise,
} from '@loopback/core';
import {Request, RequestContext} from '@loopback/rest';
import {
RestExplorerBindings,
RestExplorerConfig,
} from '@loopback/rest-explorer';
import {MiddlewareContext, Middleware} from '@loopback/express';
import {SwaggerAuthenticationBindings} from '../keys';
import {HttpAuthenticationVerifier} from '../types';

@globalInterceptor('auth', {tags: {name: 'AuthenticateSwaggerMiddleware'}})
export class AuthenticateSwaggerMiddlewareInterceptor
implements Provider<Middleware>
{
constructor(
@inject(SwaggerAuthenticationBindings.VERIFIER)
private readonly verifier: HttpAuthenticationVerifier,
@inject(RestExplorerBindings.CONFIG)
private readonly config: RestExplorerConfig,
) {}
value() {
return this.intercept.bind(this);
}

async intercept(
context: MiddlewareContext,
next: () => ValueOrPromise<InvocationResult>,
) {
let request, response;
if (this.isRequestContext(context.parent)) {
request = context.parent.request;
response = context.parent.response;
}
if (request && response && this.isOpenAPISpecRequest(request)) {
const {username, password} = this.decodeHeader(request);
const verified = this.verifier(username, password);
if (!verified) {
response
.status(401)
.setHeader('WWW-Authenticate', 'Basic realm="Node"');
response.end('Unauthorized');
return null;
}
}
return next();
}

private decodeHeader(request: Request) {
const header = request.headers.authorization ?? ''; // get the auth header
const token = header.split(/\s+/).pop() ?? ''; // and the encoded auth token
const auth = Buffer.from(token, 'base64').toString(); // convert from base64
const parts = auth.split(/:/); // split on colon
const username = parts.shift(); // username is first
const password = parts.join(':');

return {
username,
password,
};
}

private isOpenAPISpecRequest(request: Request) {
const swaggerUrl = `${this.config.path}/openapi.json`;
if (request.url.includes(swaggerUrl)) {
return true;
}
return false;
}

private isRequestContext(context?: Context): context is RequestContext {
return !!(
(context as RequestContext).request &&
(context as RequestContext).response
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './authenticate-swagger.middleware';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {inject, Provider} from '@loopback/core';
import {SFCoreBindings} from '../../../keys';
import {CoreConfig} from '../../../types';
import {HttpAuthenticationVerifier} from '../types';

export class HttpAuthenticationVerifierProvider
implements Provider<HttpAuthenticationVerifier>
{
constructor(
@inject(SFCoreBindings.config, {optional: true})
private readonly coreConfig: CoreConfig,
) {}
value(): HttpAuthenticationVerifier {
return (username, password) => {
return (
username === this.coreConfig.swaggerUsername &&
password === this.coreConfig.swaggerPassword
);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './http-authentication.verifier';
6 changes: 6 additions & 0 deletions packages/core/src/components/swagger-authentication/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type HttpAuthenticationVerifier = (
username?: string,
password?: string,
) => boolean;

export type ISwaggerAuthenticationConfig = {};
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface CoreConfig {
authentication?: boolean;
swaggerUsername?: string;
swaggerPassword?: string;
authenticateSwaggerUI?: boolean;
swaggerAuthenticate?: (
req?: IncomingMessage,
username?: string,
Expand Down

0 comments on commit 40f669f

Please sign in to comment.