[CORL-445] Change Password (#2426)

* fix: reversed `new-password` autocomplete option

* feat: initial implementation

* fix: localization and testing

* fix: updated snapshot
This commit is contained in:
Wyatt Johnson
2019-08-01 22:08:14 +00:00
committed by GitHub
parent 290ceee8e9
commit 836a2267bf
48 changed files with 1122 additions and 197 deletions
+10 -1
View File
@@ -1,10 +1,13 @@
import { LocalProfile, SSOProfile } from "coral-server/models/user";
import uuid from "uuid/v1";
import { getLocalProfile, hasLocalProfile } from "./helpers";
import { LocalProfile, SSOProfile } from "./user";
const localProfile: LocalProfile = {
type: "local",
id: "hans@email.com",
password: "secret",
passwordID: uuid(),
};
const ssoProfile: SSOProfile = {
@@ -25,6 +28,12 @@ it("will return true when the profile exists", () => {
expect(hasLocalProfile({ profiles: [localProfile] })).toBeTruthy();
});
it("will get the local profile with the correct email", () => {
expect(
getLocalProfile({ profiles: [localProfile] }, localProfile.id)
).toEqual(localProfile);
});
it("will return true when the profile exists with the right email", () => {
expect(
hasLocalProfile({ profiles: [localProfile] }, localProfile.id)
+14 -7
View File
@@ -34,11 +34,22 @@ export function needsSSOUpdate(
* @param user the User to pull the LocalProfile out of
*/
export function getLocalProfile(
user: Pick<User, "profiles">
user: Pick<User, "profiles">,
withEmail?: string
): LocalProfile | undefined {
return user.profiles.find(({ type }) => type === "local") as
const profile = user.profiles.find(({ type }) => type === "local") as
| LocalProfile
| undefined;
if (!profile) {
return;
}
if (withEmail && profile.id !== withEmail) {
return;
}
return profile;
}
/**
@@ -53,14 +64,10 @@ export function hasLocalProfile(
user: Pick<User, "profiles">,
withEmail?: string
): boolean {
const profile = getLocalProfile(user);
const profile = getLocalProfile(user, withEmail);
if (!profile) {
return false;
}
if (withEmail && profile.id !== withEmail) {
return false;
}
return true;
}
+35 -5
View File
@@ -47,6 +47,13 @@ export interface LocalProfile {
id: string;
password: string;
/**
* passwordID is used to help protect against double password change race
* conditions. Because the password cannot be compared against directly, this
* ID can be used as it is only changed when the password is changed.
*/
passwordID: string;
/**
* resetID is used during a password reset process to prevent replay attacks.
* When a password reset email is sent, a resetID is associated with the
@@ -530,9 +537,10 @@ export async function updateUserRole(
export async function verifyUserPassword(
user: Pick<User, "profiles">,
password: string
password: string,
withEmail?: string
) {
const profile = getLocalProfile(user);
const profile = getLocalProfile(user, withEmail);
if (!profile) {
throw new LocalProfileNotSetError();
}
@@ -544,7 +552,8 @@ export async function updateUserPassword(
mongo: Db,
tenantID: string,
id: string,
password: string
password: string,
passwordID: string
) {
// Hash the password.
const hashedPassword = await hashPassword(password);
@@ -556,10 +565,17 @@ export async function updateUserPassword(
id,
// This ensures that the document we're updating already has a local
// profile associated with them.
"profiles.type": "local",
profiles: {
$elemMatch: {
type: "local",
passwordID,
},
},
},
{
$set: {
// Update the passwordID with a new one.
"profiles.$[profiles].passwordID": uuid(),
"profiles.$[profiles].password": hashedPassword,
},
},
@@ -576,10 +592,15 @@ export async function updateUserPassword(
throw new UserNotFoundError(id);
}
if (!hasLocalProfile(user)) {
const profile = getLocalProfile(user);
if (!profile) {
throw new LocalProfileNotSetError();
}
if (profile.passwordID !== passwordID) {
throw new Error("passwordID mismatch");
}
throw new Error("an unexpected error occurred");
}
@@ -936,6 +957,7 @@ export async function setUserLocalProfile(
type: "local",
id: email,
password: hashedPassword,
passwordID: uuid(),
};
// The profile wasn't found, so add it to the User.
@@ -1553,6 +1575,7 @@ export async function resetUserPassword(
tenantID: string,
id: string,
password: string,
passwordID: string,
resetID: string
) {
// Hash the password.
@@ -1568,12 +1591,15 @@ export async function resetUserPassword(
profiles: {
$elemMatch: {
type: "local",
passwordID,
resetID,
},
},
},
{
$set: {
// Update the passwordID with a new one.
"profiles.$[profiles].passwordID": uuid(),
"profiles.$[profiles].password": hashedPassword,
},
$unset: {
@@ -1602,6 +1628,10 @@ export async function resetUserPassword(
throw new PasswordResetTokenExpired("reset id mismatch");
}
if (profile.passwordID !== passwordID) {
throw new PasswordResetTokenExpired("password id mismatch");
}
throw new Error("an unexpected error occurred");
}