feat: configure cors allowed headers (#6837)

## Description

Currently, the Payload doesn't support to extend the Allowed Headers in
CORS context. With this PR, `cors` property can be an object with
`origins` and `headers`.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [ ] Chore (non-breaking change which does not add functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)
- [ ] Change to the
[examples](https://github.com/payloadcms/payload/tree/main/examples)
directory (does not affect core functionality)
- [x] This change requires a documentation update

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation

Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
Hulpoi George-Valentin
2024-07-15 20:26:29 +03:00
committed by GitHub
parent f494ebabbf
commit 9c72ab97b0
6 changed files with 93 additions and 12 deletions

View File

@@ -73,7 +73,7 @@ The following options are available:
| **`serverURL`** | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
| **`cors`** | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More](#cors) |
| **`localization`** | Opt-in and control how Payload handles the translation of your content into multiple locales. [More details](./localization). |
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
@@ -179,3 +179,37 @@ When `PAYLOAD_CONFIG_PATH` is set, Payload will use this path to load the config
Payload collects **completely anonymous** telemetry data about general usage. This data is super important to us and helps us accurately understand how we're growing and what we can do to build the software into everything that it can possibly be. The telemetry that we collect also help us demonstrate our growth in an accurate manner, which helps us as we seek investment to build and scale our team. If we can accurately demonstrate our growth, we can more effectively continue to support Payload as free and open-source software. To opt out of telemetry, you can pass `telemetry: false` within your Payload Config.
For more information about what we track, take a look at our [privacy policy](/privacy).
## Cross-origin resource sharing (CORS)
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or a object with the following properties:
| Option | Description |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `origins` | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
| `headers | A list of allowed headers that will be appended in `Access-Control-Allow-Headers`. |
Here's an example showing how to allow incoming requests from any domain:
```ts
import { buildConfig } from 'payload/config'
export default buildConfig({
// ...
cors: '*'
})
```
Here's an example showing how to append a new header (`x-custom-header`) in `Access-Control-Allow-Headers`:
```ts
import { buildConfig } from 'payload/config'
export default buildConfig({
// ...
cors: {
origins: ['http://localhost:3000']
headers: ['x-custom-header']
}
})
```

View File

@@ -1,4 +1,4 @@
import type { PayloadRequest } from 'payload'
import type { CORSConfig, PayloadRequest } from 'payload'
type CorsArgs = {
headers: Headers
@@ -9,15 +9,36 @@ export const headersWithCors = ({ headers, req }: CorsArgs): Headers => {
const requestOrigin = req?.headers.get('Origin')
if (cors) {
headers.set('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')
headers.set(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Content-Encoding, x-apollo-tracing',
)
const defaultAllowedHeaders = [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'Content-Encoding',
'x-apollo-tracing',
]
if (cors === '*') {
headers.set('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')
if (typeof cors === 'object' && 'headers' in cors) {
headers.set(
'Access-Control-Allow-Headers',
[...defaultAllowedHeaders, ...cors.headers].filter(Boolean).join(', '),
)
} else {
headers.set('Access-Control-Allow-Headers', defaultAllowedHeaders.join(', '))
}
if (cors === '*' || (typeof cors === 'object' && 'origins' in cors && cors.origins === '*')) {
headers.set('Access-Control-Allow-Origin', '*')
} else if (Array.isArray(cors) && cors.indexOf(requestOrigin) > -1) {
} else if (
(Array.isArray(cors) && cors.indexOf(requestOrigin) > -1) ||
(!Array.isArray(cors) &&
typeof cors === 'object' &&
'origins' in cors &&
cors.origins.indexOf(requestOrigin) > -1)
) {
headers.set('Access-Control-Allow-Credentials', 'true')
headers.set('Access-Control-Allow-Origin', requestOrigin)
}

View File

@@ -104,7 +104,14 @@ export default joi.object({
),
collections: joi.array(),
cookiePrefix: joi.string(),
cors: [joi.string().valid('*'), joi.array().items(joi.string())],
cors: [
joi.string().valid('*'),
joi.array().items(joi.string()),
joi.object().keys({
headers: joi.array().items(joi.string()),
origins: [joi.string().valid('*'), joi.array().items(joi.string())],
}),
],
csrf: joi.array().items(joi.string().allow('')).sparse(),
custom: joi.object().pattern(joi.string(), joi.any()),
db: joi.any(),

View File

@@ -440,6 +440,11 @@ export type SharpDependency = (
options?: sharp.SharpOptions,
) => sharp.Sharp
export type CORSConfig = {
headers?: string[]
origins: '*' | string[]
}
/**
* This is the central configuration
*
@@ -592,7 +597,7 @@ export type Config = {
*/
cookiePrefix?: string
/** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */
cors?: '*' | string[]
cors?: '*' | CORSConfig | string[]
/** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */
csrf?: string[]

View File

@@ -113,4 +113,8 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
cors: {
origins: '*',
headers: ['x-custom-header'],
},
})

View File

@@ -1,13 +1,16 @@
import type { BlockField, Payload } from 'payload'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import configPromise from './config.js'
let restClient: NextRESTClient
let payload: Payload
describe('Config', () => {
beforeAll(async () => {
;({ payload } = await initPayloadInt(configPromise))
;({ payload, restClient } = await initPayloadInt(configPromise))
})
afterAll(async () => {
@@ -91,4 +94,11 @@ describe('Config', () => {
})
})
})
describe('cors config', () => {
it('includes a custom header in Access-Control-Allow-Headers', async () => {
const response = await restClient.GET(`/pages`)
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('x-custom-header')
})
})
})