Skip to content

Commit 383d3e5

Browse files
authored
test: Pages API is exempt from routeAllowList (#10659)
1 parent bdb21ee commit 383d3e5

5 files changed

Lines changed: 122 additions & 3 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,9 @@ The following table lists all route groups covered by `routeAllowList` with exam
403403
> [!NOTE]
404404
> The GraphQL API is not covered by `routeAllowList`. `routeAllowList` gates the REST API per route, while every GraphQL operation is transported over a single endpoint with the operation, target class, and field set encoded in the request body — so per-route allow-list semantics do not compose with it.
405405
406+
> [!NOTE]
407+
> The Pages API is not covered by `routeAllowList`. Its routes (default endpoint `apps`, configurable via `pages.pagesEndpoint`) serve the browser pages for email verification and password reset that Parse Server links to in the emails it sends to end users, so they must remain reachable without any Parse credentials and are not part of the client REST API. Their behavior is controlled by the email verification and password reset options (`verifyUserEmails`, `emailAdapter`) and the `pages` option.
408+
406409
## Email Verification and Password Reset
407410

408411
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options][server-options] for more details and a full list of available options.

spec/RouteAllowList.spec.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,122 @@ describe('routeAllowList', () => {
375375
});
376376
});
377377

378+
describe('Pages exemption', () => {
379+
// routeAllowList gates the client-facing REST API. The Pages API serves
380+
// the browser pages for email verification and password reset that Parse
381+
// Server links to in the emails it sends to end users, so those routes
382+
// must remain reachable without Parse credentials. The Pages router is
383+
// mounted ahead of the Parse request middleware chain and is not covered
384+
// by the allow list; its behavior is governed by the email verification
385+
// and password reset options instead.
386+
const request = require('../lib/request');
387+
const pagesConfig = () => ({
388+
appName: 'exampleAppName',
389+
publicServerURL: 'http://localhost:8378/1',
390+
verifyUserEmails: true,
391+
emailAdapter: {
392+
sendVerificationEmail: () => Promise.resolve(),
393+
sendPasswordResetEmail: () => Promise.resolve(),
394+
sendMail: () => {},
395+
},
396+
});
397+
const expectForbidden = promise =>
398+
expectAsync(promise).toBeRejectedWith(
399+
jasmine.objectContaining({
400+
data: jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN }),
401+
})
402+
);
403+
404+
it('reaches the email verification page when routeAllowList is empty array', async () => {
405+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
406+
await expectForbidden(request({ method: 'GET', url: 'http://localhost:8378/1/health' }));
407+
const response = await request({
408+
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken',
409+
followRedirects: false,
410+
});
411+
expect(response.status).toBe(200);
412+
expect(response.text).toContain('Invalid verification link!');
413+
});
414+
415+
it('reaches the password reset page when routeAllowList is empty array', async () => {
416+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
417+
await expectForbidden(
418+
request({
419+
method: 'POST',
420+
url: 'http://localhost:8378/1/requestPasswordReset',
421+
headers: {
422+
'X-Parse-Application-Id': 'test',
423+
'X-Parse-REST-API-Key': 'rest',
424+
'Content-Type': 'application/json',
425+
},
426+
body: JSON.stringify({ email: 'user@example.com' }),
427+
})
428+
);
429+
const response = await request({
430+
url: 'http://localhost:8378/1/apps/test/request_password_reset?token=invalidToken',
431+
followRedirects: false,
432+
});
433+
expect(response.status).toBe(200);
434+
expect(response.text).toContain('Invalid password reset link!');
435+
});
436+
437+
it('reaches the resend verification email route when routeAllowList is empty array', async () => {
438+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
439+
const response = await request({
440+
method: 'POST',
441+
url: 'http://localhost:8378/1/apps/test/resend_verification_email',
442+
body: 'username=unknownUser',
443+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
444+
followRedirects: false,
445+
}).catch(e => e);
446+
expect(response.status).toBe(303);
447+
expect(response.headers.location).toContain('email_verification_send_success');
448+
});
449+
450+
it('reaches static pages when routeAllowList is empty array', async () => {
451+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
452+
const response = await request({
453+
url: 'http://localhost:8378/1/apps/password_reset.html',
454+
followRedirects: false,
455+
});
456+
expect(response.status).toBe(200);
457+
expect(response.text).toContain('Reset Your Password');
458+
});
459+
460+
it('reaches Pages routes when routeAllowList contains only REST routes', async () => {
461+
await reconfigureServer({ ...pagesConfig(), routeAllowList: ['classes/AllowedClass'] });
462+
const response = await request({
463+
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken',
464+
followRedirects: false,
465+
});
466+
expect(response.status).toBe(200);
467+
expect(response.text).toContain('Invalid verification link!');
468+
});
469+
470+
it('completes email verification from the emailed link when routeAllowList is empty array', async () => {
471+
const config = { ...pagesConfig(), routeAllowList: [] };
472+
await reconfigureServer(config);
473+
const sendVerificationEmail = spyOn(
474+
config.emailAdapter,
475+
'sendVerificationEmail'
476+
).and.callThrough();
477+
const user = new Parse.User();
478+
user.setUsername('exampleUsername');
479+
user.setPassword('examplePassword');
480+
user.set('email', 'user@example.com');
481+
await user.signUp(null, { useMasterKey: true });
482+
await jasmine.timeout();
483+
const link = sendVerificationEmail.calls.all()[0].args[0].link;
484+
const response = await request({ url: link, followRedirects: false });
485+
expect(response.status).toBe(200);
486+
expect(response.text).toContain('Email verified!');
487+
const verifiedUser = await new Parse.Query(Parse.User)
488+
.equalTo('username', 'exampleUsername')
489+
.first({ useMasterKey: true });
490+
expect(verifiedUser.get('emailVerified')).toBe(true);
491+
});
492+
});
493+
378494
describe('batch sub-requests', () => {
379495
// routeAllowList must be enforced per batch sub-request. The outer
380496
// enforceRouteAllowList middleware runs only on the outer /batch URL,

src/Options/Definitions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ module.exports.ParseServerOptions = {
589589
},
590590
routeAllowList: {
591591
env: 'PARSE_SERVER_ROUTE_ALLOW_LIST',
592-
help: '(Optional) Restricts external client access to a list of allowed REST API routes.<br><br>When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.<br><br>Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.<br><br><b>Examples of normalized route identifiers:</b><ul><li>`classes/GameScore` (class CRUD)</li><li>`classes/GameScore/abc123` (object by ID)</li><li>`users` (user operations)</li><li>`login` (login endpoint)</li><li>`functions/sendEmail` (Cloud Function)</li><li>`jobs/cleanup` (Cloud Job)</li><li>`push` (push notifications)</li><li>`config` (client config)</li><li>`installations` (installations)</li></ul><b>Example patterns:</b><ul><li>`classes/ChatMessage` matches only `classes/ChatMessage`</li><li>`classes/Chat.*` matches `classes/ChatMessage`, `classes/ChatRoom`, etc.</li><li>`functions/.*` matches all Cloud Functions</li></ul>Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).<br><br>When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.<br><br>Defaults to `undefined` which means the feature is inactive and all routes are accessible.<br><br><b>Note:</b> File routes and the GraphQL API are not covered by this option.',
592+
help: '(Optional) Restricts external client access to a list of allowed REST API routes.<br><br>When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.<br><br>Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.<br><br><b>Examples of normalized route identifiers:</b><ul><li>`classes/GameScore` (class CRUD)</li><li>`classes/GameScore/abc123` (object by ID)</li><li>`users` (user operations)</li><li>`login` (login endpoint)</li><li>`functions/sendEmail` (Cloud Function)</li><li>`jobs/cleanup` (Cloud Job)</li><li>`push` (push notifications)</li><li>`config` (client config)</li><li>`installations` (installations)</li></ul><b>Example patterns:</b><ul><li>`classes/ChatMessage` matches only `classes/ChatMessage`</li><li>`classes/Chat.*` matches `classes/ChatMessage`, `classes/ChatRoom`, etc.</li><li>`functions/.*` matches all Cloud Functions</li></ul>Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).<br><br>When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.<br><br>Defaults to `undefined` which means the feature is inactive and all routes are accessible.<br><br><b>Note:</b> File routes, the Pages API and the GraphQL API are not covered by this option.',
593593
action: parsers.arrayParser,
594594
},
595595
scheduledPush: {

src/Options/docs.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)