Jenkins User Management Architecture: How `User`, `UserProperty`, and `HudsonPrivateSecurityRealm` Work

Jenkins stores users as hudson.model.User objects, augments each account with a list of UserProperty extensions, and delegates authentication to a SecurityRealm such as HudsonPrivateSecurityRealm.

This article breaks down the Jenkins user management architecture by examining the core classes in the jenkinsci/jenkins repository. You will learn how the user lifecycle is modeled, how plugins attach data to accounts, and how the built-in security realm authenticates locally stored credentials.

The Three Pillars of Jenkins User Management

Jenkins separates identity storage from authentication logic. The design revolves around three key types:

  • User — The central model object representing an account.
  • UserProperty — An extension point that lets plugins store per-user data.
  • HudsonPrivateSecurityRealm — The built-in SecurityRealm that authenticates against User records managed by Jenkins itself.

This separation allows plugins to enrich user profiles without modifying core security code, while the realm only needs a single UserProperty — Details — to validate passwords.

User Lifecycle: Lookup, Creation, and Persistence

The User class in core/src/main/java/hudson/model/User.java manages the entire lifecycle of a Jenkins account.

Loading and Creating a User

To obtain a User instance, code calls User.getById(String id, boolean create). If the user exists in memory or on disk, the method returns the existing object. When create is true and no record is found, Jenkins instantiates a new User, loads or creates a config.xml file, and invokes fixUpAfterLoad() to initialize properties.

Allocating Default Properties

After construction, allocateDefaultPropertyInstancesAsNeeded() iterates over every registered UserPropertyDescriptor via UserPropertyDescriptor.all(). For any descriptor that does not already have a matching instance attached, Jenkins creates a default property and adds it to the user. This mechanism ensures that every account carries the full set of available user properties.

Saving and Deleting

Changes to a user are persisted through User.save(), which serializes the User and its properties to $JENKINS_HOME/users/<hashed-folder>/config.xml. Deletion is handled by User.delete(), which removes the on-disk folder and invalidates the in-memory cache.

// Groovy script example: retrieve or create a user
import hudson.model.User

User alice = User.getById("alice", true)
alice.save()

The UserProperty Extension Mechanism

UserProperty is the core extension point for per-user metadata. The base class lives in core/src/main/java/hudson/model/UserProperty.java, and every concrete implementation pairs with a UserPropertyDescriptor.

Descriptor-Based Discovery

Jenkins discovers properties at startup through the Extension annotation. The static method UserProperty.all() returns every registered descriptor. Each descriptor can create a new property instance via newInstance(User).

Storage and Access

Properties are serialized as child elements inside the <user> node of config.xml. At runtime, you can retrieve a specific property with User.getProperty(Class<T>) or inspect the full list via User.getAllProperties(), which is exposed in the remote API under an admin-only permission check.

// Java plugin example: a custom UserProperty
@Extension
public static final class MyProperty extends UserProperty {
    private final String favouriteColour;

    @DataBoundConstructor
    public MyProperty(String favouriteColour) {
        this.favouriteColour = favouriteColour;
    }

    public String getFavouriteColour() {
        return favouriteColour;
    }

    @Extension
    public static class DescriptorImpl extends UserPropertyDescriptor {
        @Override
        public String getDisplayName() {
            return "My custom property";
        }

        @Override
        public MyProperty newInstance(User user) {
            return new MyProperty("blue");
        }
    }
}

When a new User is created, allocateDefaultPropertyInstancesAsNeeded() automatically invokes DescriptorImpl.newInstance(user) and attaches the result.

HudsonPrivateSecurityRealm: Built-In Authentication

When Jenkins manages its own user database, the active SecurityRealm is an instance of HudsonPrivateSecurityRealm in core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java.

The authenticate2 Method

The entry point for login is authenticate2(String username, String password). The realm loads the user's Details property — an inner class that implements UserProperty and wraps a Spring Security UserDetails view — and validates the supplied password:

// In HudsonPrivateSecurityRealm.authenticate2()
Details u = load(username);
if (!u.isPasswordCorrect(password)) {
    throw new BadCredentialsException("Bad credentials");
}
return u.asUserDetails();

If the user does not exist, the code performs a dummy password check to mitigate timing attacks before throwing an exception.

The Details Property and Password Hashing

Details stores the password hash in a field named passwordHash. The realm uses MultiPasswordEncoder to choose between BCrypt (default, prefixed with #jbcrypt:) and PBKDF2 (FIPS-140 mode, prefixed with $PBKDF2). The header in the stored hash lets the encoder recognize the algorithm during validation.

Account Creation

Administrators and the setup wizard invoke createAccount(String username, String password) to register new users. This method creates or reuses a User object and attaches a Details property with the hashed password automatically.

import hudson.security.HudsonPrivateSecurityRealm
import hudson.model.User

def realm = Jenkins.instance.getSecurityRealm()
if (realm instanceof HudsonPrivateSecurityRealm) {
    User bob = realm.createAccount("bob", "securePassword123")
    println "Created user ${bob.id}"
}

UI Integration

The realm conditionally exposes a Manage Users link through an inner class ManageUserLinks. The link is rendered only when HudsonPrivateSecurityRealm is the active security realm, giving administrators a built-in interface to list, edit, or delete accounts.

High-Level Authentication Flow

When a user submits a login request, the following sequence occurs inside the Jenkins user management architecture:

  1. The request reaches SecurityRealm.authenticate2() implemented by HudsonPrivateSecurityRealm.
  2. The realm resolves the User object and fetches its Details property using User.getProperty(Details.class).
  3. Details.isPasswordCorrect() validates the password hash and returns a Spring UserDetails instance.
  4. Spring Security creates an Authentication object, which Jenkins binds to the HTTP session.
  5. Later, permission checks delegate to User.getACL(), which is driven by the configured AuthorizationStrategy.

Summary

Frequently Asked Questions

How does Jenkins store user passwords when using the built-in security realm?

Passwords are stored as hashes inside the Details property attached to each User. The HudsonPrivateSecurityRealm uses MultiPasswordEncoder to generate BCrypt or PBKDF2 hashes, and the hash prefix determines which algorithm is used during validation.

Can plugins add custom fields to a Jenkins user profile?

Yes. Plugins implement the UserProperty extension point and provide a UserPropertyDescriptor. Jenkins automatically instantiates and attaches the property to new users through allocateDefaultPropertyInstancesAsNeeded(). The data is persisted in the user's config.xml.

What happens if HudsonPrivateSecurityRealm is not the active security realm?

When an external realm such as LDAP or SAML is configured, HudsonPrivateSecurityRealm is not installed. User objects may still be created for identity reference, but authentication is delegated to the external provider, and the Manage Users link is hidden.

Is it possible to create a Jenkins user programmatically from a script or plugin?

Yes. If HudsonPrivateSecurityRealm is the active realm, you can call realm.createAccount(username, password). This method creates the User object and attaches the Details property with a hashed password in a single step.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →