Browse Source

Merge pull request #2 from allanbarcelos/mfa-totp-plugin

many fixes
Allan R. A. Barcelos 1 năm trước cách đây
mục cha
commit
537fe2bd8f
25 tập tin đã thay đổi với 547 bổ sung449 xóa
  1. 8 0
      DEVELOPMENT.md
  2. 72 1
      README.md
  3. 10 7
      SECURITY.md
  4. 53 13
      pom.xml
  5. 56 20
      src/main/java/io/jenkins/plugins/MfaFilter.java
  6. 1 1
      src/main/java/io/jenkins/plugins/MfaFilterRegister.java
  7. 50 0
      src/main/java/io/jenkins/plugins/MfaGlobalConfig.java
  8. 15 17
      src/main/java/io/jenkins/plugins/MfaUserProperty.java
  9. 7 14
      src/main/java/io/jenkins/plugins/MfaVerifyAction.java
  10. 50 45
      src/main/java/io/jenkins/plugins/QrCodeAction.java
  11. 60 18
      src/main/java/io/jenkins/plugins/TOTPUtil.java
  12. 20 0
      src/main/resources/Messages.properties
  13. 19 0
      src/main/resources/Messages_pt_BR.properties
  14. 1 1
      src/main/resources/index.jelly
  15. 18 4
      src/main/resources/io/jenkins/plugins/Messages.properties
  16. 18 5
      src/main/resources/io/jenkins/plugins/Messages_pt_BR.properties
  17. 11 0
      src/main/resources/io/jenkins/plugins/MfaGlobalConfig/config.jelly
  18. 5 0
      src/main/resources/io/jenkins/plugins/MfaGlobalConfig/config.properties
  19. 1 0
      src/main/resources/io/jenkins/plugins/MfaUserProperty/MfaUserProperty.properties
  20. 15 52
      src/main/resources/io/jenkins/plugins/MfaUserProperty/config.jelly
  21. 15 0
      src/main/resources/io/jenkins/plugins/MfaUserProperty/config.properties
  22. 38 0
      src/main/resources/io/jenkins/plugins/MfaUserProperty/script.js
  23. 4 4
      src/main/resources/io/jenkins/plugins/MfaVerifyAction/index.jelly
  24. 0 60
      src/test/java/io/jenkins/plugins/MfaFilterRegisterTest.java
  25. 0 187
      src/test/java/io/jenkins/plugins/MfaFilterTest.java

+ 8 - 0
DEVELOPMENT.md

@@ -0,0 +1,8 @@
+## Handling Security Reports
+
+When receiving a vulnerability report:
+1. Acknowledge receipt within 48 hours
+2. Work with Jenkins Security Team if needed
+3. Prepare a fix in a private repository if necessary
+4. Coordinate disclosure timing
+5. Release an updated version following [Jenkins security release process](https://www.jenkins.io/security/for-maintainers/)

+ 72 - 1
README.md

@@ -4,7 +4,7 @@
 [![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/mfa-google-auth.svg?color=blue)](https://plugins.jenkins.io/mfa-google-auth)
 [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
 
-A Jenkins plugin that enforces **multi‑factor authentication (MFA)** using **Google Authenticator**.
+A Jenkins plugin that enforces **multi‑factor authentication (MFA)** using **TOTP Authenticator**.
 This plugin adds an additional layer of security by requiring users to provide a time‑based one‑time password (TOTP) after entering their username and password.
 
 ---
@@ -48,6 +48,69 @@ This plugin adds an additional layer of security by requiring users to provide a
 
 ---
 
+# 🔐 MFA TOTP Plugin - Global Configuration Guide
+
+## 🛠 System Configuration
+
+### 1. Enable Global MFA Enforcement
+1. Navigate to **"Manage Jenkins"** > **"Configure System"**
+2. Scroll to the **"Global MFA Settings"** section
+3. Check **"Enforce MFA for all users"**:
+   - ✅ Enabled: All users must set up MFA on next login
+   - ❌ Disabled: MFA remains optional per user
+
+### 2. Advanced Settings
+| Setting | Description | Recommended Value |
+|---------|-------------|-------------------|
+| **Exclude API tokens** | Allows API access without MFA | Enable for CI/CD pipelines | 
+| **MFA session duration (minutes)** | Time before re-verification | 480 (8 hours) |
+| **Force reconfiguration** | Require periodic MFA reset | 90 days |
+
+![Settings screenshot](path/to/screenshot.png)
+
+## 👤 User Experience Flow
+When global MFA is enabled:
+1. At first login, users will be redirected to setup
+2. They must:
+   - Scan QR code with authenticator app
+   - Verify with a valid TOTP code
+
+## ⚙️ API Configuration (JSON)
+```bash
+curl -X POST http://jenkins/configure \
+  -u admin:api_token \
+  -H "Content-Type: application/json" \
+  -d '{
+    "mfa": {
+      "enforceForAllUsers": true,
+      "excludeApiTokens": true,
+      "sessionDuration": 480
+    }
+  }'
+```
+
+## 🔐 Security Best Practices
+1. **For Admins**:
+   - Enable global MFA for all privileged accounts
+   - Maintain one emergency break-glass account
+
+2. **For Users**:
+   - Use trusted authenticator apps (Google/Microsoft Authenticator)
+   - Store recovery codes securely
+
+## 🚨 Troubleshooting
+### Issue: User cannot configure MFA
+**Solution**:
+1. Verify Jenkins server time synchronization
+2. Check logs at `/var/log/jenkins/mfa.log`
+
+### Issue: API returns 403 errors
+**Solution**:
+1. Enable "Exclude API tokens" in global settings  
+2. Generate new API tokens after MFA activation
+
+---
+
 ## 📌 Requirements
 
 * **Jenkins Core**: `2.479.3` or newer
@@ -73,6 +136,14 @@ mvn hpi:run
 Then open Jenkins at [http://localhost:8080/jenkins/](http://localhost:8080/jenkins/).
 
 
+---
+
+## Security
+
+Security vulnerabilities should be reported following the [Jenkins vulnerability reporting guidelines](https://www.jenkins.io/security/#reporting-vulnerabilities). 
+
+Please do not report security issues through GitHub issues or public discussions.
+
 ---
 
 ## 📜 License

+ 10 - 7
SECURITY.md

@@ -1,13 +1,16 @@
 # Security Policy
 
-## Reporting a Vulnerability
+## Reporting Vulnerabilities
 
-To report security issues send an email to [email protected]
+Please report security vulnerabilities through the [official Jenkins vulnerability reporting process](https://www.jenkins.io/security/#reporting-vulnerabilities).
 
-The following keys may be used to communicate sensitive information to developer:
+- **Do not** create public issues for security vulnerabilities
+- Security reports are handled by the [Jenkins Security Team](mailto:[email protected])
+- You should receive a response within 48 hours
+- Expect updates on the vulnerability status throughout the process
 
-| Name | Fingerprint |
-|------|-------------|
-| Allan Barcelos | 492F 6B41 B83C B0BF 5B64 5777 F437 AC65 26AB 7A49 |
+## Security Updates
 
-You can import a key by running the following command with that individual’s fingerprint: `gpg --keyserver hkps://keys.openpgp.org --recv-keys "<fingerprint>"` Ensure that you put quotes around fingerprints containing spaces.
+All security releases will be announced on:
+- [Jenkins Security Advisories](https://www.jenkins.io/security/advisories/)
+- The [jenkinsci-advisories](https://groups.google.com/g/jenkinsci-advisories) mailing list

+ 53 - 13
pom.xml

@@ -10,14 +10,14 @@
   </parent>
 
   <groupId>io.jenkins.plugins</groupId>
-  <artifactId>mfa-google-auth</artifactId>
+  <artifactId>mfa-totp-plugin</artifactId>
   <version>${revision}${changelist}</version>
   <packaging>hpi</packaging>
 
-  <name>MFA Google Auth Plugin</name>
-  <description>Adds TOTP-based Multi-Factor Authentication (MFA/2FA) to Jenkins using Google Authenticator.
-  Enhances security by requiring a time-based one-time password in addition to regular credentials.
-  Features QR code setup, per-user enforcement, and secure secret storage.</description>
+  <name>MFA TOTP Plugin</name>
+  <description>Adds standards-compliant TOTP (RFC 6238) Multi-Factor Authentication to Jenkins.
+    Compatible with all major authenticator apps (Google Authenticator, Microsoft Authenticator, Authy, etc.).
+    Features secure secret storage, QR code enrollment, and flexible enforcement policies.</description>
   <url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>
   <licenses>
     <license>
@@ -35,12 +35,15 @@
   <properties>
     <revision>1.0</revision>
     <changelist />
-    <!-- https://www.jenkins.io/doc/developer/plugin-development/choosing-jenkins-baseline/ -->
     <jenkins.baseline>2.479</jenkins.baseline>
     <jenkins.version>${jenkins.baseline}.3</jenkins.version>
     <gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
-
     <spotless.check.skip>false</spotless.check.skip>
+    <security.reporting>https://www.jenkins.io/security/#reporting-vulnerabilities</security.reporting>
+    <security.team>[email protected]</security.team>
+    <!-- Library versions -->
+    <otpjava.version>2.0.1</otpjava.version>
+    <zxing.version>3.5.3</zxing.version>
   </properties>
 
   <dependencyManagement>
@@ -56,23 +59,43 @@
   </dependencyManagement>
 
   <dependencies>
+    <!-- TOTP Implementation -->
+    <dependency>
+      <groupId>com.github.bastiaanjansen</groupId>
+      <artifactId>otp-java</artifactId>
+      <version>${otpjava.version}</version>
+    </dependency>
 
+    <!-- QR Code Generation -->
     <dependency>
       <groupId>com.google.zxing</groupId>
       <artifactId>core</artifactId>
-      <version>3.5.3</version>
+      <version>${zxing.version}</version>
     </dependency>
     <dependency>
       <groupId>com.google.zxing</groupId>
       <artifactId>javase</artifactId>
-      <version>3.5.3</version>
+      <version>${zxing.version}</version>
     </dependency>
     <dependency>
-      <groupId>com.warrenstrange</groupId>
-      <artifactId>googleauth</artifactId>
-      <version>1.5.0</version>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.16.0</version>
     </dependency>
-    <!-- TEST -->
+
+    <!-- Jenkins Security -->
+    <dependency>
+      <groupId>org.jenkins-ci.plugins</groupId>
+      <artifactId>credentials</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci.plugins.workflow</groupId>
+      <artifactId>workflow-aggregator</artifactId>
+      <version>2.6</version>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- Testing -->
     <dependency>
       <groupId>org.junit.vintage</groupId>
       <artifactId>junit-vintage-engine</artifactId>
@@ -90,7 +113,12 @@
       <id>repo.jenkins-ci.org</id>
       <url>https://repo.jenkins-ci.org/public/</url>
     </repository>
+    <repository>
+      <id>jitpack.io</id>
+      <url>https://jitpack.io</url>
+    </repository>
   </repositories>
+
   <pluginRepositories>
     <pluginRepository>
       <id>repo.jenkins-ci.org</id>
@@ -98,4 +126,16 @@
     </pluginRepository>
   </pluginRepositories>
 
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.jenkins-ci.tools</groupId>
+        <artifactId>maven-hpi-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <compatibleSinceVersion>2.426.1</compatibleSinceVersion>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
 </project>

+ 56 - 20
src/main/java/io/jenkins/plugins/MfaFilter.java

@@ -1,5 +1,5 @@
 /*
- * Project: MFA Google Auth Plugin
+ * Project: MFA TOTP Plugin
  *
  * Class: MfaFilter
  *
@@ -9,22 +9,28 @@
  * the verification page itself.
  *
  * Author: Allan Barcelos
- * Date: 2025-07-17 (Updated for unified implementation)
+ * Date: 2025-07-18 (Updated for Jakarta EE and unified implementation)
  */
 
 package io.jenkins.plugins;
 
 import hudson.Extension;
 import hudson.model.User;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jenkins.model.GlobalConfiguration;
 import jenkins.model.Jenkins;
 
 @Extension
 public class MfaFilter implements Filter {
 
+    private static final Logger LOGGER = Logger.getLogger(MfaFilter.class.getName());
+
     @Override
     public void init(FilterConfig filterConfig) {
         // Initialization not needed
@@ -46,38 +52,66 @@ public class MfaFilter implements Filter {
 
         HttpServletRequest req = (HttpServletRequest) request;
         HttpServletResponse rsp = (HttpServletResponse) response;
+
+        // Security headers for all responses
+        rsp.setHeader("X-Frame-Options", "DENY");
+        rsp.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
+        rsp.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
+
         String path = req.getRequestURI();
         String contextPath = req.getContextPath();
 
         // Skip if Jenkins isn't fully initialized
-        if (Jenkins.getInstanceOrNull() == null) {
+        if (Jenkins.getInstanceOrNull() == null || isExcludedPath(path, contextPath)) {
             chain.doFilter(request, response);
             return;
         }
 
-        // Skip static resources and common excluded paths
-        if (isExcludedPath(path, contextPath)) {
+        User user = User.current();
+        if (user == null) {
             chain.doFilter(request, response);
             return;
         }
 
-        User user = User.current();
-        if (user != null) {
-            MfaUserProperty mfa = user.getProperty(MfaUserProperty.class);
-            if (mfa != null && mfa.isMfaEnabled()) {
-                boolean verified = req.getSession() != null
-                        && Boolean.TRUE.equals(req.getSession().getAttribute("mfa-verified"));
-
-                if (!verified) {
-                    rsp.sendRedirect(contextPath + "/mfa-verify");
-                    return;
-                }
+        MfaGlobalConfig globalConfig = GlobalConfiguration.all().get(MfaGlobalConfig.class);
+        if (globalConfig == null) {
+            LOGGER.warning("MfaGlobalConfig not found - using default settings");
+            globalConfig = new MfaGlobalConfig();
+        }
+
+        MfaUserProperty mfa = user.getProperty(MfaUserProperty.class);
+
+        boolean mfaRequired =
+                (mfa != null && mfa.isMfaEnabled()) || (globalConfig != null && globalConfig.isEnforceMfaForAllUsers());
+
+        if (mfaRequired) {
+
+            // Check if it is a tokenized API call (if the option is enabled)
+            if (globalConfig != null && globalConfig.isExcludeApiTokens() && isApiTokenRequest(req)) {
+                chain.doFilter(request, response);
+                return;
+            }
+
+            boolean verified = req.getSession() != null
+                    && Boolean.TRUE.equals(req.getSession().getAttribute("mfa-verified"));
+
+            if (!verified) {
+                LOGGER.log(Level.INFO, "MFA required for user {0}", user.getId());
+                rsp.sendRedirect(contextPath + "/mfa-verify");
+                return;
             }
         }
 
         chain.doFilter(request, response);
     }
 
+    private boolean isApiTokenRequest(HttpServletRequest req) {
+        String authHeader = req.getHeader("Authorization");
+        if (authHeader == null) return false;
+        return authHeader.startsWith("Bearer ")
+                || (authHeader.startsWith("Basic ") && req.getRequestURI().startsWith(req.getContextPath() + "/api/"));
+    }
+
     /**
      * Determines if the requested path should be excluded from MFA enforcement
      */
@@ -89,6 +123,8 @@ public class MfaFilter implements Filter {
                 || path.startsWith(contextPath + "/login")
                 || path.startsWith(contextPath + "/signup")
                 || path.startsWith(contextPath + "/error")
-                || path.startsWith(contextPath + "/securityRealm");
+                || path.startsWith(contextPath + "/securityRealm")
+                || path.startsWith(contextPath + "/api/")
+                || path.startsWith(contextPath + "/favicon.ico");
     }
 }

+ 1 - 1
src/main/java/io/jenkins/plugins/MfaFilterRegister.java

@@ -16,7 +16,7 @@ import hudson.Extension;
 import hudson.init.InitMilestone;
 import hudson.init.Initializer;
 import hudson.util.PluginServletFilter;
-import javax.servlet.ServletException;
+import jakarta.servlet.ServletException;
 
 @Extension
 public class MfaFilterRegister {

+ 50 - 0
src/main/java/io/jenkins/plugins/MfaGlobalConfig.java

@@ -0,0 +1,50 @@
+/*
+ * Project: MFA TOTP Auth Plugin
+ *
+ * Class: MfaGlobalConfig
+ *
+ *
+ * Author: Allan Barcelos
+ * Date: 2025-07-18
+ */
+
+package io.jenkins.plugins;
+
+import hudson.Extension;
+import jenkins.model.GlobalConfiguration;
+import org.kohsuke.stapler.DataBoundSetter;
+
+@Extension
+public class MfaGlobalConfig extends GlobalConfiguration {
+
+    private boolean enforceMfaForAllUsers;
+    private boolean excludeApiTokens;
+
+    public static MfaGlobalConfig get() {
+        return GlobalConfiguration.all().get(MfaGlobalConfig.class);
+    }
+
+    public MfaGlobalConfig() {
+        load();
+    }
+
+    public boolean isEnforceMfaForAllUsers() {
+        return enforceMfaForAllUsers;
+    }
+
+    @DataBoundSetter
+    public void setEnforceMfaForAllUsers(boolean enforceMfaForAllUsers) {
+        this.enforceMfaForAllUsers = enforceMfaForAllUsers;
+        save();
+    }
+
+    public boolean isExcludeApiTokens() {
+        return excludeApiTokens;
+    }
+
+    @DataBoundSetter
+    public void setExcludeApiTokens(boolean excludeApiTokens) {
+        this.excludeApiTokens = excludeApiTokens;
+        save();
+    }
+}

+ 15 - 17
src/main/java/io/jenkins/plugins/MfaUserProperty.java

@@ -1,5 +1,5 @@
 /*
- * Project: MFA Google Auth Plugin
+ * Project: MFA TOTP Auth Plugin
  *
  * Class: MfaUserProperty
  *
@@ -11,7 +11,7 @@
  * providing validation and display name.
  *
  * Author: Allan Barcelos
- * Date: 2025-07-17
+ * Date: 2025-07-18
  */
 
 package io.jenkins.plugins;
@@ -23,17 +23,20 @@ import hudson.model.UserProperty;
 import hudson.model.UserPropertyDescriptor;
 import hudson.util.FormValidation;
 import hudson.util.Secret;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import org.kohsuke.stapler.DataBoundConstructor;
 import org.kohsuke.stapler.QueryParameter;
 
 public class MfaUserProperty extends UserProperty {
-    private final boolean mfaEnabled;
+    private static final Logger LOGGER = Logger.getLogger(MfaUserProperty.class.getName());
+
+    private final boolean userConfiguredMfa;
     private final Secret secretKey;
 
     @DataBoundConstructor
     public MfaUserProperty(boolean mfaEnabled, String secretKey, String totpCode) throws FormException {
         if (mfaEnabled) {
-            // Se o usuário marcou MFA, validamos o código informado
             if (secretKey == null || secretKey.isEmpty()) {
                 throw new FormException(Messages.MfaUserProperty_secretKey_missing(), "secretKey");
             }
@@ -44,38 +47,34 @@ public class MfaUserProperty extends UserProperty {
                 throw new FormException(Messages.MfaUserProperty_totpCode_invalid(), "totpCode");
             }
         }
-        this.mfaEnabled = mfaEnabled;
-        this.secretKey = Secret.fromString(secretKey);
-    }
 
-    public boolean isMfaEnabled() {
-        return mfaEnabled;
+        this.userConfiguredMfa = mfaEnabled;
+        this.secretKey = secretKey != null ? Secret.fromString(secretKey) : null;
     }
 
-    public String getSecretKey() {
-        return secretKey.getPlainText();
+    public boolean isMfaEnabled() {
+        MfaGlobalConfig globalConfig = MfaGlobalConfig.get();
+        return globalConfig != null && globalConfig.isEnforceMfaForAllUsers() || userConfiguredMfa;
     }
 
-    public Secret getEncryptedSecretKey() {
+    public Secret getSecretKey() {
         return secretKey;
     }
 
     @Extension
     public static final class DescriptorImpl extends UserPropertyDescriptor {
-
         public DescriptorImpl() {
             super(MfaUserProperty.class);
         }
 
         @Override
         public UserProperty newInstance(User user) {
-            // por padrão, sem MFA
             try {
                 return new MfaUserProperty(false, null, null);
             } catch (FormException e) {
-                e.printStackTrace();
+                LOGGER.log(Level.SEVERE, "Failed to create MfaUserProperty instance", e);
+                return null;
             }
-            return null;
         }
 
         @Override
@@ -84,7 +83,6 @@ public class MfaUserProperty extends UserProperty {
         }
 
         public FormValidation doCheckTotpCode(@QueryParameter String value, @QueryParameter String secretKey) {
-            // Essa validação roda enquanto o usuário digita no formulário (opcional)
             if (value == null || value.isEmpty()) {
                 return FormValidation.ok();
             }

+ 7 - 14
src/main/java/io/jenkins/plugins/MfaVerifyAction.java

@@ -1,5 +1,5 @@
 /*
- * Project: MFA Google Auth Plugin
+ * Project: MFA TOTP Auth Plugin
  *
  * Class: MfaVerifyAction
  *
@@ -10,7 +10,7 @@
  * Users without MFA enabled are redirected to the main Jenkins page directly.
  *
  * Author: Allan Barcelos
- * Date: 2025-07-17
+ * Date: 2025-07-18
  */
 package io.jenkins.plugins;
 
@@ -20,12 +20,11 @@ import hudson.model.User;
 import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import javax.servlet.http.HttpSession;
 import org.kohsuke.stapler.StaplerRequest;
 import org.kohsuke.stapler.StaplerResponse;
 
 /**
- * Página intermediária para o segundo fator (MFA).
+ * Intermediate page for the second factor (MFA).
  * URL: /mfa-verify
  */
 @Extension
@@ -35,7 +34,7 @@ public class MfaVerifyAction implements RootAction {
 
     @Override
     public String getIconFileName() {
-        return null; // não mostra no menu lateral
+        return null; // does not show in the side menu
     }
 
     @Override
@@ -49,7 +48,7 @@ public class MfaVerifyAction implements RootAction {
     }
 
     /**
-     * Processa o POST com o código TOTP
+     * Processes POST with TOTP code
      */
     public void doVerify(StaplerRequest req, StaplerResponse rsp) throws IOException {
         User u = User.current();
@@ -63,14 +62,8 @@ public class MfaVerifyAction implements RootAction {
 
         if (mfa != null && mfa.isMfaEnabled()) {
             String code = req.getParameter("totpCode");
-            if (TOTPUtil.verifyCode(mfa.getEncryptedSecretKey(), code)) {
-                // Proteção simplificada - apenas marca como verificado
-                HttpSession session = req.getSession();
-                session.setAttribute("mfa-verified", true);
-
-                // Alternativa: Rotaciona o ID da sessão sem invalidá-la
-                session = req.getSession(true);
-
+            if (TOTPUtil.verifyCode(mfa.getSecretKey(), code)) {
+                req.getSession().setAttribute("mfa-verified", true);
                 LOGGER.log(Level.INFO, "MFA verification successful for user: " + u.getId());
                 rsp.sendRedirect(req.getContextPath() + "/");
                 return;

+ 50 - 45
src/main/java/io/jenkins/plugins/QrCodeAction.java

@@ -1,21 +1,15 @@
 /*
- * Project: MFA Google Auth Plugin
+ * Project: MFA TOTP Auth Plugin
  *
  * Class: QrCodeAction
  *
- * Provides HTTP endpoints to support Google Authenticator MFA setup for Jenkins users.
- *
- * - Generates a new TOTP secret and returns the secret along with the otpauth URL in JSON format.
- * - Generates a QR code image for the provided secret to facilitate easy scanning by authenticator apps.
+ * Provides HTTP endpoints for TOTP-based MFA setup.
+ * Generates secrets and QR codes compatible with any TOTP app.
  *
  * URLs:
- *   /plugin/mfa-google-auth/generateSecret - generates and returns the secret and otpauth URL
- *   /plugin/mfa-google-auth/qrcode          - returns a PNG QR code image for the secret
- *
- * Author: Allan Barcelos
- * Date: 2025-07-17
+ *   /plugin/mfa-totp/generateSecret - generates secret and otpauth URL
+ *   /plugin/mfa-totp/qrcode        - returns QR code PNG image
  */
-
 package io.jenkins.plugins;
 
 import com.google.zxing.BarcodeFormat;
@@ -36,10 +30,7 @@ import org.kohsuke.stapler.StaplerResponse;
 public class QrCodeAction implements RootAction {
 
     private static final Logger LOGGER = Logger.getLogger(QrCodeAction.class.getName());
-
-    public QrCodeAction() {
-        LOGGER.info("QrCodeAction initialized");
-    }
+    private static final String ISSUER = "Jenkins";
 
     @Override
     public String getIconFileName() {
@@ -48,35 +39,27 @@ public class QrCodeAction implements RootAction {
 
     @Override
     public String getDisplayName() {
-        return "MFA Google Auth Action";
+        return "MFA TOTP Action";
     }
 
     @Override
     public String getUrlName() {
-        return "mfa-google-auth";
+        return "mfa-totp";
     }
 
-    // URL: /plugin/mfa-google-auth/generateSecret
-    public void doGenerateSecret(StaplerRequest req, StaplerResponse rsp) throws Exception {
-        String username = req.getSession().getAttribute("jenkins.security.SecurityRealm.user") != null
-                ? req.getSession()
-                        .getAttribute("jenkins.security.SecurityRealm.user")
-                        .toString()
-                : "user";
-
-        var key = TOTPUtil.generateSecret();
-        String secret = key.getKey();
-
-        String otpAuthUrl = TOTPUtil.getQRBarcodeURL(username, "jenkins", secret);
-
-        rsp.setContentType("application/json;charset=UTF-8");
-        JSONObject json = new JSONObject();
-        json.put("secret", secret);
-        json.put("otpAuthUrl", otpAuthUrl);
-        rsp.getWriter().print(json.toString());
+    public void doGenerateSecret(StaplerRequest req, StaplerResponse rsp) throws IOException {
+        try {
+            String username = getCurrentUsername(req);
+            String secret = TOTPUtil.generateSecret();
+            String otpAuthUrl = buildOtpAuthUrl(username, secret);
+
+            sendJsonResponse(rsp, secret, otpAuthUrl);
+        } catch (Exception e) {
+            LOGGER.severe("Failed to generate secret: " + e.getMessage());
+            rsp.sendError(500, "Failed to generate secret");
+        }
     }
 
-    // URL: /plugin/mfa-google-auth/qrcode
     public void doQrcode(StaplerRequest req, StaplerResponse rsp) throws IOException {
         String secret = req.getParameter("secret");
         if (secret == null || secret.isEmpty()) {
@@ -84,17 +67,39 @@ public class QrCodeAction implements RootAction {
             return;
         }
 
-        String otpAuth = TOTPUtil.getQRBarcodeURL("user", "jenkins", secret);
-
         try {
-            QRCodeWriter qrCodeWriter = new QRCodeWriter();
-            BitMatrix bitMatrix = qrCodeWriter.encode(otpAuth, BarcodeFormat.QR_CODE, 200, 200);
-            BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
-
-            rsp.setContentType("image/png");
-            javax.imageio.ImageIO.write(qrImage, "PNG", rsp.getOutputStream());
-        } catch (WriterException e) {
+            generateQRCodeImage(rsp, buildOtpAuthUrl("user", secret));
+        } catch (Exception e) {
+            LOGGER.severe("QR Code generation failed: " + e.getMessage());
             rsp.sendError(500, "Failed to generate QR Code");
         }
     }
+
+    private String getCurrentUsername(StaplerRequest req) {
+        Object userAttr = req.getSession().getAttribute("jenkins.security.SecurityRealm.user");
+        return userAttr != null ? userAttr.toString() : "user";
+    }
+
+    private String buildOtpAuthUrl(String username, String secret) {
+        return String.format(
+                "otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30",
+                ISSUER, username, secret, ISSUER);
+    }
+
+    private void sendJsonResponse(StaplerResponse rsp, String secret, String otpAuthUrl) throws IOException {
+        rsp.setContentType("application/json;charset=UTF-8");
+        JSONObject json = new JSONObject();
+        json.put("secret", secret);
+        json.put("otpAuthUrl", otpAuthUrl);
+        rsp.getWriter().print(json.toString());
+    }
+
+    private void generateQRCodeImage(StaplerResponse rsp, String otpAuth) throws IOException, WriterException {
+        QRCodeWriter qrCodeWriter = new QRCodeWriter();
+        BitMatrix bitMatrix = qrCodeWriter.encode(otpAuth, BarcodeFormat.QR_CODE, 200, 200);
+        BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
+
+        rsp.setContentType("image/png");
+        javax.imageio.ImageIO.write(qrImage, "PNG", rsp.getOutputStream());
+    }
 }

+ 60 - 18
src/main/java/io/jenkins/plugins/TOTPUtil.java

@@ -1,49 +1,91 @@
 /*
- * Project: MFA Google Auth Plugin
+ * Project: MFA TOTP Auth Plugin
  *
  * Class: TOTPUtil
  *
- * Utility class for handling Time-based One-Time Password (TOTP) operations
- * using Google Authenticator library. Provides methods to generate secrets,
- * create otpauth URLs for QR codes, and verify TOTP codes.
+ * Utility class for standards-compliant TOTP (RFC 6238) operations.
+ * Provides methods to generate secrets, build otpauth URIs, and verify codes.
+ * Compatible with all major authenticator apps.
  *
  * Author: Allan Barcelos
  * Date: 2025-07-17
  */
-
 package io.jenkins.plugins;
 
-import com.warrenstrange.googleauth.GoogleAuthenticator;
-import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
 import hudson.util.Secret;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.binary.Base32;
 
 public class TOTPUtil {
+    private static final Logger LOGGER = Logger.getLogger(TOTPUtil.class.getName());
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+    private static final int CODE_LENGTH = 6;
+    private static final int TIME_STEP = 30; // seconds
 
-    public static GoogleAuthenticatorKey generateSecret() {
-        GoogleAuthenticator gAuth = new GoogleAuthenticator();
-        return gAuth.createCredentials();
+    public static String generateSecret() {
+        byte[] buffer = new byte[20]; // 160 bits
+        SECURE_RANDOM.nextBytes(buffer);
+        return new Base32().encodeToString(buffer);
     }
 
-    public static String getQRBarcodeURL(String user, String host, String secret) {
-        String issuer = host;
-        return String.format("otpauth://totp/%s@%s?secret=%s&issuer=%s", user, host, secret, issuer);
+    public static String getQRBarcodeURL(String user, String issuer, String secret) {
+        return String.format(
+                "otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=%d&period=%d",
+                issuer, user, secret, issuer, CODE_LENGTH, TIME_STEP);
     }
 
     public static boolean verifyCode(Secret secret, String code) {
         try {
-            return verifyCode(secret.getPlainText(), code);
+            return secret != null && verifyCode(secret.getPlainText(), code);
         } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Failed to verify code", e);
             return false;
         }
     }
 
     public static boolean verifyCode(String secret, String code) {
         try {
-            int codeInt = Integer.parseInt(code);
-            GoogleAuthenticator gAuth = new GoogleAuthenticator();
-            return gAuth.authorize(secret, codeInt);
-        } catch (NumberFormatException e) {
+            if (secret == null || code == null || code.length() != CODE_LENGTH) {
+                return false;
+            }
+
+            long time = System.currentTimeMillis() / 1000 / TIME_STEP;
+            String expectedCode = generateTOTP(secret, time);
+            return expectedCode.equals(code);
+        } catch (Exception e) {
+            LOGGER.log(Level.SEVERE, "TOTP verification failed", e);
             return false;
         }
     }
+
+    private static String generateTOTP(String secret, long time) throws NoSuchAlgorithmException, InvalidKeyException {
+        Base32 base32 = new Base32();
+        byte[] keyBytes = base32.decode(secret);
+        byte[] timeBytes = new byte[8];
+
+        for (int i = 7; i >= 0; i--) {
+            timeBytes[i] = (byte) (time & 0xFF);
+            time >>= 8;
+        }
+
+        Mac mac = Mac.getInstance("HmacSHA1");
+        mac.init(new SecretKeySpec(keyBytes, "HmacSHA1"));
+        byte[] hash = mac.doFinal(timeBytes);
+
+        int offset = hash[hash.length - 1] & 0xF;
+        int binary = ((hash[offset] & 0x7F) << 24)
+                | ((hash[offset + 1] & 0xFF) << 16)
+                | ((hash[offset + 2] & 0xFF) << 8)
+                | (hash[offset + 3] & 0xFF);
+
+        int otp = binary % (int) Math.pow(10, CODE_LENGTH);
+        return String.format("%0" + CODE_LENGTH + "d", otp);
+    }
 }

+ 20 - 0
src/main/resources/Messages.properties

@@ -0,0 +1,20 @@
+# Core MFA Properties
+MfaUserProperty.displayName=MFA (TOTP Authenticator)
+
+# Field Validation Messages
+MfaUserProperty.secretKey.missing=Secret Key is required
+MfaUserProperty.totpCode.missing=Please enter the verification code
+MfaUserProperty.totpCode.invalid=Invalid code. Check your authenticator app.
+MfaUserProperty.totpCode.warning=Secret has not been generated yet
+MfaUserProperty.totpCode.valid=Code is valid
+
+# UI Labels
+MfaUserProperty.enable.label=Enable MFA (TOTP)
+MfaUserProperty.qrCode.label=QR Code
+MfaUserProperty.qrCode.alt=Scan this QR code with your authenticator app
+MfaUserProperty.verificationCode.label=Verification Code
+
+# Global Configuration
+MfaGlobalConfig.enforceForAllUsers.label=Enforce MFA for all users
+MfaGlobalConfig.excludeApiTokens.label=Exclude API tokens from MFA
+MfaGlobalConfig.section.title=MFA Global Settings

+ 19 - 0
src/main/resources/Messages_pt_BR.properties

@@ -0,0 +1,19 @@
+# Core MFA Properties
+MfaUserProperty.displayName=MFA (Autenticador TOTP)
+
+# Field Validation Messages
+MfaUserProperty.secretKey.missing=Chave secreta é obrigatória
+MfaUserProperty.totpCode.missing=Informe o código de verificação
+MfaUserProperty.totpCode.invalid=Código inválido. Verifique seu aplicativo autenticador.
+MfaUserProperty.totpCode.warning=Chave secreta ainda não foi gerada
+MfaUserProperty.totpCode.valid=Código válido
+
+# UI Labels
+MfaUserProperty.enable.label=Ativar MFA (TOTP)
+MfaUserProperty.qrCode.label=Código QR
+MfaUserProperty.qrCode.alt=Escaneie este código QR com seu aplicativo autenticador
+MfaUserProperty.verificationCode.label=Código de verificação
+
+# Global Configuration
+MfaGlobalConfig.enforceForAllUsers.label=Exigir MFA para todos os usuários
+MfaGlobalConfig.excludeApiTokens.label=Excluir tokens de API do MFA

+ 1 - 1
src/main/resources/index.jelly

@@ -1,6 +1,6 @@
 <?jelly escape-by-default='true'?>
 <div>
-  Adds TOTP-based Multi-Factor Authentication (MFA/2FA) to Jenkins using Google Authenticator.
+  Adds TOTP-based Multi-Factor Authentication (MFA/2FA) to Jenkins using TOTP Authenticator.
   Enhances security by requiring a time-based one-time password in addition to regular credentials.
   Features QR code setup, per-user enforcement, and secure secret storage.
 </div>

+ 18 - 4
src/main/resources/io/jenkins/plugins/Messages.properties

@@ -1,6 +1,20 @@
-MfaUserProperty.secretKey.missing=Secret Key not provided
-MfaUserProperty.totpCode.missing=Please enter the code from your app
+# Core MFA Properties
+MfaUserProperty.displayName=MFA (TOTP Authenticator)
+
+# Field Validation Messages
+MfaUserProperty.secretKey.missing=Secret Key is required
+MfaUserProperty.totpCode.missing=Please enter the verification code
 MfaUserProperty.totpCode.invalid=Invalid code. Check your authenticator app.
-MfaUserProperty.totpCode.warning=Secret has not been generated yet.
+MfaUserProperty.totpCode.warning=Secret has not been generated yet
 MfaUserProperty.totpCode.valid=Code is valid
-MfaUserProperty.displayName=MFA (Google Authenticator)
+
+# UI Labels
+MfaUserProperty.enable.label=Enable MFA (TOTP)
+MfaUserProperty.qrCode.label=QR Code
+MfaUserProperty.qrCode.alt=Scan this QR code with your authenticator app
+MfaUserProperty.verificationCode.label=Verification Code
+
+# Global Configuration
+MfaGlobalConfig.enforceForAllUsers.label=Enforce MFA for all users
+MfaGlobalConfig.excludeApiTokens.label=Exclude API tokens from MFA
+MfaGlobalConfig.section.title=MFA Global Settings

+ 18 - 5
src/main/resources/io/jenkins/plugins/Messages_pt_BR.properties

@@ -1,6 +1,19 @@
-MfaUserProperty.secretKey.missing=SecretKey não informado
-MfaUserProperty.totpCode.missing=Informe o código do app
-MfaUserProperty.totpCode.invalid=Código do app inválido. Verifique no Google Authenticator.
-MfaUserProperty.totpCode.warning=Secret ainda não foi gerado.
+# Core MFA Properties
+MfaUserProperty.displayName=MFA (Autenticador TOTP)
+
+# Field Validation Messages
+MfaUserProperty.secretKey.missing=Chave secreta é obrigatória
+MfaUserProperty.totpCode.missing=Informe o código de verificação
+MfaUserProperty.totpCode.invalid=Código inválido. Verifique seu aplicativo autenticador.
+MfaUserProperty.totpCode.warning=Chave secreta ainda não foi gerada
 MfaUserProperty.totpCode.valid=Código válido
-MfaUserProperty.displayName=MFA (Google Authenticator)
+
+# UI Labels
+MfaUserProperty.enable.label=Ativar MFA (TOTP)
+MfaUserProperty.qrCode.label=Código QR
+MfaUserProperty.qrCode.alt=Escaneie este código QR com seu aplicativo autenticador
+MfaUserProperty.verificationCode.label=Código de verificação
+
+# Global Configuration
+MfaGlobalConfig.enforceForAllUsers.label=Exigir MFA para todos os usuários
+MfaGlobalConfig.excludeApiTokens.label=Excluir tokens de API do MFA

+ 11 - 0
src/main/resources/io/jenkins/plugins/MfaGlobalConfig/config.jelly

@@ -0,0 +1,11 @@
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
+    <f:section title="Global MFA Settings">
+        <f:entry title="Enforce MFA for all users" field="enforceMfaForAllUsers">
+            <f:checkbox />
+        </f:entry>
+        <f:entry title="Exclude API tokens from MFA" field="excludeApiTokens">
+            <f:checkbox />
+        </f:entry>
+    </f:section>
+</j:jelly>

+ 5 - 0
src/main/resources/io/jenkins/plugins/MfaGlobalConfig/config.properties

@@ -0,0 +1,5 @@
+enforceMfaForAllUsers.displayName=Enforce MFA for all users
+enforceMfaForAllUsers.description=When enabled, all users will be required to set up and use MFA.
+
+excludeApiTokens.displayName=Exclude API tokens from MFA
+excludeApiTokens.description=Allow API token authentication to bypass MFA requirement.

+ 1 - 0
src/main/resources/io/jenkins/plugins/MfaUserProperty/MfaUserProperty.properties

@@ -0,0 +1 @@
+adjuncts.io.jenkins.plugins.MfaUserProperty.script=/io/jenkins/plugins/MfaUserProperty/script.js

+ 15 - 52
src/main/resources/io/jenkins/plugins/MfaUserProperty/config.jelly

@@ -1,69 +1,32 @@
-<!-- src/main/resources/io/jenkins/plugins/MfaUserProperty/config.jelly -->
 <?jelly escape-by-default='true'?>
-<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
+<j:jelly xmlns:j="jelly:core" 
+         xmlns:f="/lib/form" 
+         xmlns:l="/lib/layout"
+         xmlns:st="jelly:stapler"> <!-- Adicione esta linha -->
 
-  <f:entry title="Ativar MFA" field="mfaEnabled">
+  <f:entry title="${%MfaUserProperty.enable.label}" field="mfaEnabled">
     <f:checkbox id="mfaEnabledCheckbox" />
   </f:entry>
 
   <j:if test="${!instance.mfaEnabled}">
     <div id="mfaSetupContainer" style="display:none; margin-top:10px;">
-      <f:entry title="QR Code">
-        <img id="qrCodeImage" alt="QR Code para Google Authenticator" />
+      <f:entry title="${%MfaUserProperty.qrCode.label}">
+        <img id="qrCodeImage" alt="${%MfaUserProperty.qrCode.alt}" />
       </f:entry>
 
-      <f:entry title="Código do App">
+      <f:entry title="${%MfaUserProperty.verificationCode.label}">
         <f:textbox field="totpCode" />
       </f:entry>
 
-      <!-- input escondido para secretKey -->
       <input type="hidden" name="_.secretKey" />
     </div>
   </j:if>
-  <script>
-    (function() {
-      const checkbox = document.getElementById('mfaEnabledCheckbox');
-      const container = document.getElementById('mfaSetupContainer');
-      const qrImage = document.getElementById('qrCodeImage');
-      const secretInput = document.querySelector('input[name="_.secretKey"]');
-
-      function showOrHideMfaSetup() {
-        if (checkbox.checked) {
-          container.style.display = 'block';
-
-          // Se ainda não tiver QR gerado, buscar do backend
-          console.log('Image SRC: ' + qrImage.src)
-          if (!qrImage.src) {
-            console.log('${rootURL}/mfa-google-auth/generateSecret')
-            fetch('${rootURL}/mfa-google-auth/generateSecret')
-              .then(resp => resp.json())
-              .then(data => {
-                console.log('${rootURL}/mfa-google-auth/qrcode?secret=' + encodeURIComponent(data.secret))
-                qrImage.src = '${rootURL}/mfa-google-auth/qrcode?secret=' + encodeURIComponent(data.secret);
-                secretInput.value = data.secret;
-              })
-              .catch(err => {
-                console.error('Erro ao gerar QR Code:', err);
-              });
-          }
-        } else {
-          console.log('ERRO __')
-          container.style.display = 'none';
-          qrImage.src = '';
-          secretInput.value = '';
-        }
-      }
 
-      checkbox.addEventListener('change', () => {
-          console.log('Checkbox mudou! Estado:', checkbox.checked);
-        showOrHideMfaSetup();
-      });
-
-      // Ao carregar a página, checar se já estava ativo para mostrar QR e campo
-      if (checkbox.checked) {
-        showOrHideMfaSetup();
-      }
-    })();
+  <st:adjunct includes="io.jenkins.plugins.MfaUserProperty.script" />
+  
+  <script>
+    document.addEventListener('DOMContentLoaded', function() {
+      initMfaToggle();
+    });
   </script>
-
-</j:jelly>
+</j:jelly>

+ 15 - 0
src/main/resources/io/jenkins/plugins/MfaUserProperty/config.properties

@@ -0,0 +1,15 @@
+# Core MFA Properties
+MfaUserProperty.displayName=MFA (TOTP Authenticator)
+
+# Field Validation Messages
+MfaUserProperty.secretKey.missing=Secret Key is required
+MfaUserProperty.totpCode.missing=Please enter the verification code
+MfaUserProperty.totpCode.invalid=Invalid code. Check your authenticator app.
+MfaUserProperty.totpCode.warning=Secret has not been generated yet
+MfaUserProperty.totpCode.valid=Code is valid
+
+# UI Labels
+MfaUserProperty.enable.label=Enable MFA (TOTP)
+MfaUserProperty.qrCode.label=QR Code
+MfaUserProperty.qrCode.alt=Scan this QR code with your authenticator app
+MfaUserProperty.verificationCode.label=Verification Code

+ 38 - 0
src/main/resources/io/jenkins/plugins/MfaUserProperty/script.js

@@ -0,0 +1,38 @@
+function initMfaToggle() {
+    const checkbox = document.getElementById('mfaEnabledCheckbox');
+    const container = document.getElementById('mfaSetupContainer');
+    const qrImage = document.getElementById('qrCodeImage');
+    const secretInput = document.querySelector('input[name="_.secretKey"]');
+
+    function showOrHideMfaSetup() {
+        if (checkbox.checked) {
+            container.style.display = 'block';
+
+            if (!qrImage.src) {
+                fetch(window.rootURL + '/mfa-totp/generateSecret')
+                    .then(resp => resp.json())
+                    .then(data => {
+                        qrImage.src = window.rootURL + '/mfa-totp/qrcode?secret=' + 
+                                      encodeURIComponent(data.secret);
+                        secretInput.value = data.secret;
+                    })
+                    .catch(err => {
+                        console.error('Error generating QR Code:', err);
+                    });
+            }
+        } else {
+            container.style.display = 'none';
+            qrImage.src = '';
+            secretInput.value = '';
+        }
+    }
+
+    checkbox.addEventListener('change', showOrHideMfaSetup);
+
+    if (checkbox.checked) {
+        showOrHideMfaSetup();
+    }
+}
+
+// Exporta a função para ser chamada pelo jelly
+window.initMfaToggle = initMfaToggle;

+ 4 - 4
src/main/resources/io/jenkins/plugins/MfaVerifyAction/index.jelly

@@ -2,18 +2,18 @@
 <j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">
   <l:layout title="MFA Verification">
     <l:main-panel>
-      <h1>Verificação MFA</h1>
+      <h1>MFA Verify</h1>
       <j:if test="${request.getParameter('error') != null}">
         <div style="color:red; font-weight:bold;">
-          Código inválido. Tente novamente.
+          Invalid code. Try again.
         </div>
       </j:if>
       <form method="post" action="${rootURL}/mfa-verify/verify">
-        <f:entry title="Código do app">
+        <f:entry title="Code">
           <f:textbox name="totpCode" />
         </f:entry>
         <f:entry>
-          <f:submit value="Verificar" />
+          <f:submit value="Verify" />
         </f:entry>
       </form>
     </l:main-panel>

+ 0 - 60
src/test/java/io/jenkins/plugins/MfaFilterRegisterTest.java

@@ -1,60 +0,0 @@
-package io.jenkins.plugins;
-
-import static org.junit.Assert.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-import hudson.init.InitMilestone;
-import hudson.init.Initializer;
-import hudson.util.PluginServletFilter;
-import javax.servlet.Filter;
-import javax.servlet.ServletException;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.MockedStatic;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class MfaFilterRegisterTest {
-
-    @Test
-    public void testInitializationMilestone() throws Exception {
-        // Verifica se a anotação está correta
-        Initializer initializer =
-                MfaFilterRegister.class.getDeclaredMethod("init").getAnnotation(Initializer.class);
-
-        assertNotNull(initializer);
-        assertEquals(InitMilestone.PLUGINS_STARTED, initializer.after());
-    }
-
-    @Test
-    public void testFilterRegistration() throws Exception {
-        try (MockedStatic<PluginServletFilter> pluginServletFilter = mockStatic(PluginServletFilter.class)) {
-            // Configura o mock para resolver a ambiguidade
-            pluginServletFilter
-                    .when(() -> PluginServletFilter.addFilter(any(Filter.class)))
-                    .thenAnswer(invocation -> null);
-
-            // Executa o método de registro
-            MfaFilterRegister.init();
-
-            // Verifica se o filtro foi adicionado
-            pluginServletFilter.verify(() -> PluginServletFilter.addFilter(isA(MfaFilter.class)));
-        }
-    }
-
-    @Test
-    public void testServletExceptionHandling() {
-        try (MockedStatic<PluginServletFilter> pluginServletFilter = mockStatic(PluginServletFilter.class)) {
-            // Configura para lançar exceção
-            pluginServletFilter
-                    .when(() -> PluginServletFilter.addFilter(any(Filter.class)))
-                    .thenThrow(new ServletException("Test error"));
-
-            // Verifica se a exceção é propagada
-            ServletException exception = assertThrows(ServletException.class, MfaFilterRegister::init);
-
-            assertEquals("Test error", exception.getMessage());
-        }
-    }
-}

+ 0 - 187
src/test/java/io/jenkins/plugins/MfaFilterTest.java

@@ -1,187 +0,0 @@
-package io.jenkins.plugins;
-
-import static org.mockito.Mockito.*;
-
-import hudson.model.User;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import jenkins.model.Jenkins;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockedStatic;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class MfaFilterTest {
-
-    @Mock
-    private HttpServletRequest request;
-
-    @Mock
-    private HttpServletResponse response;
-
-    @Mock
-    private FilterChain chain;
-
-    @Mock
-    private HttpSession session;
-
-    @Mock
-    private FilterConfig filterConfig;
-
-    @Mock
-    private User user;
-
-    @Mock
-    private MfaUserProperty mfaProperty;
-
-    @Mock
-    private Jenkins jenkins;
-
-    private MfaFilter filter;
-
-    @Before
-    public void setUp() {
-        filter = new MfaFilter();
-        when(request.getSession()).thenReturn(session);
-        when(request.getContextPath()).thenReturn("/jenkins");
-        when(request.getRequestURI()).thenReturn("/jenkins/some-path");
-    }
-
-    @Test
-    public void testInitAndDestroy() throws Exception {
-        filter.init(filterConfig);
-        filter.destroy();
-    }
-
-    @Test
-    public void testNonHttpRequestPassesThrough() throws Exception {
-        ServletRequest nonHttpRequest = mock(ServletRequest.class);
-        ServletResponse nonHttpResponse = mock(ServletResponse.class);
-
-        filter.doFilter(nonHttpRequest, nonHttpResponse, chain);
-
-        verify(chain).doFilter(nonHttpRequest, nonHttpResponse);
-    }
-
-    @Test
-    public void testJenkinsNotInitializedPassesThrough() throws Exception {
-        try (MockedStatic<Jenkins> mockedJenkins = mockStatic(Jenkins.class)) {
-            mockedJenkins.when(Jenkins::getInstanceOrNull).thenReturn(null);
-            when(request.getRequestURI()).thenReturn("/jenkins/some-path");
-
-            filter.doFilter(request, response, chain);
-
-            verify(chain).doFilter(request, response);
-        }
-    }
-
-    @Test
-    public void testNoUserLoggedInPassesThrough() throws Exception {
-        try (MockedStatic<Jenkins> mockedJenkins = mockStatic(Jenkins.class);
-                MockedStatic<User> mockedUser = mockStatic(User.class)) {
-
-            mockedJenkins.when(Jenkins::getInstanceOrNull).thenReturn(jenkins);
-            mockedUser.when(User::current).thenReturn(null);
-            when(request.getRequestURI()).thenReturn("/jenkins/some-path");
-
-            filter.doFilter(request, response, chain);
-
-            verify(chain).doFilter(request, response);
-        }
-    }
-
-    @Test
-    public void testUserWithoutMfaEnabledPassesThrough() throws Exception {
-        try (MockedStatic<Jenkins> mockedJenkins = mockStatic(Jenkins.class);
-                MockedStatic<User> mockedUser = mockStatic(User.class)) {
-
-            mockedJenkins.when(Jenkins::getInstanceOrNull).thenReturn(jenkins);
-            mockedUser.when(User::current).thenReturn(user);
-            when(user.getProperty(MfaUserProperty.class)).thenReturn(null);
-            when(request.getRequestURI()).thenReturn("/jenkins/some-path");
-
-            filter.doFilter(request, response, chain);
-
-            verify(chain).doFilter(request, response);
-        }
-    }
-
-    @Test
-    public void testMfaEnabledButNotVerifiedRedirects() throws Exception {
-        try (MockedStatic<Jenkins> mockedJenkins = mockStatic(Jenkins.class);
-                MockedStatic<User> mockedUser = mockStatic(User.class)) {
-
-            mockedJenkins.when(Jenkins::getInstanceOrNull).thenReturn(jenkins);
-            mockedUser.when(User::current).thenReturn(user);
-            when(user.getProperty(MfaUserProperty.class)).thenReturn(mfaProperty);
-            when(mfaProperty.isMfaEnabled()).thenReturn(true);
-            when(request.getRequestURI()).thenReturn("/jenkins/restricted");
-
-            filter.doFilter(request, response, chain);
-
-            verify(response).sendRedirect("/jenkins/mfa-verify");
-            verify(chain, never()).doFilter(request, response);
-        }
-    }
-
-    @Test
-    public void testMfaEnabledAndVerifiedPassesThrough() throws Exception {
-        try (MockedStatic<Jenkins> mockedJenkins = mockStatic(Jenkins.class);
-                MockedStatic<User> mockedUser = mockStatic(User.class)) {
-
-            mockedJenkins.when(Jenkins::getInstanceOrNull).thenReturn(jenkins);
-            mockedUser.when(User::current).thenReturn(user);
-            when(user.getProperty(MfaUserProperty.class)).thenReturn(mfaProperty);
-            when(mfaProperty.isMfaEnabled()).thenReturn(true);
-            when(session.getAttribute("mfa-verified")).thenReturn(true);
-            when(request.getRequestURI()).thenReturn("/jenkins/restricted");
-
-            filter.doFilter(request, response, chain);
-
-            verify(chain).doFilter(request, response);
-            verify(response, never()).sendRedirect(anyString());
-        }
-    }
-
-    @Test
-    public void testExcludedPathsPassThrough() throws Exception {
-        String[] excludedPaths = {
-            "/jenkins/static/resource.css",
-            "/jenkins/adjuncts/script.js",
-            "/jenkins/mfa-verify",
-            "/jenkins/login",
-            "/jenkins/signup",
-            "/jenkins/error",
-            "/jenkins/securityRealm"
-        };
-
-        for (String path : excludedPaths) {
-            when(request.getRequestURI()).thenReturn(path);
-            filter.doFilter(request, response, chain);
-        }
-
-        verify(chain, times(excludedPaths.length)).doFilter(request, response);
-    }
-
-    @Test
-    public void testStaticResourcesWithDifferentContextPath() throws Exception {
-        when(request.getContextPath()).thenReturn("/custom-context");
-        when(request.getRequestURI()).thenReturn("/custom-context/static/resource.css");
-
-        try (MockedStatic<Jenkins> mockedJenkins = mockStatic(Jenkins.class)) {
-            mockedJenkins.when(Jenkins::getInstanceOrNull).thenReturn(jenkins);
-
-            filter.doFilter(request, response, chain);
-
-            verify(chain).doFilter(request, response);
-        }
-    }
-}