Article
In enterprise environments, centralized identity and access management are crucial for security and operational efficiency. FreeIPA (Identity, Policy, and Audit) is an open-source identity management solution that combines Linux (Fedora), 389 Directory Server, MIT Kerberos, and more. For Java applications running in enterprise Linux environments, FreeIPA integration provides robust authentication, authorization, and centralized user management.
What is FreeIPA?
FreeIPA is a complete identity management solution that provides:
- Centralized Authentication: Linux/Unix user authentication via Kerberos
- Directory Services: LDAP-based user and group management
- Policy Management: Host-based access control (HBAC) and sudo rules
- Certificate Management: SSL certificate authority and PKI
- Multi-Domain Trust: Cross-realm trust with Active Directory
Why Integrate FreeIPA with Java Applications?
- Enterprise Single Sign-On: Leverage existing corporate credentials
- Centralized User Management: Sync with enterprise directory services
- Role-Based Access Control: Use existing FreeIPA groups for authorization
- Compliance: Built-in auditing and access logging
- Cost-Effective: Open-source alternative to commercial IAM solutions
FreeIPA Integration Approaches
Java applications can integrate with FreeIPA through multiple protocols:
Java Application → LDAP (User/Group Lookup) → FreeIPA → Kerberos (Authentication) → REST API (User Management) → Certificates (mTLS)
LDAP Integration with FreeIPA
1. Add LDAP Dependencies:
<!-- pom.xml --> <dependencies> <dependency> <groupId>org.springframework.ldap</groupId> <artifactId>spring-ldap-core</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-ldap</artifactId> </dependency> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>6.0.8</version> </dependency> </dependencies>
2. FreeIPA LDAP Configuration:
# application.yml
freeipa:
ldap:
url: "ldap://freeipa.example.com"
port: 389
base-dn: "dc=example,dc=com"
user-dn: "uid=admin,cn=users,cn=accounts,dc=example,dc=com"
password: "${FREEIPA_LDAP_PASSWORD}"
user-search-base: "cn=users,cn=accounts"
user-search-filter: "(uid={0})"
group-search-base: "cn=groups,cn=accounts"
group-search-filter: "(member={0})"
group-role-attribute: "cn"
3. LDAP Configuration Class:
@Configuration
@EnableConfigurationProperties(FreeIpaLdapProperties.class)
public class FreeIpaLdapConfig {
private final FreeIpaLdapProperties properties;
public FreeIpaLdapConfig(FreeIpaLdapProperties properties) {
this.properties = properties;
}
@Bean
public LdapContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(properties.getUrl() + ":" + properties.getPort());
contextSource.setBase(properties.getBaseDn());
contextSource.setUserDn(properties.getUserDn());
contextSource.setPassword(properties.getPassword());
contextSource.setReferral("follow");
Map<String, Object> baseEnvironment = new HashMap<>();
baseEnvironment.put("com.sun.jndi.ldap.connect.timeout", "5000");
baseEnvironment.put("com.sun.jndi.ldap.read.timeout", "5000");
contextSource.setBaseEnvironmentProperties(baseEnvironment);
return contextSource;
}
@Bean
public LdapTemplate ldapTemplate() {
return new LdapTemplate(contextSource());
}
}
4. FreeIPA LDAP Service:
@Service
public class FreeIpaLdapService {
private final LdapTemplate ldapTemplate;
private final FreeIpaLdapProperties properties;
public FreeIpaLdapService(LdapTemplate ldapTemplate, FreeIpaLdapProperties properties) {
this.ldapTemplate = ldapTemplate;
this.properties = properties;
}
public Optional<FreeIpaUser> findUserByUsername(String username) {
try {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "posixAccount"));
filter.and(new EqualsFilter("uid", username));
List<FreeIpaUser> users = ldapTemplate.search(
properties.getUserSearchBase(),
filter.encode(),
new FreeIpaUserAttributesMapper()
);
return users.isEmpty() ? Optional.empty() : Optional.of(users.get(0));
} catch (Exception e) {
throw new RuntimeException("Failed to find user: " + username, e);
}
}
public List<FreeIpaUser> findUsersByGroup(String groupName) {
try {
// First find the group DN
String groupDn = findGroupDn(groupName);
// Then find all members of the group
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "posixAccount"));
filter.and(new EqualsFilter("memberOf", groupDn));
return ldapTemplate.search(
properties.getUserSearchBase(),
filter.encode(),
new FreeIpaUserAttributesMapper()
);
} catch (Exception e) {
throw new RuntimeException("Failed to find users in group: " + groupName, e);
}
}
public List<String> getUserGroups(String username) {
try {
Optional<FreeIpaUser> user = findUserByUsername(username);
if (user.isPresent()) {
return findGroupsForUser(user.get().getDn());
}
return Collections.emptyList();
} catch (Exception e) {
throw new RuntimeException("Failed to get groups for user: " + username, e);
}
}
public boolean authenticateUser(String username, String password) {
try {
Optional<FreeIpaUser> user = findUserByUsername(username);
if (user.isEmpty()) {
return false;
}
// Create a new context with user credentials
LdapContextSource userContext = new LdapContextSource();
userContext.setUrl(properties.getUrl() + ":" + properties.getPort());
userContext.setBase(properties.getBaseDn());
userContext.setUserDn(user.get().getDn());
userContext.setPassword(password);
userContext.afterPropertiesSet();
// Try to bind with user credentials
try (LdapTemplate userLdapTemplate = new LdapTemplate(userContext)) {
userLdapTemplate.setIgnorePartialResultException(true);
userLdapTemplate.list(""); // Simple operation to test binding
return true;
} catch (Exception e) {
return false;
}
} catch (Exception e) {
throw new RuntimeException("Authentication failed for user: " + username, e);
}
}
private String findGroupDn(String groupName) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "posixGroup"));
filter.and(new EqualsFilter("cn", groupName));
List<String> groups = ldapTemplate.search(
properties.getGroupSearchBase(),
filter.encode(),
(AttributesMapper<String>) attrs ->
attrs.get("dn").get().toString()
);
return groups.isEmpty() ? null : groups.get(0);
}
private List<String> findGroupsForUser(String userDn) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "posixGroup"));
filter.and(new EqualsFilter("member", userDn));
return ldapTemplate.search(
properties.getGroupSearchBase(),
filter.encode(),
(AttributesMapper<String>) attrs ->
attrs.get("cn").get().toString()
);
}
}
5. FreeIPA User Model and Mapper:
@Data
public class FreeIpaUser {
private String dn;
private String uid;
private String cn;
private String givenName;
private String sn;
private String mail;
private String telephoneNumber;
private String title;
private String department;
private List<String> memberOf;
private Long uidNumber;
private Long gidNumber;
private String loginShell;
private String homeDirectory;
}
public class FreeIpaUserAttributesMapper implements AttributesMapper<FreeIpaUser> {
@Override
public FreeIpaUser mapFromAttributes(Attributes attrs) throws NamingException {
FreeIpaUser user = new FreeIpaUser();
if (attrs.get("dn") != null) {
user.setDn(attrs.get("dn").get().toString());
}
if (attrs.get("uid") != null) {
user.setUid(attrs.get("uid").get().toString());
}
if (attrs.get("cn") != null) {
user.setCn(attrs.get("cn").get().toString());
}
if (attrs.get("givenName") != null) {
user.setGivenName(attrs.get("givenName").get().toString());
}
if (attrs.get("sn") != null) {
user.setSn(attrs.get("sn").get().toString());
}
if (attrs.get("mail") != null) {
user.setMail(attrs.get("mail").get().toString());
}
if (attrs.get("uidNumber") != null) {
user.setUidNumber(Long.valueOf(attrs.get("uidNumber").get().toString()));
}
if (attrs.get("gidNumber") != null) {
user.setGidNumber(Long.valueOf(attrs.get("gidNumber").get().toString()));
}
return user;
}
}
Kerberos Authentication Integration
1. Kerberos Configuration:
@Configuration
public class KerberosConfig {
@Value("${freeipa.kerberos.realm:EXAMPLE.COM}")
private String kerberosRealm;
@Value("${freeipa.kerberos.kdc:kdc.example.com}")
private String kerberosKdc;
@PostConstruct
public void configureKerberos() {
System.setProperty("java.security.krb5.realm", kerberosRealm);
System.setProperty("java.security.krb5.kdc", kerberosKdc);
System.setProperty("sun.security.krb5.debug", "false");
}
@Bean
public LoginContext loginContext() throws LoginException {
Configuration configuration = new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, String> options = new HashMap<>();
options.put("useTicketCache", "true");
options.put("renewTGT", "true");
options.put("doNotPrompt", "true");
return new AppConfigurationEntry[]{
new AppConfigurationEntry(
"com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
options
)
};
}
};
return new LoginContext("FreeIPAKerberos", null, null, configuration);
}
}
2. Kerberos Authentication Service:
@Service
public class KerberosAuthService {
private final FreeIpaLdapService ldapService;
public KerberosAuthService(FreeIpaLdapService ldapService) {
this.ldapService = ldapService;
}
public boolean authenticateWithKerberos(String username, String password) {
try {
// Create a Kerberos login context
LoginContext lc = createLoginContext(username, password);
lc.login();
// If login succeeds, verify user exists in FreeIPA
Optional<FreeIpaUser> user = ldapService.findUserByUsername(username);
lc.logout();
return user.isPresent();
} catch (LoginException e) {
return false;
} catch (Exception e) {
throw new RuntimeException("Kerberos authentication failed", e);
}
}
public String getServiceTicket(String servicePrincipal) {
try {
LoginContext lc = new LoginContext("FreeIPAKerberos");
lc.login();
Subject subject = lc.getSubject();
return Subject.doAs(subject, (PrivilegedAction<String>) () -> {
try {
GSSManager manager = GSSManager.getInstance();
GSSName serverName = manager.createName(
servicePrincipal, GSSName.NT_HOSTBASED_SERVICE);
GSSContext context = manager.createContext(
serverName, null, null, GSSContext.DEFAULT_LIFETIME);
byte[] token = new byte[0];
byte[] serviceTicket = context.initSecContext(token, 0, token.length);
context.dispose();
return serviceTicket != null ? Base64.getEncoder().encodeToString(serviceTicket) : null;
} catch (GSSException e) {
throw new RuntimeException("Failed to get service ticket", e);
}
});
} catch (LoginException e) {
throw new RuntimeException("Kerberos login failed", e);
}
}
private LoginContext createLoginContext(String username, String password) throws LoginException {
Configuration configuration = new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, String> options = new HashMap<>();
options.put("useTicketCache", "false");
options.put("doNotPrompt", "false");
options.put("useKeyTab", "false");
options.put("principal", username);
options.put("storeKey", "true");
return new AppConfigurationEntry[]{
new AppConfigurationEntry(
"com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
options
)
};
}
};
CallbackHandler callbackHandler = callbacks -> {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
((NameCallback) callback).setName(username);
} else if (callback instanceof PasswordCallback) {
((PasswordCallback) callback).setPassword(password.toCharArray());
}
}
};
return new LoginContext("FreeIPAKerberos", null, callbackHandler, configuration);
}
}
Spring Security Integration
1. FreeIPA Authentication Provider:
@Component
public class FreeIpaAuthenticationProvider implements AuthenticationProvider {
private final FreeIpaLdapService ldapService;
private final KerberosAuthService kerberosService;
public FreeIpaAuthenticationProvider(FreeIpaLdapService ldapService,
KerberosAuthService kerberosService) {
this.ldapService = ldapService;
this.kerberosService = kerberosService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
try {
// Try LDAP authentication first
boolean authenticated = ldapService.authenticateUser(username, password);
// Alternatively, use Kerberos authentication
// boolean authenticated = kerberosService.authenticateWithKerberos(username, password);
if (authenticated) {
Optional<FreeIpaUser> user = ldapService.findUserByUsername(username);
if (user.isPresent()) {
List<GrantedAuthority> authorities = getUserAuthorities(username);
FreeIpaUserDetails userDetails = new FreeIpaUserDetails(
user.get(), authorities
);
return new UsernamePasswordAuthenticationToken(
userDetails, password, authorities
);
}
}
} catch (Exception e) {
throw new BadCredentialsException("FreeIPA authentication failed", e);
}
throw new BadCredentialsException("Invalid FreeIPA credentials");
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
private List<GrantedAuthority> getUserAuthorities(String username) {
List<String> groups = ldapService.getUserGroups(username);
return groups.stream()
.map(group -> new SimpleGrantedAuthority("ROLE_" + group.toUpperCase()))
.collect(Collectors.toList());
}
}
2. FreeIPA User Details:
public class FreeIpaUserDetails implements UserDetails {
private final FreeIpaUser freeIpaUser;
private final List<GrantedAuthority> authorities;
public FreeIpaUserDetails(FreeIpaUser freeIpaUser, List<GrantedAuthority> authorities) {
this.freeIpaUser = freeIpaUser;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return null; // Password is not stored
}
@Override
public String getUsername() {
return freeIpaUser.getUid();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public FreeIpaUser getFreeIpaUser() {
return freeIpaUser;
}
}
3. Security Configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final FreeIpaAuthenticationProvider freeIpaAuthProvider;
public SecurityConfig(FreeIpaAuthenticationProvider freeIpaAuthProvider) {
this.freeIpaAuthProvider = freeIpaAuthProvider;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMINS")
.requestMatchers("/user/**").hasRole("USERS")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout=true")
.permitAll()
)
.authenticationProvider(freeIpaAuthProvider);
return http.build();
}
}
FreeIPA REST API Integration
FreeIPA also provides a JSON-RPC API for user management:
@Service
public class FreeIpaRestService {
private final String freeIpaUrl;
private final String adminUser;
private final String adminPassword;
public FreeIpaRestService(@Value("${freeipa.api.url}") String freeIpaUrl,
@Value("${freeipa.admin.user}") String adminUser,
@Value("${freeipa.admin.password}") String adminPassword) {
this.freeIpaUrl = freeIpaUrl;
this.adminUser = adminUser;
this.adminPassword = adminPassword;
}
public String addUser(String username, String firstName, String lastName, String email) {
try {
// Get session cookie first
String sessionCookie = authenticate();
Map<String, Object> params = new HashMap<>();
params.put("givenname", firstName);
params.put("sn", lastName);
params.put("cn", firstName + " " + lastName);
params.put("mail", email);
Map<String, Object> request = new HashMap<>();
request.put("method", "user_add");
request.put("params", new Object[][]{{new String[]{username}, params}});
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create(freeIpaUrl + "/ipa/session/json"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Cookie", sessionCookie)
.POST(HttpRequest.BodyPublishers.ofString(new ObjectMapper().writeValueAsString(request)))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());
return response.body();
} catch (Exception e) {
throw new RuntimeException("Failed to add user via FreeIPA API", e);
}
}
private String authenticate() throws Exception {
Map<String, String> authParams = new HashMap<>();
authParams.put("user", adminUser);
authParams.put("password", adminPassword);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(freeIpaUrl + "/ipa/session/login_password"))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "text/plain")
.POST(HttpRequest.BodyPublishers.ofString(
"user=" + adminUser + "&password=" + adminPassword))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Extract session cookie from response headers
return response.headers().firstValue("set-cookie").orElseThrow();
}
}
Best Practices for Production
- Connection Pooling: Use LDAP connection pooling for better performance
- SSL/TLS: Always use LDAPS for secure communication
- Caching: Cache user and group lookups to reduce LDAP queries
- Error Handling: Implement robust error handling for FreeIPA outages
- Monitoring: Monitor LDAP query performance and error rates
@Component
public class FreeIpaHealthIndicator implements HealthIndicator {
private final FreeIpaLdapService ldapService;
public FreeIpaHealthIndicator(FreeIpaLdapService ldapService) {
this.ldapService = ldapService;
}
@Override
public Health health() {
try {
// Test LDAP connectivity
Optional<FreeIpaUser> user = ldapService.findUserByUsername("admin");
if (user.isPresent()) {
return Health.up()
.withDetail("service", "FreeIPA")
.withDetail("status", "Connected")
.build();
} else {
return Health.down()
.withDetail("service", "FreeIPA")
.withDetail("status", "Admin user not found")
.build();
}
} catch (Exception e) {
return Health.down(e)
.withDetail("service", "FreeIPA")
.build();
}
}
}
Conclusion
FreeIPA integration provides Java applications with enterprise-grade identity and access management capabilities. By leveraging LDAP for directory services, Kerberos for authentication, and FreeIPA's REST API for user management, Java developers can build applications that seamlessly integrate with existing enterprise identity infrastructure.
This approach not only enhances security through centralized authentication but also simplifies user management and provides comprehensive audit capabilities. For organizations using FreeIPA as their identity provider, Java application integration ensures consistent security policies and streamlined user administration across the entire technology stack.