diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 468731e..ae62de7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,7 +30,7 @@ jobs: with: node-version: 22 - - name: Install node packages + - name: Install dependencies run: npm ci - name: Configure git diff --git a/README.md b/README.md index ab8c6af..236ea7a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ You can pass the following options during the plugin registration: ```js await fastify.register(import('@fastify/fastify-openapi-router-plugin'), { spec: './petstore.json', - securityHandler: { + securityHandlers: { APIAuth: (value, request) => {} } }); @@ -83,7 +83,7 @@ If you haven't defined any [Security Schemes](https://github.com/OAI/OpenAPI-Spe Security handlers are executed as a `onRequest` hook for every API operation if plugin founds a [Security Requirement Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-requirement-object) defined on the root level or operation level of your OpenAPI specification. According to [Fastify Lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/), it is the most secure way to implement an authentication layer because it avoids parsing the body for unauthorized accesses. -The functions you define in `securityHandlers` are cached per request. If your operation `security` uses the same security scheme, the plugin will call the security handler only once. Moreover, the plugin will only call the security handler if there's a value extracted from the request. +If your operation `security` entries use the same security scheme, the plugin will call the security handler only once. Moreover, the plugin will only call the security handler if there's a value extracted from the request. The security handler should either throw an error or return an object with `{ data, scopes }` where `data` becomes available as `request.oas.security.` in your route handler and `scopes` is array of strings that will be used to verify if the scopes defined in the API operation are satisfied. @@ -142,7 +142,7 @@ await fastify.register(import('@fastify/fastify-openapi-router-plugin'), { This method is used to register a new route by translating the given `operationId` to a compliant Fastify route. -`options` must be an `Object` containing at least the `operationId` and `handler(request, reply)`. All the available [routes options](https://fastify.dev/docs/latest/Reference/Routes/#routes-options) can be used except `method`, `url` and `schema` because those are loaded from your OpenAPI specification. +`options` must be an object containing at least the `operationId` and `handler(request, reply)`. All the available [routes options](https://fastify.dev/docs/latest/Reference/Routes/#routes-options) can be used except `method`, `url` and `schema` because those are loaded from your OpenAPI specification. **Example** @@ -165,21 +165,25 @@ This object contains all error classes that can be thrown by the plugin: #### `request.oas` -For your convenience, the object `request.oas` is populated with data related to the request being made. This is an object containing `{ operation, security }`, where `operation` is the raw API operation that activated the Fastify route and `security` is an object containing the result of all security handlers called for the Fastify route. +For your convenience, the object `request.oas` is populated with data related to the request being made. This is an object containing `{ operation, security, securityReport }`, where: + +- `operation` is the raw API operation that activated the Fastify route. +- `security` is an object where keys are security scheme names and values the returned `data` field from security handlers. +- `securityReport`: A detailed report of the security verification process. Check the [Error handler](#error-handler) section for more information. **Example** ```js await fastify.register(import('@fastify/fastify-openapi-router-plugin'), { spec: './petstore.json', - securitySchemes: { + securityHandlers: { OAuth2: async (request, reply) => { // Validate and decode token. - const { userId } = verifyToken(token); + const { userId, scopes } = verifyToken(token); return { data: { userId }, - scopes: tokenData.scopes + scopes, }; } } @@ -203,7 +207,7 @@ If there was an error associated with `security` processing of a request, the pl ```js fastify.setErrorHandler((error, request, reply) => { if (error instanceof fastify.oas.errors.UnauthorizedError) { - // Do something with `error.report`and call `reply` accordingly + // Do something with `error.report`and call `reply` accordingly. } // ... diff --git a/examples/petstore/app.js b/examples/petstore/app.js index b5fa5f0..6b78041 100644 --- a/examples/petstore/app.js +++ b/examples/petstore/app.js @@ -13,15 +13,12 @@ const fastify = Fastify({ await fastify.register(openApiRouter, { securityHandlers: { api_key: async () => {}, - petstore_auth: async (request, { verifyScopes }) => { - const { missing, ok } = verifyScopes(['read:pets']); - - if (!ok) { - throw new Error('Missing scopes ' + missing.join(', ')); - } - + petstore_auth: async (token, request) => { return { - user: { name: 'John Doe' } + data: { + user: { name: 'John Doe' } + }, + scopes: ['read:pets'] }; } }, diff --git a/src/errors/scopes-mismatch-error.js b/src/errors/scopes-mismatch-error.js index 2e25f42..982d9c4 100644 --- a/src/errors/scopes-mismatch-error.js +++ b/src/errors/scopes-mismatch-error.js @@ -5,9 +5,14 @@ const ScopesMismatchError = createError('FST_OAS_SCOPES_MISMATCH', 'Scopes do no const createScopesMismatchError = (providedScopes, requiredScopes, missingScopes) => { const err = new ScopesMismatchError(); - err.providedScopes = providedScopes; - err.requiredScopes = requiredScopes; - err.missingScopes = missingScopes; + Object.defineProperty(err, 'scopes', { + enumerable: false, + value: { + missing: missingScopes, + provided: providedScopes, + required: requiredScopes + } + }); return err; }; diff --git a/src/utils/security.test.js b/src/utils/security.test.js index c7adecb..affb081 100644 --- a/src/utils/security.test.js +++ b/src/utils/security.test.js @@ -14,9 +14,11 @@ describe('verifyScopes()', () => { } catch (err) { expect(err).toBeInstanceOf(errors.ScopesMismatchError); expect(err).toMatchObject({ - missingScopes: missing, - providedScopes: provided, - requiredScopes: required + scopes: { + missing: missing, + provided: provided, + required: required + } }); } };