@uphold/fastify-openapi-router-plugin 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -33
- package/package.json +1 -1
- package/src/index.js +2 -0
- package/src/parser/index.js +1 -1
- package/src/parser/security.js +4 -2
- package/src/parser/security.test.js +38 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ await fastify.register(import('@fastify/fastify-openapi-router-plugin'), {
|
|
|
58
58
|
| ------ | ---- | ---------- |
|
|
59
59
|
| `spec` | `string` or `object` | **REQUIRED**. A file path or object of your OpenAPI specification. |
|
|
60
60
|
| `securityHandlers` | `object` | An object containing the security handlers that match [Security Schemes](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object) described in your OpenAPI specification. |
|
|
61
|
+
| `securityErrorMapper` | `function` | A function that allows mapping the default `UnauthorizedError` to a custom error. |
|
|
61
62
|
|
|
62
63
|
#### `spec`
|
|
63
64
|
|
|
@@ -141,6 +142,45 @@ await fastify.register(import('@fastify/fastify-openapi-router-plugin'), {
|
|
|
141
142
|
> [!IMPORTANT]
|
|
142
143
|
> If your specification uses `http` security schemes with `in: cookie`, you must register [@fastify/cookie](https://github.com/fastify/fastify-cookie) before this plugin.
|
|
143
144
|
|
|
145
|
+
#### `securityErrorMapper`
|
|
146
|
+
|
|
147
|
+
The plugin will throw an `UnauthorizedError` when none of the `security` blocks succeed. By default, this error originates a `401` reply with `{ code: 'FST_OAS_UNAUTHORIZED', 'message': 'Unauthorized' }` as the payload. You can override this behavior by leveraging the `securityErrorMapper` option:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
await fastify.register(import('@fastify/fastify-openapi-router-plugin'), {
|
|
151
|
+
spec: './petstore.json',
|
|
152
|
+
securityHandlers: {
|
|
153
|
+
OAuth2: async (request, reply) => {
|
|
154
|
+
// ...
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
securityErrorMapper: (unauthorizedError) => {
|
|
158
|
+
// Use `unauthorizedError.securityReport` to perform logic and return a custom error.
|
|
159
|
+
return MyUnauthorizedError();
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The `securityReport` property of the unauthorized error contains an array of objects with the following structure:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
[
|
|
168
|
+
{
|
|
169
|
+
ok: false,
|
|
170
|
+
// Schemes can be an empty object if the security block was skipped due to missing values.
|
|
171
|
+
schemes: {
|
|
172
|
+
OAuth2: {
|
|
173
|
+
ok: false,
|
|
174
|
+
// Error thrown by the security handler or fastify.oas.errors.ScopesMismatchError if the scopes were not satisfied.
|
|
175
|
+
error: new Error(),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If you don't define a `securityErrorMapper`, you can still catch the `UnauthorizedError` in your fastify error handler.
|
|
183
|
+
|
|
144
184
|
### Decorators
|
|
145
185
|
|
|
146
186
|
#### `fastify.oas.route(options)`
|
|
@@ -167,6 +207,7 @@ fastify.oas.route({
|
|
|
167
207
|
This object contains all error classes that can be thrown by the plugin:
|
|
168
208
|
|
|
169
209
|
- `UnauthorizedError`: Thrown when all security schemes verification failed.
|
|
210
|
+
- `ScopesMismatchError`: Thrown when the scopes returned by the security handler do not satisfy the scopes defined in the API operation.
|
|
170
211
|
|
|
171
212
|
#### `request.oas`
|
|
172
213
|
|
|
@@ -174,7 +215,7 @@ For your convenience, the object `request.oas` is populated with data related to
|
|
|
174
215
|
|
|
175
216
|
- `operation` is the raw API operation that activated the Fastify route.
|
|
176
217
|
- `security` is an object where keys are security scheme names and values the returned `data` field from security handlers.
|
|
177
|
-
- `securityReport`: A detailed report of the security verification process. Check the [
|
|
218
|
+
- `securityReport`: A detailed report of the security verification process. Check the [`securityErrorMapper`](#security-error-mapper) section for more information.
|
|
178
219
|
|
|
179
220
|
**Example**
|
|
180
221
|
|
|
@@ -205,38 +246,6 @@ fastify.oas.route({
|
|
|
205
246
|
});
|
|
206
247
|
```
|
|
207
248
|
|
|
208
|
-
### Error handler
|
|
209
|
-
|
|
210
|
-
The plugin will throw an `UnauthorizedError` when none of the `security` blocks succeed. By default, this error originates a `401` reply with `{ code: 'FST_OAS_UNAUTHORIZED', 'message': 'Unauthorized' }` as the payload. You can override this behavior by registering a fastify error handler:
|
|
211
|
-
|
|
212
|
-
```js
|
|
213
|
-
fastify.setErrorHandler((error, request, reply) => {
|
|
214
|
-
if (error instanceof fastify.oas.errors.UnauthorizedError) {
|
|
215
|
-
// Do something with `error.securityReport` and call `reply` accordingly.
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ...
|
|
219
|
-
});
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
The `securityReport` property contains an array of objects with the following structure:
|
|
223
|
-
|
|
224
|
-
```js
|
|
225
|
-
[
|
|
226
|
-
{
|
|
227
|
-
ok: false,
|
|
228
|
-
// Schemes can be an empty object if the security block was skipped due to missing values.
|
|
229
|
-
schemes: {
|
|
230
|
-
OAuth2: {
|
|
231
|
-
ok: false,
|
|
232
|
-
// Error thrown by the security handler or fastify.oas.errors.ScopesMismatchError if the scopes were not satisfied.
|
|
233
|
-
error: new Error(),
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
]
|
|
238
|
-
```
|
|
239
|
-
|
|
240
249
|
## License
|
|
241
250
|
|
|
242
251
|
[MIT](./LICENSE)
|
package/package.json
CHANGED
package/src/index.js
CHANGED
package/src/parser/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export const parse = async options => {
|
|
|
28
28
|
async function (request) {
|
|
29
29
|
request[DECORATOR_NAME].operation = operation;
|
|
30
30
|
},
|
|
31
|
-
parseSecurity(operation, spec, options.securityHandlers)
|
|
31
|
+
parseSecurity(operation, spec, options.securityHandlers, options.securityErrorMapper)
|
|
32
32
|
].filter(Boolean),
|
|
33
33
|
schema: {
|
|
34
34
|
headers: parseParams(operation.parameters, 'header'),
|
package/src/parser/security.js
CHANGED
|
@@ -4,7 +4,7 @@ import { extractSecuritySchemeValueFromRequest, verifyScopes } from '../utils/se
|
|
|
4
4
|
import _ from 'lodash-es';
|
|
5
5
|
import pProps from 'p-props';
|
|
6
6
|
|
|
7
|
-
export const parseSecurity = (operation, spec, securityHandlers) => {
|
|
7
|
+
export const parseSecurity = (operation, spec, securityHandlers, securityErrorMapper) => {
|
|
8
8
|
// Use the operation security if it's defined, otherwise fallback to the spec global security.
|
|
9
9
|
const operationSecurity = operation.security ?? spec.security ?? [];
|
|
10
10
|
|
|
@@ -93,7 +93,9 @@ export const parseSecurity = (operation, spec, securityHandlers) => {
|
|
|
93
93
|
const lastResult = report[report.length - 1];
|
|
94
94
|
|
|
95
95
|
if (!lastResult.ok) {
|
|
96
|
-
|
|
96
|
+
const error = createUnauthorizedError(report);
|
|
97
|
+
|
|
98
|
+
throw securityErrorMapper?.(error) ?? error;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
// Otherwise, we can safely use the last result to decorate the request.
|
|
@@ -519,4 +519,42 @@ describe('parseSecurity()', () => {
|
|
|
519
519
|
`);
|
|
520
520
|
}
|
|
521
521
|
});
|
|
522
|
+
|
|
523
|
+
it('should map security errors by running the supplied mapper', async () => {
|
|
524
|
+
const request = {
|
|
525
|
+
[DECORATOR_NAME]: {},
|
|
526
|
+
headers: {
|
|
527
|
+
authorization: 'Bearer bearer token'
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const operation = {
|
|
531
|
+
security: [{ OAuth2: [] }]
|
|
532
|
+
};
|
|
533
|
+
const spec = {
|
|
534
|
+
components: {
|
|
535
|
+
securitySchemes: {
|
|
536
|
+
OAuth2: { type: 'oauth2' }
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
const securityHandlers = {
|
|
541
|
+
OAuth2: vi.fn(() => {
|
|
542
|
+
throw new Error('OAuth2 error');
|
|
543
|
+
})
|
|
544
|
+
};
|
|
545
|
+
const customError = new Error('Mapped error');
|
|
546
|
+
const securityErrorMapper = vi.fn(() => customError);
|
|
547
|
+
|
|
548
|
+
const onRequest = parseSecurity(operation, spec, securityHandlers, securityErrorMapper);
|
|
549
|
+
|
|
550
|
+
expect.assertions(3);
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
await onRequest(request);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
expect(err).toBe(customError);
|
|
556
|
+
expect(securityErrorMapper).toHaveBeenCalledTimes(1);
|
|
557
|
+
expect(securityErrorMapper.mock.calls[0][0]).toBeInstanceOf(errors.UnauthorizedError);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
522
560
|
});
|