Skip to content

Commit

Permalink
Merge pull request #599 from pcafstockf/master
Browse files Browse the repository at this point in the history
zero-dependency cookie schema support
  • Loading branch information
seriousme authored Aug 7, 2024
2 parents 6d3605e + 8ad16d2 commit c39eca8
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]
### Changed
- feat: added addCookieSchema option, see docs/cookieValidationHowTo.md

## [4.6.1] 14-06-2024
### Changed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ The folder [examples/generated-javascript-project](examples/generated-javascript
- fastify will by default coerce types, e.g when you expect a number a string like `"1"` will also pass validation, this can be reconfigured, see [Validation and Serialization](https://www.fastify.io/docs/latest/Reference/Validation-and-Serialization/).
- fastify only supports one schema per route. So while the v3 standard allows for multiple content types per route, each with their own schema this is currently not going to work with fastify. Potential workarounds include a custom content type parser and merging schemas upfront using JSON schema `oneOf`.
- the plugin aims to follow fastify and does not compensate for features that are possible according to the OpenApi specification but not possible in standard fastify (without plugins). This will keep the plugin lightweigth and maintainable. E.g. Fastify does not support cookie validation, while OpenApi v3 does.
- in some cases however, the plugin may be able to provide you with data which could be used to enhance OpenApi support within your own Fastify application. Here is one possible way to perform [cookie validation](docs/cookieValidationHowTo.md) yourself.
- if you have special needs on querystring handling (e.g. arrays, objects etc) then fastify supports a [custom querystring parser](https://www.fastify.io/docs/latest/Server/#querystringparser). You might need to pass the AJV option `coerceTypes: 'array'` as an option to Fastify.
- the plugin is an ECMAscript Module (aka ESM). If you are using Typescript then make sure that you have read: https://www.typescriptlang.org/docs/handbook/esm-node.html to avoid any confusion.
- If you want to use a specification that consists of multiple files then please check out the page on [subschemas](docs/subSchemas.md)
Expand Down
54 changes: 54 additions & 0 deletions docs/cookieValidationHowTo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
### Implementing cookie validation

The [OpenApi](https://www.openapis.org/) specification allows cookie validation, but Fastify itself does not validate or even parse cookies.

The `fastify-openapi-glue` plugin is intentionally designed to work without requiring additional 3rd party plugins.
However, it does provide a boolean option `addCookieSchema` which tells it to insert JSON Schema describing OpenApi cookies into the Fastify [Routes options](https://fastify.dev/docs/latest/Reference/Routes/#routes-options).

Using this `addCookieSchema` option, one possible way to implement cookie validation in your application might be:
- Register a plugin for cookie parsing with Fastify (perhaps [fastify cookie plugin](https://github.com/fastify/fastify-cookie)).
- Listen for Fastify's [`onRoute` Application Hook](https://fastify.dev/docs/latest/Reference/Hooks/#onroute).
- In your `onRoute` handler:
- Check to see if `fastify-openapi-glue` found cookie specifications that it added to the `routeOptions`.
- If cookie schema is present, pre-compile it with Ajv and add the compiled schema to the `routeOptions.config` object.
- Register a global Fastify [`preHandler`](https://fastify.dev/docs/latest/Reference/Hooks/#prehandler)
- In your global `preHandler`:
- See if the invoked route has a cookie validator (pre-compiled by your `onRoute` handler).
- Validate the cookie (which your cookie parser should have already added to the `request`).
- With your customizations in place, register `fastify-openapi-glue`.

Example:
```javascript
// Register a plugin for cookie parsing
fastify.register(cookie);

// Hook into the route registration process to compile cookie schemas
fastify.addHook('onRoute', (routeOptions) => {
const schema = routeOptions.schema;
/*
* schema.cookies will be added to the schema object if the
* 'addCookieSchema' option is passed to fastify-openapi-glue.
*/
if (schema?.cookies) {
// Compile the cookie schema and store it in the route's context
routeOptions.config = routeOptions.config || {};
routeOptions.config.cookieValidator = ajv.compile(schema.cookies);
}
});

// Pre-handler hook to validate cookies using the precompiled schema
fastify.addHook('preHandler', async (request, reply) => {
// See if this route has been configured with a cookie validator.
const cookieValidator = request.routeOptions.config?.cookieValidator;
if (cookieValidator) {
const valid = cookieValidator(request.cookies);
if (!valid) {
reply.status(400).send({error: 'Invalid cookies', details: cookieValidator.errors});
throw new Error('Invalid cookies');
}
}
});

// Magic!
fastify.register(openapiGlue, options);
```
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export interface FastifyOpenapiGlueOptions {
operationResolver?: OperationResolver;
prefix?: string;
addEmptySchema?: boolean;
/**
* NOTE:
* This does not enable cookie validation (Fastify core does not support cookie validation).
* This is simply a flag which triggers the addition of cookie schema (from the OpenAPI specification), into the 'schema' property of Fastify Routes options.
* You can then hook Fastify's 'onRoute' event to make use of the schema as you wish.
*/
addCookieSchema?: boolean;
}

declare const fastifyOpenapiGlue: FastifyPluginAsync<FastifyOpenapiGlueOptions>;
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ async function plugin(instance, opts) {
const parser = new Parser();
const config = await parser.parse(opts.specification, {
addEmptySchema: opts.addEmptySchema ?? false,
addCookieSchema: opts.addCookieSchema ?? false,
});
checkParserValidators(instance, config.contentTypes);
if (opts.service) {
Expand Down
12 changes: 11 additions & 1 deletion lib/Parser.v3.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class ParserV3 extends ParserBase {
const params = [];
const querystring = [];
const headers = [];
const cookies = [];
// const formData = [];
for (const item of data) {
switch (item.in) {
Expand All @@ -65,7 +66,12 @@ export class ParserV3 extends ParserBase {
break;
}
case "cookie": {
console.warn("cookie parameters are not supported by Fastify");
if (this.options.addCookieSchema) {
item.style = item.style || "form";
cookies.push(item);
} else {
console.warn("cookie parameters are not supported by Fastify");
}
break;
}
}
Expand All @@ -79,6 +85,9 @@ export class ParserV3 extends ParserBase {
if (headers.length > 0) {
schema.headers = this.parseParams(headers);
}
if (cookies.length > 0) {
schema.cookies = this.parseParams(cookies);
}
}

parseBody(data) {
Expand Down Expand Up @@ -135,6 +144,7 @@ export class ParserV3 extends ParserBase {
this.spec = spec;
this.options = {
addEmptySchema: options.addEmptySchema ?? false,
addCookieSchema: options.addCookieSchema ?? false,
};

for (const item in spec) {
Expand Down
32 changes: 32 additions & 0 deletions test/test-cookie-param.v3.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,35 @@ test("route registration succeeds with cookie param", (t, done) => {
}
});
});

test("route registration inserts cookie schema", (t, done) => {
const opts = {
specification: testSpec,
serviceHandlers,
addCookieSchema: true,
};

const fastify = Fastify(noStrict);
// Register onRoute handler which will be called when the plugin registers routes in the specification.
let hadCookieSchema = false;
fastify.addHook("onRoute", (routeOptions) => {
const schema = routeOptions.schema;
if (schema.operationId === "getCookieParam") {
hadCookieSchema =
schema?.cookies &&
typeof schema?.cookies?.properties?.cookieValue === "object";
}
});
fastify.register(fastifyOpenapiGlue, opts);
fastify.ready((err) => {
// Our onRoute handler above should have been invoked already and should have found the cookie schema we asked for (with 'addCookieSchema' option).
if (err) {
assert.fail("got unexpected error");
} else if (hadCookieSchema) {
assert.ok(true, "no unexpected error");
done();
} else {
assert.fail("cookie schema not found");
}
});
});

0 comments on commit c39eca8

Please sign in to comment.