From 8e99930cb448acf781a36dbca663cd3699cc22bc Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 2 Mar 2023 13:26:58 +1100 Subject: [PATCH 1/8] fix: reject signup with preventLoginWithUnverifiedEmail --- spec/ValidationAndPasswordsReset.spec.js | 17 +++++++++++++++++ src/RestWrite.js | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 3272f07fc3..6421f3bb34 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -345,6 +345,23 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); + it('does not return session token with preventLoginWithUnverified', async () => { + await reconfigureServer({ + appName: 'test', + publicServerURL: 'http://localhost:1337/1', + verifyUserEmails: true, + preventLoginWithUnverifiedEmail: true, + emailAdapter: MockEmailAdapterWithOptions({ + fromAddress: 'parse@example.com', + apiKey: 'k', + domain: 'd', + }), + }); + await expectAsync(Parse.User.signUp('uername', 'password')).toBeRejectedWith( + new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') + ); + }); + it('fails if you include an emailAdapter, set a publicServerURL, but have no appName and send a password reset email', done => { reconfigureServer({ appName: undefined, diff --git a/src/RestWrite.js b/src/RestWrite.js index 3a8385e52a..41fa63d1aa 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -879,7 +879,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = function () { this.config.verifyUserEmails ) { // verification is on - return; // do not create the session token in that case! + throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); } return this.createSessionToken(); }; From 0dd1c716f23524fdda30c15729b38a7c7d226cb7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 2 Mar 2023 13:38:01 +1100 Subject: [PATCH 2/8] refactor --- spec/ValidationAndPasswordsReset.spec.js | 25 +++++++++++++++++------- src/RestWrite.js | 6 +++++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 6421f3bb34..caf363b5f2 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -345,21 +345,32 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it('does not return session token with preventLoginWithUnverified', async () => { + it('does not allow signup with preventLoginWithUnverified', async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; await reconfigureServer({ appName: 'test', publicServerURL: 'http://localhost:1337/1', verifyUserEmails: true, preventLoginWithUnverifiedEmail: true, - emailAdapter: MockEmailAdapterWithOptions({ - fromAddress: 'parse@example.com', - apiKey: 'k', - domain: 'd', - }), + emailAdapter, }); - await expectAsync(Parse.User.signUp('uername', 'password')).toBeRejectedWith( + const newUser = new Parse.User(); + newUser.setPassword('asdf'); + newUser.setUsername('zxcv'); + newUser.set('email', 'test@example.com'); + await expectAsync(newUser.signUp()).toBeRejectedWith( new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') ); + const user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); + expect(user).toBeDefined(); + expect(sendEmailOptions).toBeDefined(); }); it('fails if you include an emailAdapter, set a publicServerURL, but have no appName and send a password reset email', done => { diff --git a/src/RestWrite.js b/src/RestWrite.js index 41fa63d1aa..30c322ec22 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -160,6 +160,9 @@ RestWrite.prototype.execute = function () { this.response.response.authDataResponse = this.authDataResponse; } } + if (this.storage.rejectSignup) { + throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); + } return this.response; }); }; @@ -879,7 +882,8 @@ RestWrite.prototype.createSessionTokenIfNeeded = function () { this.config.verifyUserEmails ) { // verification is on - throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); + this.storage.rejectSignup = true; + return; } return this.createSessionToken(); }; From ea508951e3571d4c0eec000bbcfd16fbf5597be8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 2 Mar 2023 14:16:02 +1100 Subject: [PATCH 3/8] refactor tests --- spec/ValidationAndPasswordsReset.spec.js | 74 ++++++++---------------- spec/VerifyUserPassword.spec.js | 42 ++++++-------- 2 files changed, 43 insertions(+), 73 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index caf363b5f2..6462da60ba 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -242,8 +242,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', done => { - const user = new Parse.User(); + it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', async () => { let sendEmailOptions; const emailAdapter = { sendVerificationEmail: options => { @@ -252,59 +251,34 @@ describe('Custom Pages, Email Verification, Password Reset', () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => {}, }; - reconfigureServer({ + await reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, preventLoginWithUnverifiedEmail: true, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setPassword('other-password'); - user.setUsername('user'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => { - expect(sendEmailOptions).not.toBeUndefined(); - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' - ); - user - .fetch({ useMasterKey: true }) - .then( - () => { - expect(user.get('emailVerified')).toEqual(true); - - Parse.User.logIn('user', 'other-password').then( - user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); - done(); - }, - () => { - fail('login should have succeeded'); - done(); - } - ); - }, - err => { - jfail(err); - fail('this should not fail'); - done(); - } - ) - .catch(err => { - jfail(err); - done(); - }); - }); - }); + }); + let user = new Parse.User(); + user.setPassword('other-password'); + user.setUsername('user'); + user.set('email', 'user@example.com'); + await expectAsync(user.signUp()).toBeRejectedWith( + new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') + ); + expect(sendEmailOptions).not.toBeUndefined(); + const response = await request({ + url: sendEmailOptions.link, + followRedirects: false, + }); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + ); + user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); + expect(user.get('emailVerified')).toEqual(true); + user = await Parse.User.logIn('user', 'other-password'); + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); }); it('allows user to login if email is not verified but preventLoginWithUnverifiedEmail is set to false', done => { diff --git a/spec/VerifyUserPassword.spec.js b/spec/VerifyUserPassword.spec.js index 6734dcdb71..425a85c99f 100644 --- a/spec/VerifyUserPassword.spec.js +++ b/spec/VerifyUserPassword.spec.js @@ -353,8 +353,9 @@ describe('Verify User Password', () => { done(); }); }); - it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', done => { - reconfigureServer({ + + it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', async () => { + await reconfigureServer({ publicServerURL: 'http://localhost:8378/', appName: 'emailVerify', verifyUserEmails: true, @@ -364,28 +365,23 @@ describe('Verify User Password', () => { apiKey: 'k', domain: 'd', }), - }) - .then(() => { - const user = new Parse.User(); - return user.save({ - username: 'unverified-user', - password: 'mypass', - email: 'unverified-email@user.com', - }); - }) - .then(() => { - return verifyPassword('unverified-email@user.com', 'mypass', true); - }) - .then(res => { - expect(res.status).toBe(400); - expect(res.text).toMatch('{"code":205,"error":"User email is not verified."}'); - done(); - }) - .catch(err => { - fail(err); - done(); - }); + }); + const user = new Parse.User(); + await expectAsync( + user.save({ + username: 'unverified-user', + password: 'mypass', + email: 'unverified-email@example.com', + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.')); + const res = await verifyPassword('unverified-email@example.com', 'mypass', true); + expect(res.status).toBe(400); + expect(res.data).toEqual({ + code: Parse.Error.EMAIL_NOT_FOUND, + error: 'User email is not verified.', + }); }); + it('verify password lock account if failed verify password attempts are above threshold', done => { reconfigureServer({ appName: 'lockout threshold', From 26205be44e4e7256519e60807d43d0c9fd3ddd75 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 29 Mar 2023 11:30:31 +1100 Subject: [PATCH 4/8] refactor --- spec/ValidationAndPasswordsReset.spec.js | 3 ++- spec/VerifyUserPassword.spec.js | 12 +++++------- src/Options/index.js | 7 +++++++ src/RestWrite.js | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 6462da60ba..b9ce176c18 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -319,7 +319,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it('does not allow signup with preventLoginWithUnverified', async () => { + it('does not allow signup with preventSignupWithUnverified', async () => { let sendEmailOptions; const emailAdapter = { sendVerificationEmail: options => { @@ -333,6 +333,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:1337/1', verifyUserEmails: true, preventLoginWithUnverifiedEmail: true, + preventSignupWithUnverifiedEmail: true, emailAdapter, }); const newUser = new Parse.User(); diff --git a/spec/VerifyUserPassword.spec.js b/spec/VerifyUserPassword.spec.js index 425a85c99f..eef2485815 100644 --- a/spec/VerifyUserPassword.spec.js +++ b/spec/VerifyUserPassword.spec.js @@ -367,13 +367,11 @@ describe('Verify User Password', () => { }), }); const user = new Parse.User(); - await expectAsync( - user.save({ - username: 'unverified-user', - password: 'mypass', - email: 'unverified-email@example.com', - }) - ).toBeRejectedWith(new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.')); + await user.save({ + username: 'unverified-user', + password: 'mypass', + email: 'unverified-email@example.com', + }); const res = await verifyPassword('unverified-email@example.com', 'mypass', true); expect(res.status).toBe(400); expect(res.data).toEqual({ diff --git a/src/Options/index.js b/src/Options/index.js index a4d83f94fc..267f2ccc81 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -164,6 +164,13 @@ export interface ParseServerOptions { Requires option `verifyUserEmails: true`. :DEFAULT: false */ preventLoginWithUnverifiedEmail: ?boolean; + /* Set to `true` to prevent a user from signing up if the email has not yet been verified and email verification is required. +

+ Default is `false`. +
+ Requires option `verifyUserEmails: true`. + :DEFAULT: false */ + preventSignupWithUnverifiedEmail: ?boolean; /* Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours). diff --git a/src/RestWrite.js b/src/RestWrite.js index 30c322ec22..7b16314b12 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -160,7 +160,7 @@ RestWrite.prototype.execute = function () { this.response.response.authDataResponse = this.authDataResponse; } } - if (this.storage.rejectSignup) { + if (this.storage.rejectSignup && this.config.preventSignupWithUnverifiedEmail) { throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); } return this.response; From 20601d02b22d853402c0f48771b77f7a70aa56f2 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 29 Mar 2023 11:32:41 +1100 Subject: [PATCH 5/8] definitions --- src/Options/Definitions.js | 7 +++++++ src/Options/docs.js | 1 + 2 files changed, 8 insertions(+) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index b2f0542256..ae6a22fe1a 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -404,6 +404,13 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: false, }, + preventSignupWithUnverifiedEmail: { + env: 'PARSE_SERVER_PREVENT_SIGNUP_WITH_UNVERIFIED_EMAIL', + help: + 'Set to `true` to prevent a user from signing up if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`.', + action: parsers.booleanParser, + default: false, + }, protectedFields: { env: 'PARSE_SERVER_PROTECTED_FIELDS', help: 'Protected fields that should be treated with extra security when fetching details.', diff --git a/src/Options/docs.js b/src/Options/docs.js index 1ab8c03d58..2e3dd28d98 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -76,6 +76,7 @@ * @property {Number} port The port to run the ParseServer, defaults to 1337. * @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names * @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`. + * @property {Boolean} preventSignupWithUnverifiedEmail Set to `true` to prevent a user from signing up if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. * @property {String} publicServerURL Public URL to your parse server with http:// or https://. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications From facaf93f900e384f320be7f052896ad7b397f476 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 29 Mar 2023 12:23:10 +1100 Subject: [PATCH 6/8] Update ValidationAndPasswordsReset.spec.js --- spec/ValidationAndPasswordsReset.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index b9ce176c18..ab944e14c1 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -262,9 +262,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { user.setPassword('other-password'); user.setUsername('user'); user.set('email', 'user@example.com'); - await expectAsync(user.signUp()).toBeRejectedWith( - new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') - ); + await user.signUp(); expect(sendEmailOptions).not.toBeUndefined(); const response = await request({ url: sendEmailOptions.link, From e6e502e806a382c442d25998fe3039e6c880f6bf Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 8 Jun 2023 00:11:04 +1000 Subject: [PATCH 7/8] Update src/Options/index.js Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com> Signed-off-by: Daniel --- src/Options/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/index.js b/src/Options/index.js index 622bf19320..7d43447b1b 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -165,7 +165,7 @@ export interface ParseServerOptions { Requires option `verifyUserEmails: true`. :DEFAULT: false */ preventLoginWithUnverifiedEmail: ?boolean; - /* Set to `true` to prevent a user from signing up if the email has not yet been verified and email verification is required. + /* If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
From 18b920dc3d56990bd2453f77c81ec61f6bee97df Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 8 Jun 2023 00:12:07 +1000 Subject: [PATCH 8/8] definitions --- src/Options/Definitions.js | 2 +- src/Options/docs.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 0631baee78..31700b4cc2 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -415,7 +415,7 @@ module.exports.ParseServerOptions = { preventSignupWithUnverifiedEmail: { env: 'PARSE_SERVER_PREVENT_SIGNUP_WITH_UNVERIFIED_EMAIL', help: - 'Set to `true` to prevent a user from signing up if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`.', + "If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`.", action: parsers.booleanParser, default: false, }, diff --git a/src/Options/docs.js b/src/Options/docs.js index c0601ac139..b1bf31a5e7 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -77,7 +77,7 @@ * @property {Number} port The port to run the ParseServer, defaults to 1337. * @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names * @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`. - * @property {Boolean} preventSignupWithUnverifiedEmail Set to `true` to prevent a user from signing up if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`. + * @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. * @property {String} publicServerURL Public URL to your parse server with http:// or https://. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications