#1419: My2FAService.kt

projectforge-business/src/main/kotlin/org/projectforge/security/My2FAService.kt Service, projectforge-business/src/main/kotlin/org/projectforge/security/My2FAService.kt 253 lines · 177 code · 53 comments · 23 blank
Central two-factor authentication service: validates OTP codes via Authenticator App (TOTP), SMS, or e-mail with brute-force protection.

Architecture

My2FAService is a @Service that orchestrates multi-channel 2FA verification. It wires UserAuthenticationsService (token storage), SmsSenderConfig (SMS availability check), My2FABruteForceProtection (rate limiting), My2FARequestConfiguration (group-based disable), and WebAuthnSupport (WebAuthn availability).

OTP Validation Pipeline

  1. validateOTP(code, *expectedToken) — Primary entry point. Accepts a 6-digit code and one or more expected tokens (from SMS or e-mail).
  2. preCheck(code) — Returns BLOCKED if brute force protection is active, FAILED if code is too short (<4 chars).
  3. Compares the code against each expectedToken (SMS/e-mail tokens). On match: calls bruteForceProtection.registerOTPSuccess() and updates lastSuccessful2FA timestamp.
  4. If no SMS/e-mail token matched, falls through to _validateAuthenticatorOTP(code) which validates against the stored TOTP token via TimeBased2FA.standard.validate().
  5. On failure: bruteForceProtection.registerOTPFailure() increments the failure counter.

Brute Force Protection

Success registers reset the counter. Failures increment it. getBlockedResult() returns null or a BLOCKED OTPCheckResult with a localized message. The pre-check gates with minimum code length (4) to avoid unnecessary brute-force counting on obviously invalid inputs.

2FA Status Checks

Group-Based 2FA Disable

setDisabled(groupNames) parses a semicolon/comma/colon-separated string of group names from configuration, resolves them against GroupService.allGroups, and populates mail2FADisabledGroupIds. Users in these groups skip e-mail-based 2FA. This is configured on @PostConstruct from my2FARequestConfiguration.disableEmail2FAForGroups.

Design Rationale

Multi-channel OTP validation (SMS, e-mail, authenticator app) is implemented as a fallback chain: external tokens are checked first, then TOTP. This allows a single validateOTP call to work regardless of delivery method. Brute force protection is applied at the OTP level (not login level) so each 2FA attempt is rate-limited independently. The group-based disable feature supports organizational policies where certain users (e.g. administrators with hardware tokens) don't need e-mail 2FA. The lastSuccessful2FA timestamp enables session-level 2FA caching (don't re-prompt if recently verified).

Git History

868d6abb7 2025 -> 2026
600951ee0 2025 -> 2026
63081666f Source file headers: 2024-> 2025.
199c26801 Source file headers: 2024-> 2025.
67805f2fc ThreadLocalUserContext.user -> loggedInUser
58ffda82b ThreadLocalUserContext.user -> loggedInUser
4c04cfd65 MAJOR-CHANGE! Migration of integer id's to Long id's.
fd13c259a MAJOR-CHANGE! Migration of integer id's to Long id's.
4f5a458c9 Migration stuff in progress...
635197c0b Migration stuff in progress...
77bade6df javax.* -> jakarta.*
3afa03fb1 javax.* -> jakarta.*
b6092df09 Copyright 2023 -> 2024
9a0609dd1 Copyright 2023 -> 2024
ab45d51fa Copyright 2001-2022 -> 2001-2023.
29815da21 Copyright 2001-2022 -> 2001-2023.
38bec971a ThreadLocal -> Kotlin
19bb46d57 ThreadLocal -> Kotlin
cb605ebde 2FA: WebAuthn not experimental; at least one 2FA required.
f988a9b1b 2FA: WebAuthn not experimental; at least one 2FA required.