merge code feature/Backend-DSC-MFA this branch date 15-05-2026

This commit is contained in:
Nitesh Yadav
2026-05-15 13:08:15 +05:30
32 changed files with 24462 additions and 139 deletions
@@ -85,7 +85,6 @@ public class AuthenticationApiResource {
private final ClientReadPlatformService clientReadPlatformService;
private final ConfigurationDomainService configurationDomainService;
private final UserMfaFactorRepository userMfaFactorRepository;
private final org.apache.fineract.useradministration.dsc.domain.BankUserDscRepository bankUserDscRepository;
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -148,26 +147,13 @@ public class AuthenticationApiResource {
// safe rollout (the per-user gate naturally lets unenrolled users in) and instant
// rollback (flip the c_configuration row off and everyone is unchallenged).
final boolean enforcementEnabled = configurationDomainService.isMfaFactorEnforcementEnabled();
// DSC factor check is wrapped so a misconfigured/missing m_bank_user_dsc table can't
// bring down the entire login path. If the lookup throws (table missing, columns
// renamed, JDBC down), we log and fall back to "no DSC factor enrolled" — the user
// can still authenticate via their OTP factors (or no 2FA if none enrolled).
boolean hasActiveDsc = false;
if (enforcementEnabled) {
try {
hasActiveDsc = bankUserDscRepository
.existsByAppUser_IdAndActiveTrueAndDeletedFalseAndCertStatusAndDscExpiryDateGreaterThanEqual(
principal.getId(),
org.apache.fineract.useradministration.dsc.domain.CertStatus.ACTIVE,
java.time.LocalDate.now());
} catch (final Exception e) {
log.warn("DSC factor lookup failed for user {} during login — falling back to non-DSC: {}",
principal.getUsername(), e.getMessage());
}
}
// m_user_mfa_factor is the single source of truth for "does this user require 2FA?"
// DSC enrolment writes a row there via DscEnrollmentWriteServiceImpl, and revoking the
// DSC or disabling it from the Access Control UI flips disabled_on on the same row.
// This keeps the disable flow honest — flipping a factor off in the UI actually stops
// the challenge at login, instead of being silently overridden by a stale cert mapping.
final boolean userHasFactors = enforcementEnabled
&& (userMfaFactorRepository.existsByAppUser_IdAndVerifiedTrueAndDisabledOnIsNull(principal.getId())
|| hasActiveDsc);
&& userMfaFactorRepository.existsByAppUser_IdAndVerifiedTrueAndDisabledOnIsNull(principal.getId());
final boolean isTwoFactorRequired = this.twoFactorEnabled && enforcementEnabled && userHasFactors
&& !principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
Long userId = principal.getId();
@@ -63,7 +63,6 @@ public class TwoFactorAuthenticationFilter extends GenericFilterBean {
private final TwoFactorService twoFactorService;
private final ConfigurationDomainService configurationDomainService;
private final UserMfaFactorRepository userMfaFactorRepository;
private final org.apache.fineract.useradministration.dsc.domain.BankUserDscRepository bankUserDscRepository;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
@@ -93,24 +92,12 @@ public class TwoFactorAuthenticationFilter extends GenericFilterBean {
// has no enrolled factors (or the global kill-switch is off).
final boolean hasBypass = user.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
final boolean enforcementOn = configurationDomainService.isMfaFactorEnforcementEnabled();
// DSC factor check wrapped: a misconfigured/missing m_bank_user_dsc must not bring
// down every protected request. Fall back to "no DSC factor" if the lookup throws.
boolean hasActiveDsc = false;
if (enforcementOn) {
try {
hasActiveDsc = bankUserDscRepository
.existsByAppUser_IdAndActiveTrueAndDeletedFalseAndCertStatusAndDscExpiryDateGreaterThanEqual(
user.getId(),
org.apache.fineract.useradministration.dsc.domain.CertStatus.ACTIVE,
java.time.LocalDate.now());
} catch (final Exception e) {
log.warn("DSC factor lookup failed in 2FA filter for user {} — ignoring: {}",
user.getUsername(), e.getMessage());
}
}
// m_user_mfa_factor is the single source of truth — mirrors AuthenticationApiResource.
// Disabling a factor row from the UI flips disabled_on, which makes this check fall
// through to "no challenge needed". Without that single-table behaviour the filter and
// the login response would disagree, leaving users stuck in a 2FA loop.
final boolean userHasFactors = enforcementOn
&& (userMfaFactorRepository.existsByAppUser_IdAndVerifiedTrueAndDisabledOnIsNull(user.getId())
|| hasActiveDsc);
&& userMfaFactorRepository.existsByAppUser_IdAndVerifiedTrueAndDisabledOnIsNull(user.getId());
final boolean shouldChallenge = !hasBypass && enforcementOn && userHasFactors;
if (shouldChallenge) {