Skip to content

Commit 486a405

Browse files
committed
feat: added autosignuponlogin
1 parent f62da3f commit 486a405

File tree

7 files changed

+153
-1
lines changed

7 files changed

+153
-1
lines changed

spec/ParseUser.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,32 @@ describe('Parse.User testing', () => {
186186
});
187187
});
188188

189+
it('auto signs up user on login when enabled', async () => {
190+
await reconfigureServer({ autoSignupOnLogin: true });
191+
const username = 'autoLoginUser';
192+
const password = 'autoLoginPass';
193+
const response = await request({
194+
method: 'POST',
195+
url: 'http://localhost:8378/1/login',
196+
headers: {
197+
'X-Parse-Application-Id': Parse.applicationId,
198+
'X-Parse-REST-API-Key': 'rest',
199+
'Content-Type': 'application/json',
200+
},
201+
body: {
202+
username,
203+
password,
204+
},
205+
});
206+
expect(response.data.username).toBe(username);
207+
expect(response.data.sessionToken).toBeDefined();
208+
209+
const user = await Parse.User.logIn(username, password);
210+
expect(user).toBeDefined();
211+
await Parse.User.logOut();
212+
await reconfigureServer({ autoSignupOnLogin: false });
213+
});
214+
189215
it('user login', async done => {
190216
await Parse.User.signUp('asdf', 'zxcv');
191217
const user = await Parse.User.logIn('asdf', 'zxcv');

src/Config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export class Config {
8585
pages,
8686
security,
8787
enforcePrivateUsers,
88+
autoSignupOnLogin,
8889
enableInsecureAuthAdapters,
8990
schema,
9091
requestKeywordDenylist,
@@ -131,6 +132,7 @@ export class Config {
131132
this.validateSecurityOptions(security);
132133
this.validateSchemaOptions(schema);
133134
this.validateEnforcePrivateUsers(enforcePrivateUsers);
135+
this.validateAutoSignupOnLogin(autoSignupOnLogin);
134136
this.validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters);
135137
this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
136138
this.validateRequestKeywordDenylist(requestKeywordDenylist);
@@ -183,6 +185,12 @@ export class Config {
183185
}
184186
}
185187

188+
static validateAutoSignupOnLogin(autoSignupOnLogin) {
189+
if (typeof autoSignupOnLogin !== 'boolean') {
190+
throw 'Parse Server option autoSignupOnLogin must be a boolean.';
191+
}
192+
}
193+
186194
static validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken) {
187195
if (typeof allowExpiredAuthDataToken !== 'boolean') {
188196
throw 'Parse Server option allowExpiredAuthDataToken must be a boolean.';

src/Options/Definitions.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,13 @@ module.exports.ParseServerOptions = {
482482
action: parsers.booleanParser,
483483
default: false,
484484
},
485+
autoSignupOnLogin: {
486+
env: 'PARSE_SERVER_AUTO_SIGNUP_ON_LOGIN',
487+
help:
488+
'Set to `true` to automatically create a user when calling the login endpoint with username/email and password if no matching user exists.<br><br>Default is `false`.',
489+
action: parsers.booleanParser,
490+
default: false,
491+
},
485492
protectedFields: {
486493
env: 'PARSE_SERVER_PROTECTED_FIELDS',
487494
help: 'Protected fields that should be treated with extra security when fetching details.',

src/Options/docs.js

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

src/Options/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ export interface ParseServerOptions {
190190
Requires option `verifyUserEmails: true`.
191191
:DEFAULT: false */
192192
preventSignupWithUnverifiedEmail: ?boolean;
193+
/* Set to `true` to automatically create a user when calling the login endpoint with username/email and password if no matching user exists.
194+
<br><br>
195+
Default is `false`.
196+
:DEFAULT: false */
197+
autoSignupOnLogin: ?boolean;
193198
/* 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.
194199
<br><br>
195200
For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

src/Routers/UsersRouter.js

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,23 @@ export class UsersRouter extends ClassesRouter {
199199
}
200200

201201
async handleLogIn(req) {
202-
const user = await this._authenticateUserFromRequest(req);
203202
const authData = req.body && req.body.authData;
203+
let user;
204+
let signupSessionToken;
205+
206+
try {
207+
user = await this._authenticateUserFromRequest(req);
208+
} catch (error) {
209+
const autoSignupCredentials = this._prepareAutoSignupCredentials(req, error);
210+
if (!autoSignupCredentials) {
211+
throw error;
212+
}
213+
// Create the missing user but continue through the standard login path so
214+
// that all login-time policies, triggers, and session metadata remain unchanged.
215+
signupSessionToken = await this._autoSignupOnLogin(req, autoSignupCredentials);
216+
user = await this._authenticateUserFromRequest(req);
217+
}
218+
204219
// Check if user has provided their required auth providers
205220
Auth.checkIfUserHasProvidedConfiguredProvidersForLogin(
206221
req,
@@ -299,6 +314,18 @@ export class UsersRouter extends ClassesRouter {
299314

300315
await createSession();
301316

317+
if (signupSessionToken) {
318+
// Discard the session issued by the signup shortcut; the login session we just
319+
// created is the single source of truth for the client.
320+
try {
321+
await req.config.database.destroy('_Session', { sessionToken: signupSessionToken });
322+
} catch (sessionError) {
323+
if (sessionError && sessionError.code !== Parse.Error.OBJECT_NOT_FOUND) {
324+
logger.warn('Failed to clean up auto sign-up session token', sessionError);
325+
}
326+
}
327+
}
328+
302329
const afterLoginUser = Parse.User.fromJSON(Object.assign({ className: '_User' }, user));
303330
await maybeRunTrigger(
304331
TriggerTypes.afterLogin,
@@ -317,6 +344,83 @@ export class UsersRouter extends ClassesRouter {
317344
return { response: user };
318345
}
319346

347+
348+
_getLoginPayload(req) {
349+
let source = req.body || {};
350+
if (
351+
(!source.username && req.query && req.query.username) ||
352+
(!source.email && req.query && req.query.email)
353+
) {
354+
source = req.query;
355+
}
356+
return {
357+
username: source.username,
358+
email: source.email,
359+
password: source.password,
360+
};
361+
}
362+
363+
// Returns data for auto-signup if autoSignupOnLogin is true and the error is that the user doesn't exist.
364+
// If the conditions don't match, we return `null`.
365+
// This gathers minimal credentials so that the signup path can rely on RestWrite's own validation.
366+
_prepareAutoSignupCredentials(req, error) {
367+
if (!req.config.autoSignupOnLogin) {
368+
return null;
369+
}
370+
if (!(error instanceof Parse.Error) || error.code !== Parse.Error.OBJECT_NOT_FOUND) {
371+
return null;
372+
}
373+
if (req.body && req.body.authData) {
374+
return null;
375+
}
376+
const payload = this._getLoginPayload(req);
377+
const rawUsername = typeof payload.username === 'string' ? payload.username.trim() : '';
378+
const rawEmail = typeof payload.email === 'string' ? payload.email.trim() : '';
379+
const password = payload.password;
380+
const hasUsername = rawUsername.length > 0;
381+
const hasEmail = rawEmail.length > 0;
382+
if (!hasUsername && !hasEmail) {
383+
return null;
384+
}
385+
if (typeof password !== 'string') {
386+
return null;
387+
}
388+
return {
389+
username: hasUsername ? rawUsername : rawEmail,
390+
email: hasEmail ? rawEmail : undefined,
391+
password,
392+
};
393+
}
394+
395+
async _autoSignupOnLogin(req, credentials) {
396+
const userData = {
397+
username: credentials.username,
398+
password: credentials.password,
399+
};
400+
if (credentials.email !== undefined) {
401+
userData.email = credentials.email;
402+
}
403+
// Just call the existing user creation flow so we get all schema checks, triggers,
404+
// adapters, and side effects exactly once.
405+
// As for params validation, RestWrite's validateAuthData will handle the validation of the params internally anyway.
406+
const result = await rest.create(
407+
req.config,
408+
req.auth,
409+
'_User',
410+
userData,
411+
req.info.clientSDK,
412+
req.info.context
413+
);
414+
const user = result?.response;
415+
if (!user) {
416+
throw new Parse.Error(
417+
Parse.Error.INTERNAL_SERVER_ERROR,
418+
'Unable to automatically sign up user.'
419+
);
420+
}
421+
return user.sessionToken;
422+
}
423+
320424
/**
321425
* This allows master-key clients to create user sessions without access to
322426
* user credentials. This enables systems that can authenticate access another

types/Options/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface ParseServerOptions {
7777
verifyUserEmails?: (boolean | void);
7878
preventLoginWithUnverifiedEmail?: boolean;
7979
preventSignupWithUnverifiedEmail?: boolean;
80+
autoSignupOnLogin?: boolean;
8081
emailVerifyTokenValidityDuration?: number;
8182
emailVerifyTokenReuseIfValid?: boolean;
8283
sendUserEmailVerification?: (boolean | void);

0 commit comments

Comments
 (0)