diff --git a/README.md b/README.md index 440e15d..7f18861 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ await fastify.register(import('@fastify/fastify-openapi-router-plugin'), { ``` > [!TIP] -> The `scopes` returned by the security handler can contain **wildcards**. For example, if the security handler returns `{ scopes: ['pets:*'] }`, the route will be authorized for any security scope that starts with `pets:`. +> The `scopes` returned by the security handler can contain trailing **wildcards**. For example, if the security handler returns `{ scopes: ['pets:*'] }`, the route will be authorized for any security scope that starts with `pets:`. > [!IMPORTANT] > If your specification uses `http` security schemes with `in: cookie`, you must register [@fastify/cookie](https://github.com/fastify/fastify-cookie) before this plugin. @@ -246,6 +246,18 @@ fastify.oas.route({ }); ``` +### Other exports + +#### `errors` + +This object contains all error classes that can be thrown by the plugin. It contains the same errors as `fastify.oas.errors`. + +#### `verifyScopes(providedScopes, requiredScopes)` + +Checks if the `providedScopes` satisfy the `requiredScopes`. Returns an array of missing scopes or an empty array if all scopes are satisfied. + +This functions supports trailing **wildcards** on `providedScopes`. For example, if the provided scopes is `['pets:*']` and the required scopes is `['pets:read']`, the function will return an empty array. + ### Caveats #### Coercing of `parameters` diff --git a/src/index.js b/src/index.js index 6848c3e..0719f90 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import fp from 'fastify-plugin'; import plugin from './plugin.js'; export * from './errors/index.js'; +export { verifyScopes } from './utils/security.js'; export default fp(plugin, { fastify: '4.x', diff --git a/src/parser/security.js b/src/parser/security.js index ea84c0c..ac06a21 100644 --- a/src/parser/security.js +++ b/src/parser/security.js @@ -1,5 +1,5 @@ import { DECORATOR_NAME } from '../utils/constants.js'; -import { createUnauthorizedError } from '../errors/index.js'; +import { createScopesMismatchError, createUnauthorizedError } from '../errors/index.js'; import { extractSecuritySchemeValueFromRequest, verifyScopes } from '../utils/security.js'; import _ from 'lodash-es'; import pProps from 'p-props'; @@ -66,10 +66,14 @@ export const applySecurity = (operation, spec, securityHandlers, securityErrorMa const blockResults = await pProps(block, async (requiredScopes, name) => { try { const resolved = await callSecurityHandler(name); - const { data, scopes } = resolved ?? {}; + const { data, scopes: providedScopes = [] } = resolved ?? {}; - // Verify scopes, which throws if scopes are missing. - verifyScopes(scopes ?? [], requiredScopes); + // Verify scopes to check if any is missing. + const missingScopes = verifyScopes(providedScopes, requiredScopes); + + if (missingScopes.length > 0) { + throw createScopesMismatchError(providedScopes, requiredScopes, missingScopes); + } return { data, ok: true }; } catch (error) { diff --git a/src/utils/security.js b/src/utils/security.js index 54a31fe..3fc945f 100644 --- a/src/utils/security.js +++ b/src/utils/security.js @@ -1,5 +1,3 @@ -import { createScopesMismatchError } from '../errors/index.js'; - const getValueForHttpSchemeType = (request, securityScheme) => { if (securityScheme.scheme === 'bearer') { const [, bearer] = request.headers.authorization?.match(/^Bearer (.+)$/i) ?? []; @@ -74,7 +72,5 @@ export const verifyScopes = (providedScopes, requiredScopes) => { return !hasMatchingScope; }); - if (missingScopes.length > 0) { - throw createScopesMismatchError(providedScopes, requiredScopes, missingScopes); - } + return missingScopes; }; diff --git a/src/utils/security.test.js b/src/utils/security.test.js index 90a1319..8e2b84f 100644 --- a/src/utils/security.test.js +++ b/src/utils/security.test.js @@ -1,26 +1,12 @@ import { describe, expect, it } from 'vitest'; -import { errors } from '../errors/index'; import { extractSecuritySchemeValueFromRequest, verifyScopes } from './security'; import _ from 'lodash-es'; describe('verifyScopes()', () => { const runTest = ({ missing, provided, required }) => { - try { - verifyScopes(provided, required); - - if (missing.length > 0) { - throw new Error('Expected an error to be thrown'); - } - } catch (err) { - expect(err).toBeInstanceOf(errors.ScopesMismatchError); - expect(err).toMatchObject({ - scopes: { - missing: missing, - provided: provided, - required: required - } - }); - } + const result = verifyScopes(provided, required); + + expect(result).toStrictEqual(missing); }; it('should verify regular scopes correctly against required scopes', async () => {