Skip to content

Commit dbc31c4

Browse files
authored
build: Release (#10075)
2 parents 8bd7a22 + 257a73c commit dbc31c4

19 files changed

Lines changed: 256 additions & 179 deletions

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,29 @@ jobs:
111111
- name: Install dependencies
112112
run: npm ci
113113
- run: npm run madge:circular
114+
check-docs:
115+
name: Docs
116+
timeout-minutes: 5
117+
runs-on: ubuntu-latest
118+
steps:
119+
- uses: actions/checkout@v4
120+
- name: Use Node.js ${{ matrix.NODE_VERSION }}
121+
uses: actions/setup-node@v4
122+
with:
123+
node-version: ${{ matrix.node-version }}
124+
- name: Cache Node.js modules
125+
uses: actions/cache@v4
126+
with:
127+
path: ~/.npm
128+
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
129+
restore-keys: |
130+
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
131+
- name: Install dependencies
132+
run: npm ci
133+
- name: Build source
134+
run: npm run build
135+
- name: Generate docs
136+
run: npm run docs
114137
check-docker:
115138
name: Docker Build
116139
timeout-minutes: 15

README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,6 @@ const api = new ParseServer({
388388
...otherOptions,
389389

390390
pages: {
391-
enableRouter: true,
392391
customRoutes: [{
393392
method: 'GET',
394393
path: 'custom_route',
@@ -425,7 +424,6 @@ The following paths are already used by Parse Server's built-in features and are
425424
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
426425
| ---------------------------- | -------- | --------------- | ------------- | --------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
427426
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
428-
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
429427
| `pages.customRoutes` | yes | `Array` | `[]` | - | `PARSE_SERVER_PAGES_CUSTOM_ROUTES` | The custom routes. The routes are added in the order they are defined here, which has to be considered since requests traverse routes in an ordered manner. Custom routes are traversed after build-in routes such as password reset and email verification. |
430428
| `pages.customRoutes.method` | | `String` | - | `GET`, `POST` | - | The HTTP method of the custom route. |
431429
| `pages.customRoutes.path` | | `String` | - | `custom_page` | - | The path of the custom route. Note that the same path can used if the `method` is different, for example a path `custom_page` can have two routes, a `GET` and `POST` route, which will be invoked depending on the HTTP request method. |
@@ -582,7 +580,6 @@ const api = new ParseServer({
582580
...otherOptions,
583581
584582
pages: {
585-
enableRouter: true,
586583
enableLocalization: true,
587584
}
588585
}
@@ -635,7 +632,6 @@ const api = new ParseServer({
635632
...otherOptions,
636633
637634
pages: {
638-
enableRouter: true,
639635
enableLocalization: true,
640636
customUrls: {
641637
passwordReset: 'https://example.com/page.html'
@@ -697,7 +693,6 @@ const api = new ParseServer({
697693
...otherOptions,
698694

699695
pages: {
700-
enableRouter: true,
701696
enableLocalization: true,
702697
localizationJsonPath: './private/localization.json',
703698
localizationFallbackLocale: 'en'
@@ -725,7 +720,6 @@ const api = new ParseServer({
725720
...otherOptions,
726721

727722
pages: {
728-
enableRouter: true,
729723
placeholders: {
730724
exampleKey: 'exampleValue'
731725
}
@@ -740,7 +734,6 @@ const api = new ParseServer({
740734
...otherOptions,
741735

742736
pages: {
743-
enableRouter: true,
744737
placeholders: async (params) => {
745738
const value = await doSomething(params.locale);
746739
return {
@@ -760,7 +753,6 @@ The following parameter and placeholder keys are reserved because they are used
760753
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
761754
| ----------------------------------------------- | -------- | ------------------------------------- | -------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
762755
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
763-
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
764756
| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. |
765757
| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. |
766758
| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. |

changelogs/CHANGELOG_alpha.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
## [9.3.1-alpha.4](https://github.com/parse-community/parse-server/compare/9.3.1-alpha.3...9.3.1-alpha.4) (2026-02-23)
2+
3+
4+
### Bug Fixes
5+
6+
* JWT Algorithm Confusion in Google Auth Adapter ([GHSA-4q3h-vp4r-prv2](https://github.com/parse-community/parse-server/security/advisories/GHSA-4q3h-vp4r-prv2)) ([#10072](https://github.com/parse-community/parse-server/issues/10072)) ([9d5942d](https://github.com/parse-community/parse-server/commit/9d5942d50e55c822924c27b05aa98f1393e7a330))
7+
8+
## [9.3.1-alpha.3](https://github.com/parse-community/parse-server/compare/9.3.1-alpha.2...9.3.1-alpha.3) (2026-02-23)
9+
10+
11+
### Bug Fixes
12+
13+
* GraphQL introspection disabled in `NODE_ENV=production` even with master key ([#10071](https://github.com/parse-community/parse-server/issues/10071)) ([a5269f0](https://github.com/parse-community/parse-server/commit/a5269f077666537fad1d2eeefee82a36a148255c))
14+
15+
## [9.3.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.3.1-alpha.1...9.3.1-alpha.2) (2026-02-21)
16+
17+
18+
### Bug Fixes
19+
20+
* Remove obsolete Parse Server option `pages.enableRouter` ([#10070](https://github.com/parse-community/parse-server/issues/10070)) ([00b3b72](https://github.com/parse-community/parse-server/commit/00b3b7297d806b4b40d7c08dd987b748e018e4b6))
21+
22+
## [9.3.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.3.0...9.3.1-alpha.1) (2026-02-21)
23+
24+
25+
### Bug Fixes
26+
27+
* Type error in docs creation ([#10069](https://github.com/parse-community/parse-server/issues/10069)) ([02a277f](https://github.com/parse-community/parse-server/commit/02a277f1e937fd3e6bd85bdb49870bf3f47678a0))
28+
129
# [9.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.8...9.3.0-alpha.9) (2026-02-21)
230

331

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "9.3.0",
3+
"version": "9.3.1-alpha.4",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {

spec/AuthenticationAdapters.spec.js

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -500,19 +500,60 @@ describe('google auth adapter', () => {
500500
}
501501
});
502502

503-
// it('should throw error if public key used to encode token is not available', async () => {
504-
// const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
505-
// try {
506-
// spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
507-
508-
// await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
509-
// fail();
510-
// } catch (e) {
511-
// expect(e.message).toBe(
512-
// `Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
513-
// );
514-
// }
515-
// });
503+
it('should reject forged alg:none JWT from advisory PoC (GHSA-4q3h-vp4r-prv2)', async () => {
504+
const header = Buffer.from('{"alg":"none","kid":"nonexistent-key","typ":"JWT"}').toString('base64url');
505+
const payload = Buffer.from('{"sub":"the_user_id","iss":"accounts.google.com","aud":"secret","exp":9999999999}').toString('base64url');
506+
const forgedToken = `${header}.${payload}.`;
507+
508+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
509+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
510+
511+
try {
512+
await google.validateAuthData(
513+
{ id: 'the_user_id', id_token: forgedToken },
514+
{ clientId: 'secret' }
515+
);
516+
fail('should have rejected forged token');
517+
} catch (e) {
518+
expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
519+
}
520+
});
521+
522+
it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg', async () => {
523+
const fakeClaim = {
524+
iss: 'https://accounts.google.com',
525+
aud: 'secret',
526+
exp: Date.now(),
527+
sub: 'the_user_id',
528+
};
529+
const fakeDecodedToken = { kid: '123', alg: 'ES256' };
530+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
531+
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
532+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
533+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
534+
535+
await google.validateAuthData(
536+
{ id: 'the_user_id', id_token: 'the_token' },
537+
{ clientId: 'secret' }
538+
);
539+
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
540+
});
541+
542+
it('should throw error if Google signing key is not found', async () => {
543+
const fakeDecodedToken = { kid: '789', alg: 'RS256' };
544+
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
545+
spyOn(authUtils, 'getSigningKey').and.rejectWith(new Error('key not found'));
546+
547+
try {
548+
await google.validateAuthData(
549+
{ id: 'the_user_id', id_token: 'the_token' },
550+
{ clientId: 'secret' }
551+
);
552+
fail('should have thrown');
553+
} catch (e) {
554+
expect(e.message).toBe('Unable to find matching key for Key ID: 789');
555+
}
556+
});
516557

517558
it('(using client id as string) should verify id_token (google.com)', async () => {
518559
const fakeClaim = {
@@ -521,8 +562,10 @@ describe('google auth adapter', () => {
521562
exp: Date.now(),
522563
sub: 'the_user_id',
523564
};
524-
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
565+
const fakeDecodedToken = { kid: '123', alg: 'RS256' };
566+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
525567
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
568+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
526569
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
527570

528571
const result = await google.validateAuthData(
@@ -537,8 +580,10 @@ describe('google auth adapter', () => {
537580
iss: 'https://not.google.com',
538581
sub: 'the_user_id',
539582
};
540-
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
583+
const fakeDecodedToken = { kid: '123', alg: 'RS256' };
584+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
541585
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
586+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
542587
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
543588

544589
try {
@@ -561,8 +606,10 @@ describe('google auth adapter', () => {
561606
exp: Date.now(),
562607
sub: 'the_user_id',
563608
};
564-
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
609+
const fakeDecodedToken = { kid: '123', alg: 'RS256' };
610+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
565611
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
612+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
566613
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
567614

568615
try {
@@ -583,8 +630,10 @@ describe('google auth adapter', () => {
583630
exp: Date.now(),
584631
sub: 'the_user_id',
585632
};
586-
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
633+
const fakeDecodedToken = { kid: '123', alg: 'RS256' };
634+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
587635
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
636+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
588637
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
589638

590639
try {
@@ -897,7 +946,27 @@ describe('apple signin auth adapter', () => {
897946
{ clientId: 'secret' }
898947
);
899948
expect(result).toEqual(fakeClaim);
900-
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
949+
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
950+
});
951+
952+
it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg (GHSA-4q3h-vp4r-prv2)', async () => {
953+
const fakeClaim = {
954+
iss: 'https://appleid.apple.com',
955+
aud: 'secret',
956+
exp: Date.now(),
957+
sub: 'the_user_id',
958+
};
959+
const fakeDecodedToken = { kid: '123', alg: 'none' };
960+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
961+
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
962+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
963+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
964+
965+
await apple.validateAuthData(
966+
{ id: 'the_user_id', token: 'the_token' },
967+
{ clientId: 'secret' }
968+
);
969+
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
901970
});
902971

903972
it('should not verify invalid id_token', async () => {
@@ -1236,7 +1305,27 @@ describe('facebook limited auth adapter', () => {
12361305
{ clientId: 'secret' }
12371306
);
12381307
expect(result).toEqual(fakeClaim);
1239-
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
1308+
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
1309+
});
1310+
1311+
it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg (GHSA-4q3h-vp4r-prv2)', async () => {
1312+
const fakeClaim = {
1313+
iss: 'https://www.facebook.com',
1314+
aud: 'secret',
1315+
exp: Date.now(),
1316+
sub: 'the_user_id',
1317+
};
1318+
const fakeDecodedToken = { kid: '123', alg: 'none' };
1319+
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
1320+
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
1321+
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
1322+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1323+
1324+
await facebook.validateAuthData(
1325+
{ id: 'the_user_id', token: 'the_token' },
1326+
{ clientId: 'secret' }
1327+
);
1328+
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
12401329
});
12411330

12421331
it('should not verify invalid id_token', async () => {

0 commit comments

Comments
 (0)