Skip to content

feat: Allow multiple origins for header Access-Control-Allow-Origin #8517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 1, 2023
6 changes: 6 additions & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ function mapperFor(elt, t) {
if (type == 'NumberOrBoolean') {
return wrap(t.identifier('numberOrBooleanParser'));
}
if (type === 'StringOrStringArray') {
return wrap(t.identifier('arrayParser'));
}
return wrap(t.identifier('objectParser'));
}
}
Expand Down Expand Up @@ -278,6 +281,9 @@ function inject(t, list) {
const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name;
type = `Adapter<${adapterType}>`;
}
if (type === 'StringOrStringArray') {
type = 'String|String[]';
}
comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`;
const obj = t.objectExpression(props);
return t.objectProperty(t.stringLiteral(elt.name), obj);
Expand Down
58 changes: 58 additions & 0 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,64 @@ describe('middlewares', () => {
expect(headers['Access-Control-Allow-Origin']).toEqual('https://parseplatform.org/');
});

it('should support multiple origins if several are defined in allowOrigin as a comma delimited string', () => {
AppCache.put(fakeReq.body._ApplicationId, {
allowOrigin: 'https://a.com,https://b.com,https://c.com',
});
const headers = {};
const res = {
header: (key, value) => {
headers[key] = value;
},
};
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
// Test with the first domain
fakeReq.headers.origin = 'https://a.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
// Test with the second domain
fakeReq.headers.origin = 'https://b.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://b.com');
// Test with the third domain
fakeReq.headers.origin = 'https://c.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://c.com');
// Test with an unauthorized domain
fakeReq.headers.origin = 'https://unauthorized.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
});

it('should support multiple origins if several are defined in allowOrigin as an array', () => {
AppCache.put(fakeReq.body._ApplicationId, {
allowOrigin: ['https://a.com', 'https://b.com', 'https://c.com'],
});
const headers = {};
const res = {
header: (key, value) => {
headers[key] = value;
},
};
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
// Test with the first domain
fakeReq.headers.origin = 'https://a.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
// Test with the second domain
fakeReq.headers.origin = 'https://b.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://b.com');
// Test with the third domain
fakeReq.headers.origin = 'https://c.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://c.com');
// Test with an unauthorized domain
fakeReq.headers.origin = 'https://unauthorized.com';
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
});

it('should use user provided on field userFromJWT', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
Expand Down
4 changes: 3 additions & 1 deletion src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ module.exports.ParseServerOptions = {
},
allowOrigin: {
env: 'PARSE_SERVER_ALLOW_ORIGIN',
help: 'Sets the origin to Access-Control-Allow-Origin',
help:
'Sets the origin to Access-Control-Allow-Origin. Can be a string for a single origin or a comma separated string or array for multiple',
action: parsers.arrayParser,
},
analyticsAdapter: {
env: 'PARSE_SERVER_ANALYTICS_ADAPTER',
Expand Down
2 changes: 1 addition & 1 deletion src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Adapter<T> = string | any | T;
type NumberOrBoolean = number | boolean;
type NumberOrString = number | string;
type ProtectedFields = any;
type StringOrStringArray = string | string[];
type RequestKeywordDenylist = {
key: string | any,
value: any,
Expand All @@ -61,8 +62,8 @@ export interface ParseServerOptions {
appName: ?string;
/* Add headers to Access-Control-Allow-Headers */
allowHeaders: ?(string[]);
/* Sets the origin to Access-Control-Allow-Origin */
allowOrigin: ?string;
/* Sets the origin to Access-Control-Allow-Origin. Can be a string for a single origin or a comma separated string or array for multiple */
allowOrigin: ?StringOrStringArray;
/* Adapter module for the analytics */
analyticsAdapter: ?Adapter<AnalyticsAdapter>;
/* Adapter module for the files sub-system */
Expand Down
15 changes: 13 additions & 2 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,19 @@ export function allowCrossDomain(appId) {
if (config && config.allowHeaders) {
allowHeaders += `, ${config.allowHeaders.join(', ')}`;
}
const allowOrigin = (config && config.allowOrigin) || '*';
res.header('Access-Control-Allow-Origin', allowOrigin);

// Support for multiple origins
let allowedOrigins = config && config.allowOrigin ? config.allowOrigin : ['*'];

// Convert comma-separated string to an array if needed
if (typeof allowedOrigins === 'string') {
allowedOrigins = allowedOrigins.split(',').map(domain => domain.trim());
}

const requestOrigin = req.headers.origin;
const originToSet =
requestOrigin && allowedOrigins.includes(requestOrigin) ? requestOrigin : allowedOrigins[0];
res.header('Access-Control-Allow-Origin', originToSet);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', allowHeaders);
res.header('Access-Control-Expose-Headers', 'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id');
Expand Down