Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions spec/HeaderAliasesValidation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

const Config = require('../lib/Config');

describe('Config.validateHeaderAliases', () => {
it('should accept null and undefined', () => {
expect(() => Config.validateHeaderAliases(null)).not.toThrow();
expect(() => Config.validateHeaderAliases(undefined)).not.toThrow();
});

it('should accept a valid headerAliases object', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Parse-Application-Id': ['X-App-Id'],
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
})
).not.toThrow();
});

it('should reject an alias that normalizes to the same value as its canonical header', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Foo': ['x-foo'],
})
).toThrowError(/must not normalize to the same value as the canonical header name/);
});

it('should reject duplicate normalized aliases within the same aliases array', () => {
expect(() =>
Config.validateHeaderAliases({
'X-A': ['foo', 'FOO'],
})
).toThrowError(/Duplicate normalized header alias/);
});

it('should reject two canonical keys that normalize to the same value', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Foo': [],
'x-foo': [],
})
).toThrowError(/collides with.*after trim and lowercasing/);
});

it('should reject the same normalized alias used for two different canonical headers', () => {
expect(() =>
Config.validateHeaderAliases({
'X-A': ['shared-alias'],
'X-B': ['Shared-Alias'],
})
).toThrowError(/collides with alias/);
});

it('should reject an alias that normalizes to another canonical header key', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Parse-Bar': [],
'X-Parse-Foo': ['x-parse-bar'],
})
).toThrowError(/collides with canonical header/);
});
});
111 changes: 111 additions & 0 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,25 @@ describe('middlewares', () => {
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
});

it('should append configured header aliases to Access-Control-Allow-Headers', () => {
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
});
const headers = {};
const res = {
header: (key, value) => {
headers[key] = value;
},
};
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Headers']).toContain('X-App-Id');
expect(headers['Access-Control-Allow-Headers']).toContain('X-Session-Token-Alias');
});

it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
AppCachePut(fakeReq.body._ApplicationId, {
allowOrigin: undefined,
Expand Down Expand Up @@ -409,6 +428,98 @@ describe('middlewares', () => {
});
});

it('should resolve app id from configured header alias', done => {
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
},
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-app-id'] = fakeReq.body._ApplicationId;
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
expect(fakeReq.headers['x-parse-application-id']).toEqual(fakeReq.body._ApplicationId);
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.appId).toEqual(fakeReq.body._ApplicationId);
done();
});
});
});

it('should resolve session token from configured header alias', done => {
const sessionToken = 'session-token-via-alias';
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-session-token-alias'] = sessionToken;
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.sessionToken).toEqual(sessionToken);
done();
});
});
});

it('should prefer canonical session token over alias when both headers are present', done => {
const canonicalToken = 'session-token-canonical';
const aliasToken = 'session-token-alias-value';
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-parse-session-token'] = canonicalToken;
fakeReq.headers['x-session-token-alias'] = aliasToken;
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.sessionToken).toEqual(canonicalToken);
done();
});
});
});

it('should resolve master key from configured alias in handleParseAuth', async () => {
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Master-Key': ['X-Master-Key-Alias'],
},
masterKey: 'masterKey',
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-master-key-alias'] = 'masterKey';
await new Promise(resolve =>
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, resolve)
);
await new Promise(resolve =>
middlewares.handleParseAuth(fakeReq.body._ApplicationId)(fakeReq, fakeRes, resolve)
);
expect(fakeReq.auth.isMaster).toBe(true);
});

it('should call next without throwing when app is not in AppCache', () => {
const next = jasmine.createSpy('next');
middlewares.handleHeaderAliases('NotInCacheAppId')(fakeReq, fakeRes, next);
expect(next).toHaveBeenCalled();
});

it('should call next without throwing when headerAliases is missing or null', done => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['0.0.0.0/0'],
});
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['0.0.0.0/0'],
headerAliases: null,
});
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, done);
});
});

it('should give invalid response when upload file without x-parse-application-id in header', () => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
Expand Down
98 changes: 97 additions & 1 deletion spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const {
GraphQLList,
} = require('graphql');
const { ParseServer } = require('../');
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
const { ParseGraphQLServer, getCSRFRequestHeaders } = require('../lib/GraphQL/ParseGraphQLServer');
const { ReadPreference, Collection } = require('mongodb');
let uuidv4;

Expand Down Expand Up @@ -105,6 +105,30 @@ describe('ParseGraphQLServer', () => {
});
});

describe('getCSRFRequestHeaders', () => {
it('should include safe application-id header aliases with canonical header', () => {
const headers = getCSRFRequestHeaders({
'X-Parse-Application-Id': ['X-App-Id', 'X-Client-App'],
});
expect(headers).toEqual(['X-Parse-Application-Id', 'X-App-Id', 'X-Client-App']);
});

it('should exclude CORS-safelisted request-header names and Range from CSRF whitelist', () => {
const headers = getCSRFRequestHeaders({
'X-Parse-Application-Id': [
'Accept',
'accept-language',
'Content-Language',
'Content-Type',
'Range',
'rAnGe',
'X-Safe-Custom',
],
});
expect(headers).toEqual(['X-Parse-Application-Id', 'X-Safe-Custom']);
});
});

describe('_getServer', () => {
it('should only return new server on schema changes', async () => {
parseGraphQLServer.server = undefined;
Expand Down Expand Up @@ -135,6 +159,7 @@ describe('ParseGraphQLServer', () => {
expect(server).toBe(firstServer);
});
});

});

describe('_getGraphQLOptions', () => {
Expand Down Expand Up @@ -201,6 +226,46 @@ describe('ParseGraphQLServer', () => {
).not.toThrow();
expect(useCount).toBeGreaterThan(0);
});

it('registers header alias normalization before parse header handling', async () => {
const parseServerWithAliases = await global.reconfigureServer({
maintenanceKey: 'test2',
maxUploadSize: '1kb',
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
},
});
const graphQLServerWithAliases = new ParseGraphQLServer(parseServerWithAliases, {
graphQLPath: '/graphql',
playgroundPath: '/playground',
});
const middlewares = require('../lib/middlewares');
const useCalls = [];
const app = {
use: (...args) => {
useCalls.push(args);
},
};
graphQLServerWithAliases.applyGraphQL(app);
const parseHeadersIndex = useCalls.findIndex(
([path, middleware]) => path === '/graphql' && middleware === middlewares.handleParseHeaders
);
expect(parseHeadersIndex).toBeGreaterThan(0);
const [path, aliasMiddleware] = useCalls[parseHeadersIndex - 1];
expect(path).toBe('/graphql');
const req = {
originalUrl: '/graphql',
url: '/graphql',
protocol: 'http',
headers: {
host: 'localhost',
'x-app-id': parseServerWithAliases.config.appId,
},
get: key => req.headers[key.toLowerCase()],
};
await new Promise(resolve => aliasMiddleware(req, {}, resolve));
expect(req.headers['x-parse-application-id']).toBe(parseServerWithAliases.config.appId);
});
});

describe('applyPlayground', () => {
Expand Down Expand Up @@ -8325,6 +8390,37 @@ describe('ParseGraphQLServer', () => {
});

describe('Session Token', () => {
it('should retrieve me with session token header alias', async () => {
parseServer = await global.reconfigureServer({
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
});
await createGQLFromParseServer(parseServer);
const username = `alias-gql-${uuidv4()}`;
const user = new Parse.User();
user.setUsername(username);
user.setPassword('password');
await user.signUp();
const result = await apolloClient.query({
query: gql`
query GetCurrentUser {
viewer {
user {
username
}
}
}
`,
context: {
headers: {
'X-Session-Token-Alias': user.getSessionToken(),
},
},
});
expect(result.data.viewer.user.username).toBe(username);
});

it('should fail due to invalid session token', async () => {
try {
await apolloClient.query({
Expand Down
49 changes: 49 additions & 0 deletions spec/rest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,55 @@ describe('read-only masterKey', () => {
});
});

describe('rest header aliases', () => {
it('supports REST requests with application-id header alias only', async () => {
await reconfigureServer({
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
},
});
try {
const response = await request({
url: `${Parse.serverURL}/schemas`,
method: 'GET',
headers: {
'X-App-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
},
});
expect(response.data.results).toBeDefined();
expect(Array.isArray(response.data.results)).toBe(true);
} finally {
await reconfigureServer();
}
});

it('supports /users/me with session-token header alias', async () => {
await reconfigureServer({
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
});
try {
const username = `alias-rest-${Date.now()}`;
const user = await Parse.User.signUp(username, 'password');
const response = await request({
url: `${Parse.serverURL}/users/me`,
method: 'GET',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'X-Session-Token-Alias': user.getSessionToken(),
},
});
expect(response.data.objectId).toBe(user.id);
expect(response.data.username).toBe(username);
} finally {
await reconfigureServer();
}
});
});

describe('rest context', () => {
it('should support dependency injection on rest api', async () => {
const requestContextMiddleware = (req, res, next) => {
Expand Down
Loading
Loading