fix(users): encode password and email it when approving a pending user creation
finalizeCreateUser() (run when a checker approves a CREATE_USER request queued in m_user_pending_request) saved AppUser.fromJson()'s result straight to m_appuser via appUserRepository.saveAndFlush() with no encoding step for the password AppUser.fromJson() auto-generates when the maker ticks "send password to email" -- AppUser's constructor stores whatever string it is given verbatim, so that random password was landing in m_appuser.password as plain text. Only the maker-chosen-password case (request.getPasswordHash() != null) was ever hashed, because that happens earlier, at submitCreateUser() time. It also never called the email service at all, in either case. The live create-user flow (JpaUserDomainService.create()) captures the unencoded password, encodes it, saves, then emails the welcome message -- finalizeCreateUser() duplicates everything up to the save but stops there, so a user created through this maker-checker flow never received their credentials regardless of what sendPasswordToEmail said in the submitted payload. Fix: capture the auto-generated plaintext right after AppUser.fromJson(), encode it with this class's own PasswordEncoder bean (already used the same way for the maker-chosen-password branch, consistent within the file) before saving, and call PlatformEmailService.sendToUserAccount() afterwards when sendPasswordToEmail was true and no maker-chosen password overrides it. A maker-chosen password is unrecoverable by approval time (submitCreateUser() redacts it to "[PROTECTED]" immediately), so that case still cannot be emailed, and isn't -- there is nothing wrong to fix there. Found by manually submitting a CREATE_USER request locally and reading both AppUser.fromJson() and the approval path end to end; no existing approved user was re-created to verify in place, since doing so would need resetting a real password. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+30
-1
@@ -40,6 +40,7 @@ import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRu
|
|||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.PlatformEmailService;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
||||||
@@ -82,6 +83,7 @@ public class UserPendingWritePlatformServiceImpl implements UserPendingWritePlat
|
|||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final UserDataValidator fromApiJsonDeserializer;
|
private final UserDataValidator fromApiJsonDeserializer;
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final PlatformEmailService emailService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -293,13 +295,40 @@ public class UserPendingWritePlatformServiceImpl implements UserPendingWritePlat
|
|||||||
appUser.updatePasswordResetRequired(true);
|
appUser.updatePasswordResetRequired(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set pre-hashed password
|
// AppUser.fromJson() already generated a random plaintext password here when the maker ticked
|
||||||
|
// "send password to email" (it ignores whatever password the maker typed in that case) -- the
|
||||||
|
// field still holds that plaintext at this point, since nothing has encoded it yet. If the
|
||||||
|
// maker instead supplied their own password, request.getPasswordHash() carries the bcrypt
|
||||||
|
// hash submitCreateUser() produced from it; that plaintext was redacted to "[PROTECTED]"
|
||||||
|
// immediately and is gone for good, so there is nothing to email in that case.
|
||||||
|
final boolean sendPasswordToEmail = Boolean.TRUE.equals(replay.booleanObjectValueOfParameterNamed("sendPasswordToEmail"));
|
||||||
|
final String unencodedPassword = appUser.getPassword();
|
||||||
|
|
||||||
if (request.getPasswordHash() != null) {
|
if (request.getPasswordHash() != null) {
|
||||||
|
// Maker-chosen password: already hashed, used as-is.
|
||||||
appUser.updatePassword(request.getPasswordHash());
|
appUser.updatePassword(request.getPasswordHash());
|
||||||
|
} else {
|
||||||
|
// Auto-generated password (the normal "send password to email" case): this service's own
|
||||||
|
// generic PasswordEncoder, not the salted PlatformPasswordEncoder the live create-user flow
|
||||||
|
// uses -- consistent with how the maker-chosen-password branch above and submitCreateUser()
|
||||||
|
// already hash a password in this file. Encoding it here is not optional: AppUser's
|
||||||
|
// constructor stores whatever string it is given verbatim (see AppUser(Office, User, ...)),
|
||||||
|
// so skipping this would persist the plaintext password straight into m_appuser.password.
|
||||||
|
appUser.updatePassword(this.passwordEncoder.encode(unencodedPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appUserRepository.saveAndFlush(appUser);
|
this.appUserRepository.saveAndFlush(appUser);
|
||||||
|
|
||||||
|
// Mirrors JpaUserDomainService.create(): the welcome email the live create-user flow sends is
|
||||||
|
// the one thing this parallel, pending-request flow never did -- sendPasswordToEmail was
|
||||||
|
// accepted and stored in the payload but nothing here ever acted on it, so a user created this
|
||||||
|
// way never received their password. Only fires for the auto-generated password: there is no
|
||||||
|
// plaintext to send for a maker-chosen one, per the comment above.
|
||||||
|
if (sendPasswordToEmail && request.getPasswordHash() == null) {
|
||||||
|
this.emailService.sendToUserAccount(userOffice.getName(), appUser.getFirstname(), appUser.getEmail(), appUser.getUsername(),
|
||||||
|
unencodedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
// MFA Enrollment
|
// MFA Enrollment
|
||||||
final String otpDeliveryMethod = replay.stringValueOfParameterNamed("otpDeliveryMethod");
|
final String otpDeliveryMethod = replay.stringValueOfParameterNamed("otpDeliveryMethod");
|
||||||
if (otpDeliveryMethod != null) {
|
if (otpDeliveryMethod != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user