Skip to main content
Jahia Store
EN

UPA - MFA factors - TOTP

community
Download 0.2.0

Information

Module ID
mfa-factors-totp
Group ID
org.jahia.community
Status
community
Category
Authentication
Author
Florent BOURASSE
Developer website
http://www.jahia.com
Requires Jahia
8.2.3.0
Updated
2026-08-13
Source
scm:git:git@github.com:Jahia/jahia-private.git/jahia-modules/mfa-factors-totp

Adds a time-based one-time-password (TOTP, RFC 6238) factor to the UPA Multi-Factor Authentication framework. Users enroll by scanning a QR code with any standard authenticator app, then confirm with a 6-digit code. Secrets are encrypted at rest, and each user gets a set of one-shot backup codes for when their device is unavailable.

Ships its own self-service dashboard panel and a per-site administration page (MFA Community → Two-factor authentication) for policy, group scoping, and per-user reset, plus an audit and enrollment report.

  • Standards-based TOTP (RFC 6238), compatible with all common authenticator apps
  • QR-code enrollment with confirmation step
  • Encrypted secret storage; encryption key kept in server configuration only
  • One-shot backup codes with regeneration
  • Per-site policy, group scoping, administrator-initiated user reset
  • Rate-limited management operations and full audit trail

Screenshots

Versions

Security

Three vulnerabilities affecting 0.1.0 / 0.1.1, found by an adversarial review and each confirmed by reproduction against a live Jahia 8.2 before and after the fix (#100).

All three shared one root cause: a client-supplied site value was trusted to narrow or suppress an access-control decision.

  • The site request parameter defeated the /cms/login gate. MfaLoginGateDecision.isGated() resolved the site from request.getParameter("site"), validated only against [A-Za-z0-9_-]+, and used it to replace the "is any site enforcing?" answer. Naming a site where the factor was not enabled turned gating off — a password-only POST authenticated as root with no second factor while enforcement was active. The site context now only ever widens the decision.
  • The site argument to mfaInitiate suppressed a globally enforced factor. Per-site applicability was evaluated before enforcement, so a caller holding only a password could name a site with the factor disabled, receive a "skipped" preparation, and have verify() accept an empty code. Global enforcement is now evaluated first, and a per-site switch can never skip a user who owns the factor.
  • Any single-site administrator could strip MFA from any account, including root. resetUserMfa / resetUserWebauthn authorized on siteKey alone, then acted on an unconstrained userId. The subject is now resolved once and authorized in its own right; a reset targeting a global user requires server-administrator rights.

Also: enrollmentReport no longer hands any site administrator the MFA status of every account platform-wide, and unknown usernames are reported instead of returning a green confirmation for a user that was never touched.

New — loginGate.gateBasicAuth

The /cms/login gate can now also refuse a password presented in an Authorization: Basic header, closing the same bypass on the machine-facing surface. Default false, and deliberately opt-in: that credential shape is not confined to one endpoint, so arming it refuses the provisioning API, GraphQL, the tools and WebDAV platform-wide the moment one site enforces a factor.

Read the README section before enabling it — in particular, whitelist your automation hosts, not just an admin VPN range, and verify a whitelisted client actually gets through first, because behind a reverse proxy the IP whitelist fails closed (GHSA-4v3g-mcmj-83fp).

Fixed

  • The /cms/login gate wrote the response twice (valve + filter), surfacing as HTTP 500 instead of a redirect whenever a distinct loginUrl was configured.
  • The gate matched on the mere presence of username + password request parameters while running at pipeline position 0, so it refused unrelated endpoints that happened to use those field names. It now mirrors LoginEngineAuthValveImpl#isLoginRequested.
  • The admin screens could wipe your configuration. useQuery was read without error, so a failed load left constructor defaults in the form and Save wrote them back — silently clearing enforcedFactors, the IP whitelist and the login URL.
  • The admin UI stated that trusting X-Forwarded-For was ON by default and defaulted the checkbox to true. It is OFF, because trusting the header makes the IP whitelist spoofable (SEC-135).
  • The self-service disable/regenerate dialog offered backup codes the server does not accept there — each rejected attempt burned a rate-limit strike, walking a user who had lost their authenticator into locking themselves out of their own recovery.
  • The destructive "reset a user's MFA" action never showed its in-flight state: a React 18 batching bug unmounted the confirmation box in the same render that was meant to display it. Affected both the TOTP and WebAuthn copies.

Known limitation (documented, unchanged behaviour)

The /cms/login gate is scoped to global enforcement: with enforcedFactors empty it is inert, and loginGate.enabled does not override that. A per-site opt-in deployment still challenges enrolled users through the MFA login UI, but /cms/login remains a password-only door. See The /cms/login gate → Known limitation in the README for the two supported mitigations.

Quality

The module previously had no CI, no lint gate, no coverage measurement, and no tests for its React admin apps. This release adds all four: a build workflow, yarn lint and yarn test wired into the Maven build, JaCoCo coverage, and 115 component tests — plus 22 dependency updates, ESLint 9, and Node 22.23.2.

352 Java tests + 115 React tests + 26 Cypress specs, all green.

Compatibility

Jahia 8.2.3+, user-password-authentication 0.2.0, graphql-dxm-provider 3.4+, Java 11+.

Operators who use per-site group scoping should note that a globally enforced factor now overrides it (an out-of-scope user is challenged rather than skipped, and the override is logged) — see the README's Global enforcement section.

Full Changelog: 0_1_0...0_2_0

Requires Jahia 8.2.3.0Updated 2026-08-13

Fixes

  • Admin UI: restored the vertical scrollbar on the MFA extensions global settings page (/jahia/administration/mfa-extensions-settings). The form was clipped with no scrollbar when it grew past the viewport.

Contents

0.1.0 plus the single fix above. No API or dependency changes.

Requirements

  • Jahia 8.2.3.0
  • user-password-authentication 0.2.0 (modules import org.jahia.modules.upa.mfa at [0.2,1))

Install

Deploy the three .jar bundles via the module manager / hot-deploy folder; install mfa-factors-login-ui-0.1.1.tgz via the provisioning API.

Requires Jahia 8.2.3.0Updated 2026-06-29
Requires Jahia 8.2.3.0Updated 2026-06-28