or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

aot-native-support.mdauthentication-core.mdauthentication-events.mdauthentication-management.mdauthentication-tokens.mdauthorities.mdauthorization.mdcompromised-password.mdconcurrent-async.mddao-authentication.mdexpression-access-control.mdindex.mdjaas-authentication.mdjackson-serialization.mdmethod-security.mdobservation-metrics.mdone-time-tokens.mdprovisioning.mdsecurity-context.mdsession-management.mduser-details.md
tile.json

provisioning.mddocs/

User Provisioning and Management

User provisioning provides comprehensive CRUD (Create, Read, Update, Delete) operations for managing user accounts and groups. This extends the basic UserDetailsService with full user lifecycle management capabilities.

Key Information for Agents

Core Capabilities:

  • UserDetailsManager extends UserDetailsService with CRUD operations
  • User creation, update, deletion, and password change
  • Group management via GroupManager interface (groups with authorities)
  • In-memory implementation: InMemoryUserDetailsManager (for testing/development)
  • JDBC implementation: JdbcUserDetailsManager (persistent, with group support)
  • Group operations: Create, delete, rename groups; add/remove users; manage group authorities

Key Interfaces and Classes:

  • UserDetailsManager - Interface: createUser(), updateUser(), deleteUser(), changePassword(), userExists()
  • GroupManager - Interface: createGroup(), deleteGroup(), addUserToGroup(), findGroupAuthorities(), etc.
  • InMemoryUserDetailsManager - In-memory implementation (implements both interfaces)
  • JdbcUserDetailsManager - JDBC implementation (implements both interfaces, extends JdbcDaoImpl)

Default Behaviors:

  • createUser() throws exception if user already exists
  • updateUser() replaces entire user (must provide all fields)
  • deleteUser() removes user and all associated authorities/groups
  • changePassword() validates old password before changing (requires AuthenticationManager)
  • userExists() returns boolean (no exception if not found)
  • Group authorities: Users inherit authorities from groups they belong to

Threading Model:

  • Synchronous operations execute in calling thread
  • Thread-safe: JDBC implementation is thread-safe (database transactions)

Lifecycle:

  • Users persisted to database (JDBC) or memory (InMemory)
  • Groups persisted separately with group-authority and group-member relationships
  • Password changes validated against current password

Exceptions:

  • User already exists: Thrown by createUser() if userExists() returns true
  • User not found: Thrown by updateUser(), deleteUser() if user doesn't exist
  • Invalid password: Thrown by changePassword() if old password incorrect

Edge Cases:

  • User existence: Check via userExists() before createUser()
  • Password encoding: Passwords should be encoded before calling createUser() / updateUser()
  • Group membership: Users can belong to multiple groups
  • Group authorities: Groups have authorities that apply to all members
  • Custom SQL: JdbcUserDetailsManager allows custom SQL via setter methods
  • Transaction management: JDBC operations should be transactional

UserDetailsManager

Extended interface providing full user management capabilities beyond just loading user details.

public interface UserDetailsManager extends UserDetailsService

{ .api }

Key Methods:

void createUser(UserDetails user)

{ .api }

Creates a new user account with the provided details.

void updateUser(UserDetails user)

{ .api }

Updates an existing user's information.

void deleteUser(String username)

{ .api }

Deletes a user account by username.

void changePassword(String oldPassword, String newPassword)

{ .api }

Changes the password for the currently authenticated user.

boolean userExists(String username)

{ .api }

Checks if a user with the given username exists.

Usage Example:

@Service
public class UserProvisioningService {

    private final UserDetailsManager userDetailsManager;

    public void registerNewUser(RegistrationRequest request) {
        if (userDetailsManager.userExists(request.getUsername())) {
            throw new UserAlreadyExistsException(request.getUsername());
        }

        UserDetails user = User.builder()
            .username(request.getUsername())
            .password(passwordEncoder.encode(request.getPassword()))
            .authorities("ROLE_USER")
            .build();

        userDetailsManager.createUser(user);
    }

    public void updateUserProfile(String username, ProfileUpdate update) {
        UserDetails existing = userDetailsManager.loadUserByUsername(username);

        UserDetails updated = User.builder()
            .username(existing.getUsername())
            .password(existing.getPassword())
            .authorities(existing.getAuthorities())
            .accountExpired(!update.isAccountValid())
            .accountLocked(update.isLocked())
            .credentialsExpired(!update.areCredentialsValid())
            .disabled(!update.isEnabled())
            .build();

        userDetailsManager.updateUser(updated);
    }

    public void removeUser(String username) {
        if (!userDetailsManager.userExists(username)) {
            throw new UsernameNotFoundException(username);
        }
        userDetailsManager.deleteUser(username);
    }
}

GroupManager

Interface for managing user groups and group-based authorities.

public interface GroupManager

{ .api }

Key Methods:

List<String> findAllGroups()

{ .api }

Returns a list of all group names.

List<String> findUsersInGroup(String groupName)

{ .api }

Returns usernames of all users in the specified group.

void createGroup(String groupName, List<GrantedAuthority> authorities)

{ .api }

Creates a new group with the specified authorities.

void deleteGroup(String groupName)

{ .api }

Deletes a group.

void renameGroup(String oldName, String newName)

{ .api }

Renames an existing group.

void addUserToGroup(String username, String groupName)

{ .api }

Adds a user to a group.

void removeUserFromGroup(String username, String groupName)

{ .api }

Removes a user from a group.

List<GrantedAuthority> findGroupAuthorities(String groupName)

{ .api }

Returns the authorities assigned to a group.

void addGroupAuthority(String groupName, GrantedAuthority authority)

{ .api }

Adds an authority to a group.

void removeGroupAuthority(String groupName, GrantedAuthority authority)

{ .api }

Removes an authority from a group.

Usage Example:

@Service
public class GroupManagementService {

    private final GroupManager groupManager;

    public void createDepartmentGroup(String department, Set<String> permissions) {
        List<GrantedAuthority> authorities = permissions.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        groupManager.createGroup(department, authorities);
    }

    public void assignUserToDepartment(String username, String department) {
        if (!groupManager.findAllGroups().contains(department)) {
            throw new IllegalArgumentException("Group does not exist: " + department);
        }
        groupManager.addUserToGroup(username, department);
    }

    public void grantGroupPermission(String groupName, String permission) {
        GrantedAuthority authority = new SimpleGrantedAuthority(permission);
        groupManager.addGroupAuthority(groupName, authority);
    }

    public List<String> getDepartmentMembers(String department) {
        return groupManager.findUsersInGroup(department);
    }
}

In-Memory Implementation

InMemoryUserDetailsManager

In-memory implementation of UserDetailsManager, useful for development and testing.

public class InMemoryUserDetailsManager
    implements UserDetailsManager, UserDetailsPasswordService

{ .api }

Constructors:

public InMemoryUserDetailsManager()

{ .api }

Creates an empty manager.

public InMemoryUserDetailsManager(UserDetails... users)

{ .api }

Creates a manager with initial users.

public InMemoryUserDetailsManager(Collection<UserDetails> users)

{ .api }

Creates a manager with a collection of users.

Configuration Examples:

@Configuration
public class InMemoryUserConfig {

    @Bean
    public InMemoryUserDetailsManager userDetailsManager() {
        UserDetails admin = User.builder()
            .username("admin")
            .password("{bcrypt}$2a$10$...")
            .roles("ADMIN", "USER")
            .build();

        UserDetails user = User.builder()
            .username("user")
            .password("{bcrypt}$2a$10$...")
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(admin, user);
    }
}

Dynamic User Management:

@Service
public class DynamicUserService {

    private final InMemoryUserDetailsManager userDetailsManager;

    public void registerUser(String username, String password) {
        UserDetails user = User.builder()
            .username(username)
            .password("{noop}" + password) // For demo only - use proper encoding
            .roles("USER")
            .build();

        userDetailsManager.createUser(user);
    }

    public void promoteToAdmin(String username) {
        UserDetails user = userDetailsManager.loadUserByUsername(username);

        Collection<GrantedAuthority> updatedAuthorities =
            new ArrayList<>(user.getAuthorities());
        updatedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

        UserDetails updated = User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(updatedAuthorities)
            .build();

        userDetailsManager.updateUser(updated);
    }

    public boolean isUsernameTaken(String username) {
        return userDetailsManager.userExists(username);
    }
}

Testing Setup:

@TestConfiguration
public class TestSecurityConfig {

    @Bean
    public InMemoryUserDetailsManager testUsers() {
        UserDetails testAdmin = User.builder()
            .username("testadmin")
            .password("{noop}admin123")
            .roles("ADMIN")
            .build();

        UserDetails testUser = User.builder()
            .username("testuser")
            .password("{noop}user123")
            .roles("USER")
            .build();

        UserDetails testGuest = User.builder()
            .username("testguest")
            .password("{noop}guest123")
            .roles("GUEST")
            .disabled(false)
            .accountExpired(false)
            .accountLocked(false)
            .credentialsExpired(false)
            .build();

        return new InMemoryUserDetailsManager(testAdmin, testUser, testGuest);
    }
}

JDBC Implementation

JdbcUserDetailsManager

JDBC-based implementation supporting both UserDetailsManager and GroupManager.

public class JdbcUserDetailsManager
    extends JdbcDaoImpl
    implements UserDetailsManager, GroupManager

{ .api }

Default SQL Schema:

-- Users table
CREATE TABLE users (
    username VARCHAR(50) NOT NULL PRIMARY KEY,
    password VARCHAR(500) NOT NULL,
    enabled BOOLEAN NOT NULL
);

-- Authorities table
CREATE TABLE authorities (
    username VARCHAR(50) NOT NULL,
    authority VARCHAR(50) NOT NULL,
    CONSTRAINT fk_authorities_users FOREIGN KEY(username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);

-- Groups table
CREATE TABLE groups (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    group_name VARCHAR(50) NOT NULL
);

-- Group authorities table
CREATE TABLE group_authorities (
    group_id BIGINT NOT NULL,
    authority VARCHAR(50) NOT NULL,
    CONSTRAINT fk_group_authorities_group FOREIGN KEY(group_id) REFERENCES groups(id)
);

-- Group members table
CREATE TABLE group_members (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    group_id BIGINT NOT NULL,
    CONSTRAINT fk_group_members_group FOREIGN KEY(group_id) REFERENCES groups(id)
);

Default SQL Queries:

// User Operations
CREATE_USER_SQL = "INSERT INTO users (username, password, enabled) VALUES (?, ?, ?)";
UPDATE_USER_SQL = "UPDATE users SET password = ?, enabled = ? WHERE username = ?";
DELETE_USER_SQL = "DELETE FROM users WHERE username = ?";
USER_EXISTS_SQL = "SELECT username FROM users WHERE username = ?";
CHANGE_PASSWORD_SQL = "UPDATE users SET password = ? WHERE username = ?";

// Authority Operations
INSERT_AUTHORITY_SQL = "INSERT INTO authorities (username, authority) VALUES (?, ?)";
DELETE_USER_AUTHORITIES_SQL = "DELETE FROM authorities WHERE username = ?";

// Group Operations
INSERT_GROUP_SQL = "INSERT INTO groups (group_name) VALUES (?)";
FIND_GROUPS_SQL = "SELECT group_name FROM groups";
FIND_USERS_IN_GROUP_SQL = "SELECT username FROM group_members gm, groups g " +
    "WHERE gm.group_id = g.id AND g.group_name = ?";

{ .api }

Configuration:

@Configuration
public class JdbcUserManagementConfig {

    @Bean
    public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource) {
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);

        // Optional: Customize SQL queries
        manager.setCreateUserSql(
            "INSERT INTO users (username, password, enabled) VALUES (?, ?, ?)");
        manager.setUpdateUserSql(
            "UPDATE users SET password = ?, enabled = ? WHERE username = ?");
        manager.setDeleteUserSql(
            "DELETE FROM users WHERE username = ?");
        manager.setUserExistsSql(
            "SELECT username FROM users WHERE username = ?");
        manager.setChangePasswordSql(
            "UPDATE users SET password = ? WHERE username = ?");

        return manager;
    }

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:data.sql")
            .build();
    }
}

Key Methods:

void setCreateUserSql(String createUserSql)

{ .api }

Customizes the SQL for creating users.

void setUpdateUserSql(String updateUserSql)

{ .api }

Customizes the SQL for updating users.

void setDeleteUserSql(String deleteSql)

{ .api }

Customizes the SQL for deleting users.

void setUserExistsSql(String userExistsSql)

{ .api }

Customizes the SQL for checking user existence.

void setChangePasswordSql(String changePasswordSql)

{ .api }

Customizes the SQL for changing passwords.

Complete Service Example:

@Service
@Transactional
public class JdbcUserProvisioningService {

    private final JdbcUserDetailsManager userManager;
    private final PasswordEncoder passwordEncoder;

    public void createUserAccount(UserRegistration registration) {
        if (userManager.userExists(registration.getUsername())) {
            throw new DuplicateUsernameException(registration.getUsername());
        }

        UserDetails user = User.builder()
            .username(registration.getUsername())
            .password(passwordEncoder.encode(registration.getPassword()))
            .authorities(registration.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .toList())
            .accountExpired(false)
            .accountLocked(false)
            .credentialsExpired(false)
            .disabled(false)
            .build();

        userManager.createUser(user);
    }

    public void setupUserGroups(String username, List<String> groupNames) {
        for (String groupName : groupNames) {
            userManager.addUserToGroup(username, groupName);
        }
    }

    public void createOrganizationalGroup(String groupName, List<String> permissions) {
        List<GrantedAuthority> authorities = permissions.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        userManager.createGroup(groupName, authorities);
    }

    public void disableUser(String username) {
        UserDetails user = userManager.loadUserByUsername(username);

        UserDetails disabled = User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(user.getAuthorities())
            .disabled(true)
            .build();

        userManager.updateUser(disabled);
    }

    public void changeUserPassword(String username, String newPassword) {
        UserDetails user = userManager.loadUserByUsername(username);

        UserDetails updated = User.builder()
            .username(user.getUsername())
            .password(passwordEncoder.encode(newPassword))
            .authorities(user.getAuthorities())
            .build();

        userManager.updateUser(updated);
    }

    public List<String> getUserGroups(String username) {
        return userManager.findAllGroups().stream()
            .filter(group -> userManager.findUsersInGroup(group).contains(username))
            .collect(Collectors.toList());
    }
}

Group Management Example

@Service
public class GroupAdministrationService {

    private final GroupManager groupManager;

    public void createDepartment(String departmentName, Set<String> basePermissions) {
        List<GrantedAuthority> authorities = basePermissions.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        groupManager.createGroup(departmentName, authorities);
    }

    public void assignUserToDepartment(String username, String department) {
        groupManager.addUserToGroup(username, department);
    }

    public void transferUser(String username, String fromDept, String toDept) {
        groupManager.removeUserFromGroup(username, fromDept);
        groupManager.addUserToGroup(username, toDept);
    }

    public void grantDepartmentPermission(String department, String permission) {
        GrantedAuthority authority = new SimpleGrantedAuthority(permission);
        groupManager.addGroupAuthority(department, authority);
    }

    public void revokeDepartmentPermission(String department, String permission) {
        GrantedAuthority authority = new SimpleGrantedAuthority(permission);
        groupManager.removeGroupAuthority(department, authority);
    }

    public void renameDepartment(String oldName, String newName) {
        groupManager.renameGroup(oldName, newName);
    }

    public void deleteDepartment(String department) {
        // First, remove all users from the group
        List<String> members = groupManager.findUsersInGroup(department);
        members.forEach(username ->
            groupManager.removeUserFromGroup(username, department));

        // Then delete the group
        groupManager.deleteGroup(department);
    }

    public Map<String, List<String>> getDepartmentStructure() {
        Map<String, List<String>> structure = new HashMap<>();

        for (String group : groupManager.findAllGroups()) {
            List<String> members = groupManager.findUsersInGroup(group);
            structure.put(group, members);
        }

        return structure;
    }

    public Set<String> getUserPermissions(String username) {
        Set<String> permissions = new HashSet<>();

        for (String group : groupManager.findAllGroups()) {
            if (groupManager.findUsersInGroup(group).contains(username)) {
                List<GrantedAuthority> authorities =
                    groupManager.findGroupAuthorities(group);
                authorities.stream()
                    .map(GrantedAuthority::getAuthority)
                    .forEach(permissions::add);
            }
        }

        return permissions;
    }
}

Complete Application Example

@RestController
@RequestMapping("/api/admin/users")
public class UserAdministrationController {

    private final UserDetailsManager userManager;
    private final GroupManager groupManager;
    private final PasswordEncoder passwordEncoder;

    @PostMapping
    public ResponseEntity<Void> createUser(@RequestBody CreateUserRequest request) {
        UserDetails user = User.builder()
            .username(request.getUsername())
            .password(passwordEncoder.encode(request.getPassword()))
            .authorities(request.getRoles())
            .build();

        userManager.createUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @PutMapping("/{username}")
    public ResponseEntity<Void> updateUser(
        @PathVariable String username,
        @RequestBody UpdateUserRequest request
    ) {
        UserDetails existing = userManager.loadUserByUsername(username);

        UserDetails updated = User.builder()
            .username(username)
            .password(existing.getPassword())
            .authorities(request.getRoles())
            .disabled(!request.isEnabled())
            .accountLocked(request.isLocked())
            .build();

        userManager.updateUser(updated);
        return ResponseEntity.ok().build();
    }

    @DeleteMapping("/{username}")
    public ResponseEntity<Void> deleteUser(@PathVariable String username) {
        userManager.deleteUser(username);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/{username}/exists")
    public ResponseEntity<Boolean> userExists(@PathVariable String username) {
        return ResponseEntity.ok(userManager.userExists(username));
    }

    @PostMapping("/groups")
    public ResponseEntity<Void> createGroup(@RequestBody CreateGroupRequest request) {
        List<GrantedAuthority> authorities = request.getPermissions().stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        groupManager.createGroup(request.getName(), authorities);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @PostMapping("/groups/{groupName}/members/{username}")
    public ResponseEntity<Void> addUserToGroup(
        @PathVariable String groupName,
        @PathVariable String username
    ) {
        groupManager.addUserToGroup(username, groupName);
        return ResponseEntity.ok().build();
    }

    @GetMapping("/groups/{groupName}/members")
    public ResponseEntity<List<String>> getGroupMembers(@PathVariable String groupName) {
        return ResponseEntity.ok(groupManager.findUsersInGroup(groupName));
    }
}

Package

org.springframework.security.provisioning