diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js
index 1a235d8d13..15c317a94c 100644
--- a/spec/EmailVerificationToken.spec.js
+++ b/spec/EmailVerificationToken.spec.js
@@ -298,7 +298,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
method(req) {
- expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
+ expect(Object.keys(req)).toEqual([
+ 'original',
+ 'object',
+ 'master',
+ 'ip',
+ 'installationId',
+ 'createdWith',
+ ]);
+ expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
return false;
},
};
@@ -359,7 +367,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
method(req) {
- expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
+ expect(Object.keys(req)).toEqual([
+ 'original',
+ 'object',
+ 'master',
+ 'ip',
+ 'installationId',
+ 'createdWith',
+ ]);
+ expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
if (req.object.get('username') === 'no_email') {
return false;
}
@@ -394,6 +410,71 @@ describe('Email Verification Token Expiration:', () => {
expect(verifySpy).toHaveBeenCalledTimes(5);
});
+ it('provides createdWith on signup when verification blocks session creation', async () => {
+ const verifyUserEmails = {
+ method: params => {
+ expect(params.object).toBeInstanceOf(Parse.User);
+ expect(params.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
+ return true;
+ },
+ };
+ const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
+ await reconfigureServer({
+ appName: 'emailVerifyToken',
+ verifyUserEmails: verifyUserEmails.method,
+ preventLoginWithUnverifiedEmail: true,
+ preventSignupWithUnverifiedEmail: true,
+ emailAdapter: MockEmailAdapterWithOptions({
+ fromAddress: 'parse@example.com',
+ apiKey: 'k',
+ domain: 'd',
+ }),
+ publicServerURL: 'http://localhost:8378/1',
+ });
+
+ const user = new Parse.User();
+ user.setUsername('signup_created_with');
+ user.setPassword('pass');
+ user.setEmail('signup@example.com');
+ const res = await user.signUp().catch(e => e);
+ expect(res.message).toBe('User email is not verified.');
+ expect(user.getSessionToken()).toBeUndefined();
+ expect(verifySpy).toHaveBeenCalledTimes(2); // before signup completion and on preventLoginWithUnverifiedEmail
+ });
+
+ it('provides createdWith with auth provider on login verification', async () => {
+ const user = new Parse.User();
+ user.setUsername('user_created_with_login');
+ user.setPassword('pass');
+ user.set('email', 'login@example.com');
+ await user.signUp();
+
+ const verifyUserEmails = {
+ method: async params => {
+ expect(params.object).toBeInstanceOf(Parse.User);
+ expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
+ return true;
+ },
+ };
+ const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
+ await reconfigureServer({
+ appName: 'emailVerifyToken',
+ publicServerURL: 'http://localhost:8378/1',
+ verifyUserEmails: verifyUserEmails.method,
+ preventLoginWithUnverifiedEmail: verifyUserEmails.method,
+ preventSignupWithUnverifiedEmail: true,
+ emailAdapter: MockEmailAdapterWithOptions({
+ fromAddress: 'parse@example.com',
+ apiKey: 'k',
+ domain: 'd',
+ }),
+ });
+
+ const res = await Parse.User.logIn('user_created_with_login', 'pass').catch(e => e);
+ expect(res.code).toBe(205);
+ expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2); // before login completion and on preventLoginWithUnverifiedEmail
+ });
+
it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => {
const emailAdapter = {
sendVerificationEmail: () => {},
@@ -797,6 +878,7 @@ describe('Email Verification Token Expiration:', () => {
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
expect(params.resendRequest).toBeTrue();
+ expect(params.createdWith).toBeUndefined();
return true;
},
};
diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js
index 3f6d4048c5..f5bbe2d48d 100644
--- a/spec/ValidationAndPasswordsReset.spec.js
+++ b/spec/ValidationAndPasswordsReset.spec.js
@@ -284,6 +284,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
expect(params.ip).toBeDefined();
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
+ expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true;
},
};
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index 66c1d8bcea..4a1264dee4 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -481,7 +481,6 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL',
help:
'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`.',
- action: parsers.booleanParser,
default: false,
},
preventSignupWithUnverifiedEmail: {
@@ -637,7 +636,7 @@ module.exports.ParseServerOptions = {
verifyUserEmails: {
env: 'PARSE_SERVER_VERIFY_USER_EMAILS',
help:
- 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
Default is `false`.',
+ 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.
Default is `false`.',
default: false,
},
webhookKey: {
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 9569239ef7..a56cdf2d8f 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -109,7 +109,7 @@
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose
* @property {Boolean} verifyServerUrl Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.
⚠️ Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.
Default is `true`.
- * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
Default is `false`.
+ * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.
Default is `false`.
* @property {String} webhookKey Key sent with outgoing webhook calls
*/
diff --git a/src/Options/index.js b/src/Options/index.js
index cdeb7cd846..0dedafb3ab 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -43,6 +43,18 @@ type RequestKeywordDenylist = {
key: string | any,
value: any,
};
+type EmailVerificationRequest = {
+ original?: any,
+ object: any,
+ master?: boolean,
+ ip?: string,
+ installationId?: string,
+ createdWith?: {
+ action: 'login' | 'signup',
+ authProvider: string,
+ },
+ resendRequest?: boolean,
+};
export interface ParseServerOptions {
/* Your Parse Application ID
@@ -174,18 +186,21 @@ export interface ParseServerOptions {
/* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */
maxUploadSize: ?string;
- /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
+ /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.
Default is `false`.
:DEFAULT: false */
- verifyUserEmails: ?(boolean | void);
+ verifyUserEmails: ?(boolean | (EmailVerificationRequest => boolean | Promise));
/* 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`.
:DEFAULT: false */
- preventLoginWithUnverifiedEmail: ?boolean;
+ preventLoginWithUnverifiedEmail: ?(
+ | boolean
+ | (EmailVerificationRequest => boolean | Promise)
+ );
/* 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`.
diff --git a/src/RestWrite.js b/src/RestWrite.js
index a0de5577a5..9e220fb741 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -771,6 +771,28 @@ RestWrite.prototype._validateUserName = function () {
});
};
+RestWrite.prototype.getCreatedWith = function () {
+ if (this.storage.createdWith) {
+ return this.storage.createdWith;
+ }
+ const isCreateOperation = !this.query;
+ // Determine authProvider: from stored authProvider or authData keys (e.g., anonymous, facebook).
+ // Default to 'password' on signup with no authData so createdWith aligns with legacy expectations/tests.
+ const authProvider =
+ this.storage.authProvider ||
+ (this.data &&
+ this.data.authData &&
+ Object.keys(this.data.authData).length &&
+ Object.keys(this.data.authData).join(','));
+ const action = authProvider ? 'login' : isCreateOperation ? 'signup' : undefined;
+ if (!action) {
+ return;
+ }
+ const resolvedAuthProvider = authProvider || (action === 'signup' ? 'password' : undefined);
+ this.storage.createdWith = { action, authProvider: resolvedAuthProvider };
+ return this.storage.createdWith;
+};
+
/*
As with usernames, Parse should not allow case insensitive collisions of email.
unlike with usernames (which can have case insensitive collisions in the case of
@@ -826,6 +848,7 @@ RestWrite.prototype._validateEmail = function () {
master: this.auth.isMaster,
ip: this.config.ip,
installationId: this.auth.installationId,
+ createdWith: this.getCreatedWith(),
};
return this.config.userController.setEmailVerifyToken(this.data, request, this.storage);
}
@@ -961,6 +984,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () {
master: this.auth.isMaster,
ip: this.config.ip,
installationId: this.auth.installationId,
+ createdWith: this.getCreatedWith(),
};
// Get verification conditions which can be booleans or functions; the purpose of this async/await
// structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the
@@ -987,12 +1011,14 @@ RestWrite.prototype.createSessionToken = async function () {
this.storage.authProvider = Object.keys(this.data.authData).join(',');
}
- const { sessionData, createSession } = RestWrite.createSession(this.config, {
- userId: this.objectId(),
- createdWith: {
+ const createdWith =
+ this.getCreatedWith() || {
action: this.storage.authProvider ? 'login' : 'signup',
authProvider: this.storage.authProvider || 'password',
- },
+ };
+ const { sessionData, createSession } = RestWrite.createSession(this.config, {
+ userId: this.objectId(),
+ createdWith,
installationId: this.auth.installationId,
});
diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js
index 3828e465e7..d725ef7704 100644
--- a/src/Routers/UsersRouter.js
+++ b/src/Routers/UsersRouter.js
@@ -140,11 +140,20 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
}
// Create request object for verification functions
+ const authProvider =
+ req.body &&
+ req.body.authData &&
+ Object.keys(req.body.authData).length &&
+ Object.keys(req.body.authData).join(',');
const request = {
master: req.auth.isMaster,
ip: req.config.ip,
installationId: req.auth.installationId,
object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)),
+ createdWith: {
+ action: 'login',
+ authProvider: authProvider || 'password',
+ },
};
// If request doesn't use master or maintenance key with ignoring email verification
diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts
index ad11050648..c4c62bd7e5 100644
--- a/types/Options/index.d.ts
+++ b/types/Options/index.d.ts
@@ -26,6 +26,18 @@ type RequestKeywordDenylist = {
key: string;
value: any;
};
+export interface VerifyUserEmailsRequest {
+ original?: any;
+ object: any;
+ master?: boolean;
+ ip?: string;
+ installationId?: string;
+ createdWith?: {
+ action: 'login' | 'signup';
+ authProvider: string;
+ };
+ resendRequest?: boolean;
+}
export interface ParseServerOptions {
appId: string;
masterKey: (() => void) | string;
@@ -74,8 +86,8 @@ export interface ParseServerOptions {
auth?: Record;
enableInsecureAuthAdapters?: boolean;
maxUploadSize?: string;
- verifyUserEmails?: (boolean | void);
- preventLoginWithUnverifiedEmail?: boolean;
+ verifyUserEmails?: boolean | ((params: VerifyUserEmailsRequest) => boolean | Promise);
+ preventLoginWithUnverifiedEmail?: boolean | ((params: VerifyUserEmailsRequest) => boolean | Promise);
preventSignupWithUnverifiedEmail?: boolean;
emailVerifyTokenValidityDuration?: number;
emailVerifyTokenReuseIfValid?: boolean;