Login Services provide the foundation for user authentication in Jetty Security, managing user credentials and creating UserIdentity objects from successful authentications.
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);
}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;
}
}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);
}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);
}
}# 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,role4Provides 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);
}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 = ?"
);
}
}-- 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 rolesLogin 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
}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
}
}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
}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();
}
}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();
}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;
}
}
}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; }
}
}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);
}
}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.