Skip to content

Commit

Permalink
Merge branch 'master' into security
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed Oct 11, 2019
2 parents cafd358 + 4a6b0af commit c703b5d
Show file tree
Hide file tree
Showing 20 changed files with 280 additions and 167 deletions.
57 changes: 39 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ npm i express-openapi-validator

## Usage

Install the openapi validator
1. Install the openapi validator

```javascript
new OpenApiValidator({
Expand All @@ -29,21 +29,26 @@ new OpenApiValidator({
validateResponses: true, // false by default
}).install(app);
```
_Note: response validation is currently a beta feature_

Then, register an error handler to customize errors
2. Register an error handler

```javascript
app.use((err, req, res, next) => {
// format error
res.status(err.status).json({
res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
});
```

#### Alternatively...
_**Note:** Ensure express is configured with all relevant body parsers. See an [example](#example-express-api-server)_

## Advanced Usage

For OpenAPI 3.0.x 3rd party and custom formats, see [Options](#Options).

#### Optionally inline the spec...

The `apiSpec` option may be specified as the spec object itself, rather than a path e.g.

Expand Down Expand Up @@ -72,13 +77,25 @@ new OpenApiValidator(options).install(app);

**`validateRequests:`** enable response validation.

- true - (default) validate requests.
- false - do not validate requests.
- `true` - (default) validate requests.
- `false` - do not validate requests.

**`validateResponses:`** enable response validation.

- true - validate responses
- false - (default) do not validate responses
- `true` - validate responses
- `false` - (default) do not validate responses

**`unknownFormats:`** handling of unknown and/or custom formats. Option values:

- `true` (default) - if an unknown format is encountered, validation will report a 400 error.
- `[string]` - an array of unknown format names that will be ignored by the validator. This option can be used to allow usage of third party schemas with format(s), but still fail if another unknown format is used. (_Recommended if unknown formats are used_)
- `"ignore"` - to log warning during schema compilation and always pass validation. This option is not recommended, as it allows to mistype format name and it won't be validated without any error message.

**example:**

```javascript
unknownFormats: ['phone-number', 'uuid']
```

**`securityHandlers:`** register authentication handlers

Expand Down Expand Up @@ -125,9 +142,9 @@ new OpenApiValidator(options).install(app);

**`coerceTypes:`** change data type of data to match type keyword. See the example in Coercing data types and coercion rules. Option values:

- true - (default) coerce scalar data types.
- false - no type coercion.
- "array" - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema).
- `true` - (default) coerce scalar data types.
- `false` - no type coercion.
- `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema).

**`multerOpts:`** the [multer opts](https://github.com/expressjs/multer) to passthrough to multer

Expand All @@ -148,17 +165,21 @@ const app = express();
// 1. Import the express-openapi-validator library
const OpenApiValidator = require('express-openapi-validator').OpenApiValidator;

// 2. Set up body parsers for the request body types you expect
app.use(bodyParser.json());
app.use(bodyParser.text());
app.use(bodyParser.urlencoded());

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// 2. (optionally) Serve the OpenAPI spec
// 3. (optionally) Serve the OpenAPI spec
app.use('/spec', express.static(spec));

// 3. Install the OpenApiValidator onto your express app
// 4. Install the OpenApiValidator onto your express app
new OpenApiValidator({
apiSpec: './openapi.yaml',
}).install(app);
Expand All @@ -176,7 +197,7 @@ app.get('/v1/pets/:id', function(req, res, next) {
res.json({ id: req.params.id, name: 'sparky' });
});

// 4. Define route(s) to upload file(s)
// 5. Define route(s) to upload file(s)
app.post('/v1/pets/:id/photos', function(req, res, next) {
// files are found in req.files
// non-file multipart params can be found as such: req.body['my-param']
Expand All @@ -192,10 +213,10 @@ app.post('/v1/pets/:id/photos', function(req, res, next) {
});
});

// 5. Create an Express error handler
// 6. Create an Express error handler
app.use((err, req, res, next) => {
// 6. Customize errors
res.status(err.status).json({
// 7. Customize errors
res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
Expand Down
1 change: 1 addition & 0 deletions example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const app = express();

app.use(bodyParser.urlencoded());
app.use(bodyParser.json());
app.use(bodyParser.text());
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "express-openapi-validator",
"version": "2.3.0",
"version": "2.4.1",
"description": "Automatically validate API requests using an OpenAPI 3 and Express.",
"main": "dist/index.js",
"scripts": {
Expand Down
55 changes: 42 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ export type SecurityHandlers = {
};
export interface OpenApiValidatorOpts {
apiSpec: OpenAPIV3.Document | string;
coerceTypes?: boolean;
validateResponses?: boolean;
validateRequests?: boolean;
securityHandlers?: SecurityHandlers;
coerceTypes?: boolean;
unknownFormats?: string[] | string | boolean;
multerOpts?: {};
}

Expand All @@ -26,7 +27,9 @@ export class OpenApiValidator {
private options: OpenApiValidatorOpts;

constructor(options: OpenApiValidatorOpts) {
if (!options.apiSpec) throw ono('apiSpec required.');
this.validateOptions(options);

if (options.unknownFormats == null) options.unknownFormats === true;
if (options.coerceTypes == null) options.coerceTypes = true;
if (options.validateRequests == null) options.validateRequests = true;

Expand Down Expand Up @@ -58,26 +61,33 @@ export class OpenApiValidator {
});
}

const coerceTypes = this.options.coerceTypes;
const aoav = new middlewares.RequestValidator(this.context.apiDoc, {
nullable: true,
coerceTypes,
removeAdditional: false,
useDefaults: true,
});
const { coerceTypes, unknownFormats } = this.options;
const requestValidator = new middlewares.RequestValidator(
this.context.apiDoc,
{
nullable: true,
coerceTypes,
removeAdditional: false,
useDefaults: true,
unknownFormats,
},
);

const requestValidator = (req, res, next) => {
return aoav.validate(req, res, next);
const requestValidatorMw = (req, res, next) => {
return requestValidator.validate(req, res, next);
};

const responseValidator = new middlewares.ResponseValidator(
this.context.apiDoc,
{
coerceTypes,
unknownFormats,
},
);

const securityMiddleware = middlewares.security(this.options.securityHandlers);
const securityMiddleware = middlewares.security(
this.options.securityHandlers,
);

const components = this.context.apiDoc.components;
const use = [
Expand All @@ -86,9 +96,28 @@ export class OpenApiValidator {
];
// TODO validate security functions exist for each security key
if (components && components.securitySchemes) use.push(securityMiddleware);
if (this.options.validateRequests) use.push(requestValidator);
if (this.options.validateRequests) use.push(requestValidatorMw);
if (this.options.validateResponses) use.push(responseValidator.validate());

app.use(use);
}

private validateOptions(options: OpenApiValidatorOpts): void {
if (!options.apiSpec) throw ono('apiSpec required.');
const unknownFormats = options.unknownFormats;
if (typeof unknownFormats === 'boolean') {
if (!unknownFormats) {
throw ono(
"unknownFormats must contain an array of unknownFormats, 'ignore' or true",
);
}
} else if (
typeof unknownFormats === 'string' &&
unknownFormats !== 'ignore' &&
!Array.isArray(unknownFormats)
)
throw ono(
"unknownFormats must contain an array of unknownFormats, 'ignore' or true",
);
}
}
3 changes: 2 additions & 1 deletion src/middlewares/ajv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ export function createResponseAjv(openApiSpec, options: any = {}) {
function createAjv(openApiSpec, options: any = {}, request: boolean = true) {
const ajv = new Ajv({
...options,
formats: { ...formats, ...options.formats },
schemaId: 'auto',
allErrors: true,
meta: draftSchema,
formats: { ...formats, ...options.formats },
unknownFormats: options.unknownFormats,
});
ajv.removeKeyword('propertyNames');
ajv.removeKeyword('contains');
Expand Down
5 changes: 2 additions & 3 deletions src/middlewares/openapi.response.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ export class ResponseValidator {
constructor(openApiSpec, options: any = {}) {
this.spec = openApiSpec;
this.ajv = createResponseAjv(openApiSpec, options);
(<any>mung).onError = function(err, req, res, next) {
// monkey patch mung to rethrow exception
(<any>mung).onError = (err, req, res, next) => {
return next(err);
};
}

validate() {
return mung.jsonAsync((body, req: any, res) => {
return mung.json((body, req: any, res) => {
if (req.openapi) {
const responses = req.openapi.schema && req.openapi.schema.responses;
const validators = this._getOrBuildValidator(req, responses);
Expand Down
25 changes: 11 additions & 14 deletions test/additional.props.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const packageJson = require('../package.json');

describe(packageJson.name, () => {
let app = null;
let basePath = null;

before(async () => {
// Set up the express app
Expand All @@ -17,16 +16,14 @@ describe(packageJson.name, () => {
'resources',
'additional.properties.yaml',
);
app = await createApp({ apiSpec }, 3005);
basePath = app.basePath;

// Define new coercion routes
app.use(
`${basePath}/additional_props`,
express
.Router()
.post(`/false`, (req, res) => res.json(req.body))
.post(`/true`, (req, res) => res.json(req.body)),
app = await createApp({ apiSpec }, 3005, app =>
app.use(
`${app.basePath}/additional_props`,
express
.Router()
.post(`/false`, (req, res) => res.json(req.body))
.post(`/true`, (req, res) => res.json(req.body)),
),
);
});

Expand All @@ -36,22 +33,22 @@ describe(packageJson.name, () => {

it('should return 400 if additionalProperties=false, but extra props sent', async () =>
request(app)
.post(`${basePath}/additional_props/false`)
.post(`${app.basePath}/additional_props/false`)
.send({
name: 'test',
extra_prop: 'test',
})
.expect(400)
.then(r => {
expect(r.body.errors).to.be.an('array')
expect(r.body.errors).to.be.an('array');
expect(r.body.errors).to.have.length(1);
const message = r.body.errors[0].message;
expect(message).to.equal('should NOT have additional properties');
}));

it('should return 200 if additonalProperities=true and extra props are sent', async () =>
request(app)
.post(`${basePath}/additional_props/true`)
.post(`${app.basePath}/additional_props/true`)
.send({
name: 'test',
extra_prop: 'test',
Expand Down
Loading

0 comments on commit c703b5d

Please sign in to comment.