diff --git a/test/api/v3/integration/user/auth/POST-login-local.test.js b/test/api/v3/integration/user/auth/POST-login-local.test.js
index ad1eecb778..484ff16af5 100644
--- a/test/api/v3/integration/user/auth/POST-login-local.test.js
+++ b/test/api/v3/integration/user/auth/POST-login-local.test.js
@@ -110,4 +110,22 @@ describe('POST /user/auth/local/login', () => {
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
+
+ it('user uses social authentication and has no password', async () => {
+ await user.unset({
+ 'auth.local.hashed_password': 1,
+ });
+
+ await user.sync();
+ expect(user.auth.local.hashed_password).to.be.undefined;
+
+ await expect(api.post(endpoint, {
+ username: user.auth.local.username,
+ password: 'any-password',
+ })).to.eventually.be.rejected.and.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('invalidLoginCredentialsLong'),
+ });
+ });
});
diff --git a/test/helpers/api-integration/api-classes.js b/test/helpers/api-integration/api-classes.js
index 7612c88cfd..f914d0d3a5 100644
--- a/test/helpers/api-integration/api-classes.js
+++ b/test/helpers/api-integration/api-classes.js
@@ -4,6 +4,7 @@ import { requester } from './requester';
import {
getDocument as getDocumentFromMongo,
updateDocument as updateDocumentInMongo,
+ unsetDocument as unsetDocumentInMongo,
} from '../mongo';
import {
assign,
@@ -29,6 +30,18 @@ class ApiObject {
return this;
}
+ async unset (options) {
+ if (isEmpty(options)) {
+ return;
+ }
+
+ await unsetDocumentInMongo(this._docType, this, options);
+
+ _updateLocalParameters((this, options));
+
+ return this;
+ }
+
async sync () {
let updatedDoc = await getDocumentFromMongo(this._docType, this);
diff --git a/test/helpers/mongo.js b/test/helpers/mongo.js
index fb16e3a24f..ca2af9ce45 100644
--- a/test/helpers/mongo.js
+++ b/test/helpers/mongo.js
@@ -98,6 +98,19 @@ export async function updateDocument (collectionName, doc, update) {
});
}
+// Unset a property in the database.
+// Useful for testing.
+export async function unsetDocument (collectionName, doc, update) {
+ let collection = mongoose.connection.db.collection(collectionName);
+
+ return new Promise((resolve) => {
+ collection.updateOne({ _id: doc._id }, { $unset: update }, (updateErr) => {
+ if (updateErr) throw new Error(`Error updating ${collectionName}: ${updateErr}`);
+ resolve();
+ });
+ });
+}
+
export async function getDocument (collectionName, doc) {
let collection = mongoose.connection.db.collection(collectionName);
diff --git a/website/common/locales/en/front.json b/website/common/locales/en/front.json
index c249bc9f51..06ead889ae 100644
--- a/website/common/locales/en/front.json
+++ b/website/common/locales/en/front.json
@@ -276,7 +276,6 @@
"usernameTOSRequirements": "Usernames must conform to our Terms of Service and Community Guidelines. If you didn’t previously set a login name, your username was auto-generated.",
"usernameTaken": "Username already taken.",
"passwordConfirmationMatch": "Password confirmation doesn't match password.",
- "invalidLoginCredentials": "Incorrect username and/or email and/or password.",
"passwordResetPage": "Reset Password",
"passwordReset": "If we have your email on file, instructions for setting a new password have been sent to your email.",
"passwordResetEmailSubject": "Password Reset for Habitica",
diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js
index 37d7f7e451..84c5485fe6 100644
--- a/website/server/controllers/api-v3/auth.js
+++ b/website/server/controllers/api-v3/auth.js
@@ -98,6 +98,9 @@ api.loginLocal = {
// load the entire user because we may have to save it to convert the password to bcrypt
let user = await User.findOne(login).exec();
+ // if user is using social login, then user will not have a hashed_password stored
+ if (!user.auth.local.hashed_password) throw new NotAuthorized(res.t('invalidLoginCredentialsLong'));
+
let isValidPassword;
if (!user) {