or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authentication.mdindex.mdjaas.mdlogin-services.mdsecurity-framework.mduser-identity.md
tile.json

login-services.mddocs/

Login Services

Login Services provide the foundation for user authentication in Jetty Security, managing user credentials and creating UserIdentity objects from successful authentications.

Core Login Service Interface

LoginService

The base interface for all login service implementations:

public interface LoginService {
    
    // Service identification
    String getName();
    
    // Authentication methods
    UserIdentity login(String username, Object credentials, Request request, 
                      Function<Boolean, Session> getOrCreateSession);
    
    UserIdentity getUserIdentity(Subject subject, Principal userPrincipal, boolean create);
    
    // User validation and lifecycle
    boolean validate(UserIdentity user);
    void logout(UserIdentity user);
    
    // Identity service integration
    IdentityService getIdentityService();
    void setIdentityService(IdentityService service);
}

AbstractLoginService

Base Implementation

Provides common functionality for login service implementations:

public abstract class AbstractLoginService implements LoginService {
    
    // Configuration
    public String getName();
    public void setName(String name);
    
    public IdentityService getIdentityService();
    public void setIdentityService(IdentityService identityService);
    
    // Validation mode
    public boolean isFullValidate();
    public void setFullValidate(boolean fullValidate);
    
    // Abstract methods for subclasses
    protected abstract List<RolePrincipal> loadRoleInfo(UserPrincipal user);
    protected abstract UserPrincipal loadUserInfo(String username);
    
    // Implemented login logic
    @Override
    public UserIdentity login(String username, Object credentials, Request request, 
                             Function<Boolean, Session> getOrCreateSession) {
        
        UserPrincipal userPrincipal = loadUserInfo(username);
        if (userPrincipal != null && userPrincipal.authenticate(credentials)) {
            List<RolePrincipal> roles = loadRoleInfo(userPrincipal);
            Subject subject = new Subject();
            userPrincipal.configureSubject(subject);
            
            String[] roleArray = roles.stream()
                .map(RolePrincipal::getName)
                .toArray(String[]::new);
            
            return getIdentityService().newUserIdentity(subject, userPrincipal, roleArray);
        }
        return null;
    }
}

HashLoginService

Property File-Based Authentication

Manages users and roles from properties files with hot-reload capability:

public class HashLoginService extends AbstractLoginService {
    
    // Configuration file management
    public Resource getConfig();
    public void setConfig(Resource config);
    
    // Auto-reload configuration
    public int getReloadInterval();
    public void setReloadInterval(int reloadIntervalSeconds);
    
    // User store integration
    public void setUserStore(UserStore userStore);
    
    // Constructors
    public HashLoginService();
    public HashLoginService(String name);
    public HashLoginService(String name, Resource config);
    
    // Legacy reload configuration (deprecated)
    @Deprecated
    public boolean isHotReload();
    @Deprecated
    public void setHotReload(boolean enable);
    
    @Override
    protected UserPrincipal loadUserInfo(String username);
    
    @Override
    protected List<RolePrincipal> loadRoleInfo(UserPrincipal user);
}

HashLoginService Setup

public class HashLoginServiceExample {
    
    public void setupBasicHashLogin() {
        HashLoginService loginService = new HashLoginService();
        loginService.setName("MyRealm");
        
        // Load from properties file
        Resource config = Resource.newResource("users.properties");
        loginService.setConfig(config);
        
        // Enable auto-reload every 30 seconds
        loginService.setReloadInterval(30);
        
        // Use with security handler
        SecurityHandler security = new SecurityHandler.PathMapped();
        security.setLoginService(loginService);
    }
    
    public void setupProgrammaticUsers() {
        HashLoginService loginService = new HashLoginService("ProgrammaticRealm");
        
        // Create custom user store
        PropertyUserStore userStore = new PropertyUserStore();
        
        // Add users programmatically
        userStore.addUser("admin", Credential.getCredential("admin123"), 
                         new String[]{"admin", "user"});
        userStore.addUser("john", Credential.getCredential("password"), 
                         new String[]{"user"});
        userStore.addUser("guest", Credential.getCredential("guest"), 
                         new String[]{"guest"});
        
        loginService.setUserStore(userStore);
    }
}

Properties File Format

# users.properties format:
# username: password,role1,role2,...

admin: password123,admin,user
john: mypassword,user,developer  
jane: secret,user,manager
guest: guest,guest
api-user: apikey456,api-user

# Support for different credential formats:
# Plain text (not recommended for production)
user1: plaintext,role1

# MD5 hashed passwords
user2: MD5:5d41402abc4b2a76b9719d911017c592,role2  

# Crypt passwords
user3: CRYPT:$1$salt$hash,role3

# OBF obfuscated passwords  
user4: OBF:1v2j1uum1xtv1zej1zer1xtn1uvk1v1v,role4

JDBCLoginService

Database-Backed Authentication

Provides authentication against SQL databases:

public class JDBCLoginService extends AbstractLoginService {
    
    // Database configuration
    public void setConfig(String configFile);
    public void setConfig(Resource config);
    
    // DataSource configuration
    public void setDataSourceName(String dataSourceName);
    public DataSource getDataSource();
    public void setDataSource(DataSource dataSource);
    
    // Direct connection configuration
    public void setDriverClassName(String driverClassName);
    public void setUrl(String url);
    public void setUserName(String userName);
    public void setPassword(String password);
    
    // Table and column configuration
    public void setUserTableName(String tableName);
    public void setUserTableKey(String keyColumn);
    public void setUserTablePasswordField(String passwordColumn);
    
    public void setRoleTableName(String tableName);
    public void setRoleTableKey(String keyColumn);
    public void setRoleTableRoleField(String roleColumn);
    
    // Custom SQL queries
    public void setUserQuery(String query);
    public void setRoleQuery(String query);
    
    // Connection management
    public void setConnectionTimeout(int timeout);
    
    @Override
    protected UserPrincipal loadUserInfo(String username);
    
    @Override
    protected List<RolePrincipal> loadRoleInfo(UserPrincipal user);
}

JDBC Setup Examples

public class JDBCLoginServiceExample {
    
    public void setupBasicJDBCLogin() {
        JDBCLoginService loginService = new JDBCLoginService();
        loginService.setName("DatabaseRealm");
        
        // Database connection
        loginService.setDriverClassName("com.mysql.cj.jdbc.Driver");
        loginService.setUrl("jdbc:mysql://localhost:3306/myapp");
        loginService.setUserName("app_user");
        loginService.setPassword("app_password");
        
        // Table configuration (using defaults)
        loginService.setUserTableName("users");
        loginService.setUserTableKey("username");
        loginService.setUserTablePasswordField("password");
        
        loginService.setRoleTableName("user_roles");
        loginService.setRoleTableKey("username");
        loginService.setRoleTableRoleField("role");
        
        // Connection timeout
        loginService.setConnectionTimeout(30);
    }
    
    public void setupDataSourceJDBC() {
        JDBCLoginService loginService = new JDBCLoginService();
        loginService.setName("DataSourceRealm");
        
        // Use JNDI DataSource
        loginService.setDataSourceName("java:comp/env/jdbc/UserDB");
        
        // Custom table structure
        loginService.setUserTableName("app_users");
        loginService.setUserTableKey("user_id");
        loginService.setUserTablePasswordField("password_hash");
        
        loginService.setRoleTableName("app_user_roles");
        loginService.setRoleTableKey("user_id");
        loginService.setRoleTableRoleField("role_name");
    }
    
    public void setupCustomQueries() {
        JDBCLoginService loginService = new JDBCLoginService();
        loginService.setName("CustomQueryRealm");
        
        // Custom SQL for user lookup
        loginService.setUserQuery(
            "SELECT username, password FROM users WHERE username = ? AND active = 1"
        );
        
        // Custom SQL for role lookup with JOIN
        loginService.setRoleQuery(
            "SELECT r.role_name FROM users u " +
            "JOIN user_roles ur ON u.id = ur.user_id " +
            "JOIN roles r ON ur.role_id = r.id " +
            "WHERE u.username = ?"
        );
    }
}

Database Schema

-- Example database schema for JDBC login service

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(100),
    active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE roles (
    id INT PRIMARY KEY AUTO_INCREMENT,
    role_name VARCHAR(50) UNIQUE NOT NULL,
    description VARCHAR(255)
);

CREATE TABLE user_roles (
    user_id INT,
    role_id INT,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (role_id) REFERENCES roles(id)
);

-- Alternative simple schema (flat table)
CREATE TABLE user_roles_flat (
    username VARCHAR(50),
    role VARCHAR(50),
    PRIMARY KEY (username, role)
);

-- Insert sample data
INSERT INTO users (username, password) VALUES 
('admin', 'MD5:21232f297a57a5a743894a0e4a801fc3'),  -- MD5 of 'admin'
('john', 'password123'),
('jane', 'CRYPT:$1$salt$hash');

INSERT INTO roles (role_name) VALUES ('admin'), ('user'), ('manager');

INSERT INTO user_roles (user_id, role_id) VALUES 
(1, 1), (1, 2),  -- admin has admin and user roles
(2, 2),          -- john has user role
(3, 2), (3, 3);  -- jane has user and manager roles

EmptyLoginService

No-Authentication Service

Login service that accepts no users (always returns null):

public class EmptyLoginService implements LoginService {
    
    public EmptyLoginService();
    public EmptyLoginService(String name);
    
    @Override
    public String getName();
    
    @Override
    public UserIdentity login(String username, Object credentials, Request request, 
                             Function<Boolean, Session> getOrCreateSession) {
        return null; // Never authenticates any user
    }
    
    @Override
    public boolean validate(UserIdentity user) {
        return false; // No users are valid
    }
    
    @Override
    public void logout(UserIdentity user) {
        // No-op
    }
    
    // Other methods with minimal implementations
}

EmptyLoginService Usage

public class EmptyLoginServiceExample {
    
    public void setupEmptyLogin() {
        // Useful for development or when authentication is disabled
        EmptyLoginService emptyLogin = new EmptyLoginService("NoAuth");
        
        SecurityHandler security = new SecurityHandler.PathMapped();
        security.setLoginService(emptyLogin);
        
        // All requests will fail authentication
        // Useful with constraints that allow anonymous access
        security.put("/*", Constraint.ALLOWED);
        security.put("/admin/*", Constraint.FORBIDDEN); // Always deny
    }
}

SPNEGOLoginService

Kerberos Authentication Service

Specialized login service for SPNEGO/Kerberos authentication:

public class SPNEGOLoginService extends AbstractLoginService {
    
    public SPNEGOLoginService();
    public SPNEGOLoginService(String name);
    
    @Override
    protected UserPrincipal loadUserInfo(String username);
    
    @Override
    protected List<RolePrincipal> loadRoleInfo(UserPrincipal user);
    
    // SPNEGO-specific configuration is typically done via JAAS config
}

UserStore Interface and Implementations

UserStore Interface

Interface for storing and retrieving user and role information:

public interface UserStore {
    
    // User retrieval
    UserPrincipal getUserPrincipal(String username);
    List<RolePrincipal> getRolePrincipals(String username);
    
    // User management (if supported)
    default void addUser(String username, Credential credential, String[] roles) {
        throw new UnsupportedOperationException();
    }
    
    default void removeUser(String username) {
        throw new UnsupportedOperationException();
    }
    
    default void updateUser(String username, Credential credential, String[] roles) {
        throw new UnsupportedOperationException();
    }
}

PropertyUserStore

Implementation backed by properties files with hot-reload:

public class PropertyUserStore implements UserStore {
    
    // Configuration
    public void setConfig(Resource config);
    public Resource getConfig();
    
    // Hot reload configuration
    public void setReloadInterval(int reloadIntervalSeconds);
    public int getReloadInterval();
    
    public void setHotReload(boolean enable);
    public boolean isHotReload();
    
    // User management
    @Override
    public UserPrincipal getUserPrincipal(String username);
    
    @Override
    public List<RolePrincipal> getRolePrincipals(String username);
    
    // Programmatic user management
    public void addUser(String username, Credential credential, String[] roles);
    public void removeUser(String username);
    public void updateUser(String username, Credential credential, String[] roles);
    
    // Lifecycle
    public void start();
    public void stop();
}

Custom Login Service Implementation

Creating Custom Login Services

public class LDAPLoginService extends AbstractLoginService {
    private final String ldapUrl;
    private final String userBaseDn;
    private final String roleBaseDn;
    private final LdapTemplate ldapTemplate;
    
    public LDAPLoginService(String name, String ldapUrl, String userBaseDn, String roleBaseDn) {
        setName(name);
        this.ldapUrl = ldapUrl;
        this.userBaseDn = userBaseDn;
        this.roleBaseDn = roleBaseDn;
        this.ldapTemplate = createLdapTemplate();
    }
    
    @Override
    protected UserPrincipal loadUserInfo(String username) {
        try {
            // LDAP query to find user
            String userDn = String.format("uid=%s,%s", username, userBaseDn);
            
            // Verify user exists
            if (ldapTemplate.lookup(userDn) != null) {
                // Create user principal with LDAP-based credential
                return new LDAPUserPrincipal(username, userDn);
            }
        } catch (Exception e) {
            logger.warn("Failed to load user info for: " + username, e);
        }
        return null;
    }
    
    @Override
    protected List<RolePrincipal> loadRoleInfo(UserPrincipal user) {
        List<RolePrincipal> roles = new ArrayList<>();
        
        try {
            // Query LDAP for user's roles/groups
            String filter = String.format("(member=%s)", ((LDAPUserPrincipal) user).getDn());
            
            List<String> roleNames = ldapTemplate.search(roleBaseDn, filter, 
                (attributes) -> attributes.get("cn").get().toString());
            
            for (String roleName : roleNames) {
                roles.add(new RolePrincipal(roleName));
            }
            
        } catch (Exception e) {
            logger.warn("Failed to load roles for user: " + user.getName(), e);
        }
        
        return roles;
    }
    
    private LdapTemplate createLdapTemplate() {
        // LDAP connection setup
        // Implementation would depend on LDAP library used
        return null;
    }
    
    // Custom user principal for LDAP
    private static class LDAPUserPrincipal extends UserPrincipal {
        private final String dn;
        
        public LDAPUserPrincipal(String name, String dn) {
            super(name, new LDAPCredential(dn));
            this.dn = dn;
        }
        
        public String getDn() {
            return dn;
        }
    }
    
    // Custom credential for LDAP authentication
    private static class LDAPCredential extends Credential {
        private final String dn;
        
        public LDAPCredential(String dn) {
            this.dn = dn;
        }
        
        @Override
        public boolean check(Object credentials) {
            if (credentials instanceof String) {
                return authenticateWithLDAP(dn, (String) credentials);
            }
            return false;
        }
        
        private boolean authenticateWithLDAP(String dn, String password) {
            // LDAP bind authentication
            // Implementation would perform LDAP bind with provided credentials
            return false;
        }
    }
}

REST API-Based Login Service

public class RestApiLoginService extends AbstractLoginService {
    private final String apiBaseUrl;
    private final HttpClient httpClient;
    
    public RestApiLoginService(String name, String apiBaseUrl) {
        setName(name);
        this.apiBaseUrl = apiBaseUrl;
        this.httpClient = new HttpClient();
    }
    
    @Override
    protected UserPrincipal loadUserInfo(String username) {
        try {
            // Call REST API to get user info
            String url = apiBaseUrl + "/users/" + username;
            ContentResponse response = httpClient.GET(url);
            
            if (response.getStatus() == 200) {
                UserInfo userInfo = parseUserInfo(response.getContentAsString());
                return new UserPrincipal(username, Credential.getCredential(userInfo.getPasswordHash()));
            }
        } catch (Exception e) {
            logger.error("Failed to load user from API: " + username, e);
        }
        return null;
    }
    
    @Override
    protected List<RolePrincipal> loadRoleInfo(UserPrincipal user) {
        try {
            // Call REST API to get user roles
            String url = apiBaseUrl + "/users/" + user.getName() + "/roles";
            ContentResponse response = httpClient.GET(url);
            
            if (response.getStatus() == 200) {
                String[] roles = parseRoles(response.getContentAsString());
                return Arrays.stream(roles)
                    .map(RolePrincipal::new)
                    .collect(Collectors.toList());
            }
        } catch (Exception e) {
            logger.error("Failed to load roles from API: " + user.getName(), e);
        }
        return Collections.emptyList();
    }
    
    private UserInfo parseUserInfo(String json) {
        // JSON parsing implementation
        return null;
    }
    
    private String[] parseRoles(String json) {
        // JSON parsing implementation
        return new String[0];
    }
    
    private static class UserInfo {
        public String getPasswordHash() { return null; }
    }
}

Performance and Caching

Caching Login Service

public class CachingLoginServiceWrapper implements LoginService {
    private final LoginService delegate;
    private final Cache<String, UserIdentity> userCache;
    private final Cache<String, List<RolePrincipal>> roleCache;
    
    public CachingLoginServiceWrapper(LoginService delegate, Duration cacheTTL) {
        this.delegate = delegate;
        this.userCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(cacheTTL)
            .build();
        this.roleCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(cacheTTL)
            .build();
    }
    
    @Override
    public String getName() {
        return delegate.getName();
    }
    
    @Override
    public UserIdentity login(String username, Object credentials, Request request, 
                             Function<Boolean, Session> getOrCreateSession) {
        
        // Check cache first for successful authentications
        String cacheKey = buildCacheKey(username, credentials);
        UserIdentity cached = userCache.getIfPresent(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // Delegate to underlying service
        UserIdentity result = delegate.login(username, credentials, request, getOrCreateSession);
        
        // Cache successful authentications
        if (result != null) {
            userCache.put(cacheKey, result);
        }
        
        return result;
    }
    
    private String buildCacheKey(String username, Object credentials) {
        return username + ":" + credentials.hashCode();
    }
    
    @Override
    public boolean validate(UserIdentity user) {
        return delegate.validate(user);
    }
    
    @Override
    public void logout(UserIdentity user) {
        // Invalidate cache entries for this user
        String username = user.getUserPrincipal().getName();
        userCache.asMap().entrySet().removeIf(entry -> entry.getKey().startsWith(username + ":"));
        
        delegate.logout(user);
    }
    
    @Override
    public IdentityService getIdentityService() {
        return delegate.getIdentityService();
    }
    
    @Override
    public void setIdentityService(IdentityService service) {
        delegate.setIdentityService(service);
    }
    
    @Override
    public UserIdentity getUserIdentity(Subject subject, Principal userPrincipal, boolean create) {
        return delegate.getUserIdentity(subject, userPrincipal, create);
    }
}

Best Practices

Login Service Configuration

public class LoginServiceBestPractices {
    
    public void configureProductionLoginService() {
        
        // Use JDBC for production with connection pooling
        JDBCLoginService loginService = new JDBCLoginService();
        loginService.setName("ProductionRealm");
        
        // Use DataSource with connection pooling
        loginService.setDataSourceName("java:comp/env/jdbc/UserDB");
        
        // Configure proper timeouts
        loginService.setConnectionTimeout(30);
        
        // Use with caching wrapper for performance
        CachingLoginServiceWrapper cachingService = new CachingLoginServiceWrapper(
            loginService, Duration.ofMinutes(10));
        
        SecurityHandler security = new SecurityHandler.PathMapped();
        security.setLoginService(cachingService);
    }
    
    public void configureDevelopmentLoginService() {
        
        // Use HashLoginService for development
        HashLoginService devLogin = new HashLoginService("DevRealm");
        
        // Load from classpath resource
        Resource config = Resource.newClassPathResource("dev-users.properties");
        devLogin.setConfig(config);
        
        // Enable hot reload for development
        devLogin.setReloadInterval(5); // Reload every 5 seconds
        
        SecurityHandler security = new SecurityHandler.PathMapped();
        security.setLoginService(devLogin);
    }
    
    public void configureFailoverLoginService() {
        
        // Primary JDBC service
        JDBCLoginService primary = createPrimaryJDBCService();
        
        // Fallback hash service
        HashLoginService fallback = createFallbackHashService();
        
        // Create failover wrapper
        FailoverLoginService failover = new FailoverLoginService(primary, fallback);
        
        SecurityHandler security = new SecurityHandler.PathMapped();
        security.setLoginService(failover);
    }
    
    private static class FailoverLoginService implements LoginService {
        private final LoginService primary;
        private final LoginService fallback;
        
        public FailoverLoginService(LoginService primary, LoginService fallback) {
            this.primary = primary;
            this.fallback = fallback;
        }
        
        @Override
        public UserIdentity login(String username, Object credentials, Request request, 
                                 Function<Boolean, Session> getOrCreateSession) {
            try {
                UserIdentity result = primary.login(username, credentials, request, getOrCreateSession);
                if (result != null) {
                    return result;
                }
            } catch (Exception e) {
                logger.warn("Primary login service failed, trying fallback", e);
            }
            
            return fallback.login(username, credentials, request, getOrCreateSession);
        }
        
        // Implement other LoginService methods with similar failover logic
    }
}

Login Services provide flexible and powerful user authentication capabilities, supporting various storage backends and custom authentication flows while maintaining high performance and security standards.