Ver código fonte

first version, working

Allan Barcelos 1 ano atrás
commit
1c77faa011

+ 1 - 0
.github/CODEOWNERS

@@ -0,0 +1 @@
+* @jenkinsci/mfa-google-auth-plugin-developers

+ 12 - 0
.github/dependabot.yml

@@ -0,0 +1,12 @@
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates
+---
+version: 2
+updates:
+  - package-ecosystem: maven
+    directory: /
+    schedule:
+      interval: monthly
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: monthly

+ 4 - 0
.github/release-drafter.yml

@@ -0,0 +1,4 @@
+# See https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc
+---
+_extends: .github
+tag-template: mfa-google-auth-$NEXT_MINOR_VERSION

+ 23 - 0
.github/workflows/jenkins-security-scan.yml

@@ -0,0 +1,23 @@
+# More information about the Jenkins security scan can be found at the developer docs: https://www.jenkins.io/redirect/jenkins-security-scan/
+---
+name: Jenkins Security Scan
+on:
+  push:
+    branches:
+      - "master"
+      - "main"
+  pull_request:
+    types: [opened, synchronize, reopened]
+  workflow_dispatch:
+
+permissions:
+  security-events: write
+  contents: read
+  actions: read
+
+jobs:
+  security-scan:
+    uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2
+    with:
+      java-cache: 'maven'  # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate.
+      # java-version: 21  # Optionally specify what version of Java to set up for the build, or remove to use a recent default.

+ 19 - 0
.github/workflows/release-drafter.yml

@@ -0,0 +1,19 @@
+# Automates creation of Release Drafts using Release Drafter
+# More Info: https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc
+---
+name: Release Drafter
+
+on:
+  push:
+    branches:
+      - master
+      - main
+
+jobs:
+  update_release_draft:
+    runs-on: ubuntu-latest
+    steps:
+      # Drafts your next Release notes as Pull Requests are merged into the default branch
+      - uses: release-drafter/release-drafter@v6
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+target
+
+# mvn hpi:run
+work
+
+# IntelliJ IDEA project files
+*.iml
+*.iws
+*.ipr
+.idea
+
+# Eclipse project files
+.settings
+.classpath
+.project

+ 7 - 0
.mvn/extensions.xml

@@ -0,0 +1,7 @@
+<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 https://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
+  <extension>
+    <groupId>io.jenkins.tools.incrementals</groupId>
+    <artifactId>git-changelist-maven-extension</artifactId>
+    <version>1.10</version>
+  </extension>
+</extensions>

+ 2 - 0
.mvn/maven.config

@@ -0,0 +1,2 @@
+-Pconsume-incrementals
+-Pmight-produce-incrementals

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "java.configuration.updateBuildConfiguration": "interactive"
+}

+ 11 - 0
Jenkinsfile

@@ -0,0 +1,11 @@
+/*
+ * See the documentation for more options:
+ * https://github.com/jenkins-infra/pipeline-library/
+ */
+buildPlugin(
+  forkCount: '1C', // run this number of tests in parallel for faster feedback.  If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores
+  useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests
+  configurations: [
+    [platform: 'linux', jdk: 21],
+    [platform: 'windows', jdk: 17],
+])

+ 9 - 0
LICENSE.md

@@ -0,0 +1,9 @@
+The MIT License
+
+Copyright 2025
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 77 - 0
README.md

@@ -0,0 +1,77 @@
+# MFA Google Auth Plugin
+
+[![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/mfa-google-auth.svg)](https://plugins.jenkins.io/mfa-google-auth)
+[![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**.
+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.
+
+---
+
+## ✨ Features
+
+✅ Enforces MFA for Jenkins users
+✅ Integrates with [Google Authenticator](https://github.com/google/google-authenticator) or any TOTP app
+✅ Configurable enforcement policies
+✅ Easy to set up and manage through Jenkins UI
+
+---
+
+## 📦 Installation
+
+1. Build the plugin:
+
+   ```bash
+   mvn clean package
+   ```
+
+   After a successful build, you will find the `.hpi` file in the `target/` directory.
+
+2. Upload to Jenkins:
+
+   * Go to **Manage Jenkins → Manage Plugins → Advanced**
+   * Use the **Upload Plugin** section to upload the `mfa-google-auth.hpi`.
+
+3. Restart Jenkins if required.
+
+---
+
+## 🔧 Configuration
+
+1. In Jenkins, go to **Manage Jenkins → Configure Global Security**.
+2. Enable **MFA Google Auth Plugin** under the security realm or configure it as instructed in your environment.
+3. Each user should scan the generated QR code with Google Authenticator (or a compatible TOTP app) and enter their verification code.
+
+---
+
+## 📌 Requirements
+
+* **Jenkins Core**: `2.479.3` or newer
+* **Java**: 17 or newer
+* A TOTP app such as [Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2).
+
+---
+
+## 🛠 Development
+
+This plugin is built with the [Jenkins Plugin Parent POM](https://github.com/jenkinsci/plugin-pom).
+
+To work on the plugin locally:
+
+```bash
+# Build and run tests
+mvn clean verify
+
+# Run Jenkins with the plugin
+mvn hpi:run
+```
+
+Then open Jenkins at [http://localhost:8080/jenkins/](http://localhost:8080/jenkins/).
+
+
+---
+
+## 📜 License
+
+This project is licensed under the [MIT License](LICENSE).

+ 89 - 0
pom.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.jenkins-ci.plugins</groupId>
+    <artifactId>plugin</artifactId>
+    <version>5.18</version>
+    <relativePath />
+  </parent>
+
+  <groupId>io.jenkins.plugins</groupId>
+  <artifactId>mfa-google-auth</artifactId>
+  <version>${revision}${changelist}</version>
+  <packaging>hpi</packaging>
+
+  <name>MFA Google Auth Plugin</name>
+  <description>Multi-Factor Authentication with Google Authenticator.</description>
+  <url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>
+  <licenses>
+    <license>
+      <name>MIT License</name>
+      <url>https://opensource.org/license/mit/</url>
+    </license>
+  </licenses>
+  <scm child.scm.connection.inherit.append.path="false" child.scm.developerConnection.inherit.append.path="false" child.scm.url.inherit.append.path="false">
+    <connection>scm:git:https://github.com/${gitHubRepo}</connection>
+    <developerConnection>scm:git:https://github.com/${gitHubRepo}</developerConnection>
+    <tag>${scmTag}</tag>
+    <url>https://github.com/${gitHubRepo}</url>
+  </scm>
+
+  <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>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>io.jenkins.tools.bom</groupId>
+        <artifactId>bom-${jenkins.baseline}.x</artifactId>
+        <version>5043.v855ff4819a_0f</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>com.google.zxing</groupId>
+      <artifactId>core</artifactId>
+      <version>3.5.3</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.zxing</groupId>
+      <artifactId>javase</artifactId>
+      <version>3.5.3</version>
+    </dependency>
+    <dependency>
+      <groupId>com.warrenstrange</groupId>
+      <artifactId>googleauth</artifactId>
+      <version>1.5.0</version>
+    </dependency>
+
+  </dependencies>
+
+  <repositories>
+    <repository>
+      <id>repo.jenkins-ci.org</id>
+      <url>https://repo.jenkins-ci.org/public/</url>
+    </repository>
+  </repositories>
+  <pluginRepositories>
+    <pluginRepository>
+      <id>repo.jenkins-ci.org</id>
+      <url>https://repo.jenkins-ci.org/public/</url>
+    </pluginRepository>
+  </pluginRepositories>
+
+</project>

+ 80 - 0
src/main/java/io/jenkins/plugins/MfaEnforceFilter.java

@@ -0,0 +1,80 @@
+/*
+ * Project: MFA Google Auth Plugin
+ *
+ * Class: MfaEnforceFilter
+ *
+ * This servlet filter enforces multi-factor authentication (MFA) for Jenkins users.
+ * It intercepts HTTP requests and redirects users with MFA enabled but not yet verified
+ * to the MFA verification page before allowing access to other Jenkins pages.
+ * Static resources and the verification page itself are excluded from this enforcement.
+ *
+ * Author: Allan Barcelos
+ * Date: 2025-07-17
+ */
+
+package io.jenkins.plugins;
+
+import hudson.Extension;
+import hudson.model.User;
+import hudson.util.PluginServletFilter;
+import java.io.IOException;
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+@Extension
+public class MfaEnforceFilter implements javax.servlet.Filter {
+
+    static {
+        try {
+            PluginServletFilter.addFilter(new MfaEnforceFilter());
+        } catch (ServletException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+            HttpServletRequest req = (HttpServletRequest) request;
+            HttpServletResponse rsp = (HttpServletResponse) response;
+
+            String path = req.getRequestURI();
+
+            // Skip static resources
+            if (path.startsWith(req.getContextPath() + "/static/")
+                    || path.startsWith(req.getContextPath() + "/adjuncts/")) {
+                chain.doFilter(request, response);
+                return;
+            }
+
+            User current = User.current();
+
+            if (current != null) {
+                HttpSession session = req.getSession(false);
+                boolean verified = session != null && Boolean.TRUE.equals(session.getAttribute("mfa-verified"));
+
+                MfaUserProperty mfa = current.getProperty(MfaUserProperty.class);
+                boolean mfaEnabled = mfa != null && mfa.isMfaEnabled();
+
+                // String path = req.getRequestURI();
+
+                if (mfaEnabled && !verified && !path.contains("/mfa-verify")) {
+                    rsp.sendRedirect(req.getContextPath() + "/mfa-verify/");
+                    return;
+                }
+            }
+        }
+
+        chain.doFilter(request, response);
+    }
+
+    @Override
+    public void init(javax.servlet.FilterConfig filterConfig) {}
+
+    @Override
+    public void destroy() {}
+}

+ 82 - 0
src/main/java/io/jenkins/plugins/MfaFilter.java

@@ -0,0 +1,82 @@
+/*
+ * Project: MFA Google Auth Plugin
+ *
+ * Class: MfaFilter
+ *
+ * This servlet filter blocks access to Jenkins pages for users who have MFA enabled
+ * but have not yet completed MFA verification in the current session.
+ * It allows free access to MFA verification and login URLs.
+ *
+ * Author: Allan Barcelos
+ * Date: 2025-07-17
+ */
+
+package io.jenkins.plugins;
+
+import hudson.Extension;
+import hudson.model.User;
+import java.io.IOException;
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import jenkins.model.Jenkins;
+
+/**
+ * Filtro que bloqueia o acesso se o usuário não tiver passado pela verificação MFA.
+ */
+@Extension
+public class MfaFilter implements Filter {
+
+    @Override
+    public void init(FilterConfig filterConfig) {
+        // nada a inicializar
+    }
+
+    @Override
+    public void destroy() {
+        // nada a destruir
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        if (!(request instanceof HttpServletRequest)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        HttpServletRequest req = (HttpServletRequest) request;
+        HttpServletResponse rsp = (HttpServletResponse) response;
+
+        // Garante que Jenkins já está inicializado
+        if (Jenkins.getInstanceOrNull() == null) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        User u = User.current();
+        if (u != null) {
+            MfaUserProperty mfa = u.getProperty(MfaUserProperty.class);
+            if (mfa != null && mfa.isMfaEnabled()) {
+                Object verified = req.getSession().getAttribute("mfa-verified");
+                String path = req.getRequestURI();
+                String ctx = req.getContextPath();
+
+                // Permitir acesso às próprias URLs de verificação MFA e login
+                if (path.startsWith(ctx + "/mfa-verify") || path.startsWith(ctx + "/login")) {
+                    chain.doFilter(request, response);
+                    return;
+                }
+
+                // Se não verificado, redirecionar
+                if (verified == null) {
+                    rsp.sendRedirect(ctx + "/mfa-verify");
+                    return;
+                }
+            }
+        }
+
+        chain.doFilter(request, response);
+    }
+}

+ 28 - 0
src/main/java/io/jenkins/plugins/MfaFilterRegister.java

@@ -0,0 +1,28 @@
+/*
+ * Project: MFA Google Auth Plugin
+ *
+ * Class: MfaFilterRegister
+ *
+ * Registers the MfaFilter servlet filter with Jenkins after all plugins have started.
+ * This ensures the MFA enforcement filter is applied to incoming HTTP requests.
+ *
+ * Author: Allan Barcelos
+ * Date: 2025-07-17
+ */
+
+package io.jenkins.plugins;
+
+import hudson.Extension;
+import hudson.init.InitMilestone;
+import hudson.init.Initializer;
+import hudson.util.PluginServletFilter;
+import javax.servlet.ServletException;
+
+@Extension
+public class MfaFilterRegister {
+
+    @Initializer(after = InitMilestone.PLUGINS_STARTED)
+    public static void init() throws ServletException {
+        PluginServletFilter.addFilter(new MfaFilter());
+    }
+}

+ 100 - 0
src/main/java/io/jenkins/plugins/MfaUserProperty.java

@@ -0,0 +1,100 @@
+/*
+ * Project: MFA Google Auth Plugin
+ *
+ * Class: MfaUserProperty
+ *
+ * Represents a user property that manages multi-factor authentication (MFA) settings
+ * for a Jenkins user. It stores whether MFA is enabled and the secret key used for
+ * TOTP verification. The constructor validates the TOTP code when MFA is enabled.
+ *
+ * Includes an inner Descriptor class to integrate with Jenkins user property UI,
+ * providing validation and display name.
+ *
+ * Author: Allan Barcelos
+ * Date: 2025-07-17
+ */
+
+package io.jenkins.plugins;
+
+import hudson.Extension;
+import hudson.model.Descriptor.FormException;
+import hudson.model.User;
+import hudson.model.UserProperty;
+import hudson.model.UserPropertyDescriptor;
+import hudson.util.FormValidation;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+
+public class MfaUserProperty extends UserProperty {
+    private final boolean mfaEnabled;
+    private final String 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");
+            }
+            if (totpCode == null || totpCode.isEmpty()) {
+                throw new FormException(Messages.MfaUserProperty_totpCode_missing(), "totpCode");
+            }
+            if (!TOTPUtil.verifyCode(secretKey, totpCode)) {
+                throw new FormException(Messages.MfaUserProperty_totpCode_invalid(), "totpCode");
+            }
+        }
+        this.mfaEnabled = mfaEnabled;
+        this.secretKey = secretKey;
+    }
+
+    public boolean isMfaEnabled() {
+        return mfaEnabled;
+    }
+
+    public String 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();
+            }
+            return null;
+        }
+
+        @Override
+        public String getDisplayName() {
+            return Messages.MfaUserProperty_displayName();
+        }
+
+        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();
+            }
+            if (secretKey == null || secretKey.isEmpty()) {
+                return FormValidation.warning(Messages.MfaUserProperty_totpCode_warning());
+            }
+            boolean valid = TOTPUtil.verifyCode(secretKey, value);
+            return valid
+                    ? FormValidation.ok(Messages.MfaUserProperty_totpCode_valid())
+                    : FormValidation.error(Messages.MfaUserProperty_totpCode_invalid());
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return true;
+        }
+    }
+}

+ 73 - 0
src/main/java/io/jenkins/plugins/MfaVerifyAction.java

@@ -0,0 +1,73 @@
+/*
+ * Project: MFA Google Auth Plugin
+ *
+ * Class: MfaVerifyAction
+ *
+ * Provides the intermediate MFA verification page at the URL /mfa-verify.
+ * Handles the submission of the TOTP code from users with MFA enabled.
+ * On successful verification, marks the session as MFA-verified and redirects to the Jenkins main page.
+ * If verification fails, redirects back to the MFA verification page with an error indication.
+ * Users without MFA enabled are redirected to the main Jenkins page directly.
+ *
+ * Author: Allan Barcelos
+ * Date: 2025-07-17
+ */
+package io.jenkins.plugins;
+
+import hudson.Extension;
+import hudson.model.RootAction;
+import hudson.model.User;
+import java.io.IOException;
+import java.util.logging.Logger;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+
+/**
+ * Página intermediária para o segundo fator (MFA).
+ * URL: /mfa-verify
+ */
+@Extension
+public class MfaVerifyAction implements RootAction {
+
+    private static final Logger LOGGER = Logger.getLogger(MfaVerifyAction.class.getName());
+
+    @Override
+    public String getIconFileName() {
+        return null; // não mostra no menu lateral
+    }
+
+    @Override
+    public String getDisplayName() {
+        return "MFA Verify";
+    }
+
+    @Override
+    public String getUrlName() {
+        return "mfa-verify";
+    }
+
+    /**
+     * Processa o POST com o código TOTP
+     */
+    public void doVerify(StaplerRequest req, StaplerResponse rsp) throws IOException {
+        User u = User.current();
+        if (u == null) {
+            rsp.sendRedirect(req.getContextPath() + "/login");
+            return;
+        }
+        MfaUserProperty mfa = u.getProperty(MfaUserProperty.class);
+        if (mfa != null && mfa.isMfaEnabled()) {
+            String code = req.getParameter("totpCode");
+            if (TOTPUtil.verifyCode(mfa.getSecretKey(), code)) {
+                req.getSession().setAttribute("mfa-verified", true);
+                rsp.sendRedirect(req.getContextPath() + "/");
+                return;
+            } else {
+                rsp.sendRedirect("mfa-verify?error=1");
+                return;
+            }
+        }
+        // Se usuário não tem MFA, apenas redireciona
+        rsp.sendRedirect(req.getContextPath() + "/");
+    }
+}

+ 100 - 0
src/main/java/io/jenkins/plugins/QrCodeAction.java

@@ -0,0 +1,100 @@
+/*
+ * Project: MFA Google 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.
+ *
+ * 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
+ */
+
+package io.jenkins.plugins;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.WriterException;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import hudson.Extension;
+import hudson.model.RootAction;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.logging.Logger;
+import net.sf.json.JSONObject;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+
+@Extension
+public class QrCodeAction implements RootAction {
+
+    private static final Logger LOGGER = Logger.getLogger(QrCodeAction.class.getName());
+
+    public QrCodeAction() {
+        LOGGER.info("QrCodeAction initialized");
+    }
+
+    @Override
+    public String getIconFileName() {
+        return null;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return "MFA Google Auth Action";
+    }
+
+    @Override
+    public String getUrlName() {
+        return "mfa-google-auth";
+    }
+
+    // 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());
+    }
+
+    // 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()) {
+            rsp.sendError(400, "Missing secret parameter");
+            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) {
+            rsp.sendError(500, "Failed to generate QR Code");
+        }
+    }
+}

+ 40 - 0
src/main/java/io/jenkins/plugins/TOTPUtil.java

@@ -0,0 +1,40 @@
+/*
+ * Project: MFA Google 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.
+ *
+ * Author: Allan Barcelos
+ * Date: 2025-07-17
+ */
+
+package io.jenkins.plugins;
+
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
+
+public class TOTPUtil {
+
+    public static GoogleAuthenticatorKey generateSecret() {
+        GoogleAuthenticator gAuth = new GoogleAuthenticator();
+        return gAuth.createCredentials();
+    }
+
+    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 boolean verifyCode(String secret, String code) {
+        try {
+            int codeInt = Integer.parseInt(code);
+            GoogleAuthenticator gAuth = new GoogleAuthenticator();
+            return gAuth.authorize(secret, codeInt);
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+}

+ 4 - 0
src/main/resources/index.jelly

@@ -0,0 +1,4 @@
+<?jelly escape-by-default='true'?>
+<div>
+    TODO
+</div>

+ 6 - 0
src/main/resources/io/jenkins/plugins/Messages.properties

@@ -0,0 +1,6 @@
+MfaUserProperty.secretKey.missing=Secret Key not provided
+MfaUserProperty.totpCode.missing=Please enter the code from your app
+MfaUserProperty.totpCode.invalid=Invalid code. Check your authenticator app.
+MfaUserProperty.totpCode.warning=Secret has not been generated yet.
+MfaUserProperty.totpCode.valid=Code is valid
+MfaUserProperty.displayName=MFA (Google Authenticator)

+ 6 - 0
src/main/resources/io/jenkins/plugins/Messages_pt_BR.properties

@@ -0,0 +1,6 @@
+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.
+MfaUserProperty.totpCode.valid=Código válido
+MfaUserProperty.displayName=MFA (Google Authenticator)

+ 69 - 0
src/main/resources/io/jenkins/plugins/MfaUserProperty/config.jelly

@@ -0,0 +1,69 @@
+<!-- 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">
+
+  <f:entry title="Ativar MFA" 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>
+
+      <f:entry title="Código do App">
+        <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();
+      }
+    })();
+  </script>
+
+</j:jelly>

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

@@ -0,0 +1,21 @@
+<?jelly escape-by-default='true'?>
+<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>
+      <j:if test="${request.getParameter('error') != null}">
+        <div style="color:red; font-weight:bold;">
+          Código inválido. Tente novamente.
+        </div>
+      </j:if>
+      <form method="post" action="${rootURL}/mfa-verify/verify">
+        <f:entry title="Código do app">
+          <f:textbox name="totpCode" />
+        </f:entry>
+        <f:entry>
+          <f:submit value="Verificar" />
+        </f:entry>
+      </form>
+    </l:main-panel>
+  </l:layout>
+</j:jelly>