pac4j

Authentication LibraryJavaOAuthSAMLOpenID ConnectSpring BootAuthenticationAuthorization

Authentication Library

pac4j

Overview

pac4j is a comprehensive security engine for Java that provides authentication, authorization, and multi-framework support.

Details

pac4j is a powerful and flexible security framework for Java applications. Provided under the Apache 2 license, it supports diverse authentication mechanisms including OAuth, CAS, SAML, OpenID Connect, LDAP, JWT, MongoDB, and CouchDB. It also provides authorization features such as roles, anonymous/remember-me/fully authenticated users, profile type and attribute checks. It can integrate with almost all Java frameworks including Spring Boot, Play Framework, Vert.x, Spark Java, JAX-RS, and Dropwizard. It provides two client types: DirectClient and IndirectClient, supporting authentication for both web services and UI applications. Advanced security features like CSRF protection, security headers, and IP address restrictions are built-in. The configuration-based approach allows easy construction of clients and authenticators from property files.

Pros and Cons

Pros

  • Diverse Authentication Support: Wide support for OAuth, SAML, OpenID Connect, LDAP, JWT, etc.
  • Framework Agnostic: Compatible with almost all Java frameworks like Spring Boot, Play, Vert.x
  • Unified API: Provides all authentication mechanisms through a unified API
  • Flexible Authorization: Fine-grained authorization control through roles, attributes, profile types
  • Built-in Security: CSRF, security headers, session protection included
  • External Configuration: Configuration management through property files
  • Active Community: Continuous development and support

Cons

  • Learning Curve: Requires understanding of concepts due to extensive functionality
  • Configuration Complexity: Advanced features require detailed configuration
  • Java Limited: Cannot be used outside Java ecosystem
  • Documentation: Some integrations lack detailed documentation
  • Debug Complexity: Multi-layered authentication flows can be difficult to debug
  • Dependencies: Additional dependencies required for each provider

Key Links

Code Examples

Basic Spring Boot Integration

// application.properties
pac4j.callbackUrl=http://localhost:8080/callback
pac4j.properties.oidc.id=your-client-id
pac4j.properties.oidc.secret=your-client-secret
pac4j.properties.oidc.issuer=https://accounts.google.com
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public Config config() {
        // Google OpenID Connect configuration
        OidcConfiguration oidcConfig = new OidcConfiguration();
        oidcConfig.setClientId("your-client-id");
        oidcConfig.setSecret("your-client-secret");
        oidcConfig.setIssuer("https://accounts.google.com");
        
        OidcClient oidcClient = new OidcClient(oidcConfig);
        oidcClient.setName("Google");

        // Clients configuration
        Clients clients = new Clients("http://localhost:8080/callback", oidcClient);

        // Build configuration
        return new Config(clients);
    }

    @Bean
    public SecurityInterceptor securityInterceptor(Config config) {
        return new SecurityInterceptor(config, "Google");
    }
}

Multiple Authentication Providers

@Configuration
public class AuthenticationConfig {

    @Bean
    public Config multiProviderConfig() {
        // Google OpenID Connect
        OidcConfiguration googleConfig = new OidcConfiguration();
        googleConfig.setClientId("google-client-id");
        googleConfig.setSecret("google-client-secret");
        googleConfig.setIssuer("https://accounts.google.com");
        OidcClient googleClient = new OidcClient(googleConfig);
        googleClient.setName("Google");

        // Facebook OAuth
        FacebookConfiguration facebookConfig = new FacebookConfiguration();
        facebookConfig.setKey("facebook-app-id");
        facebookConfig.setSecret("facebook-app-secret");
        facebookConfig.setScope("email,user_profile");
        FacebookClient facebookClient = new FacebookClient(facebookConfig);

        // SAML configuration
        SAML2Configuration samlConfig = new SAML2Configuration();
        samlConfig.setKeystorePath("samlKeystore.jks");
        samlConfig.setKeystorePassword("password");
        samlConfig.setPrivateKeyPassword("password");
        samlConfig.setIdentityProviderMetadataPath("idp-metadata.xml");
        SAML2Client samlClient = new SAML2Client(samlConfig);

        // JWT configuration
        SecretSignatureConfiguration jwtConfig = 
            new SecretSignatureConfiguration("myJwtSecret");
        JwtAuthenticator jwtAuthenticator = 
            new JwtAuthenticator(jwtConfig);
        HeaderClient jwtClient = new HeaderClient("Authorization", 
            "Bearer ", jwtAuthenticator);

        Clients clients = new Clients("http://localhost:8080/callback",
            googleClient, facebookClient, samlClient, jwtClient);

        return new Config(clients);
    }
}

Security Control and Authorizers

@RestController
@RequestMapping("/api")
public class ApiController {

    // Authentication required
    @GetMapping("/secure")
    @RequiresAuthentication
    public String secureEndpoint(HttpServletRequest request) {
        ProfileManager manager = new ProfileManager(
            new JEEContext(request, null));
        Optional<UserProfile> profile = manager.get(true);
        
        return "Hello " + profile.get().getDisplayName();
    }

    // Specific role required
    @GetMapping("/admin")
    @RequiresRoles("ADMIN")
    public String adminEndpoint() {
        return "Admin area";
    }

    // Custom authorization
    @GetMapping("/custom")
    @RequiresAuthorization("customAuthorizer")
    public String customEndpoint() {
        return "Custom authorized content";
    }
}

// Custom authorizer
@Component("customAuthorizer")
public class CustomAuthorizer implements Authorizer<UserProfile> {
    
    @Override
    public boolean isAuthorized(WebContext context, 
                               List<UserProfile> profiles) {
        if (profiles.isEmpty()) {
            return false;
        }
        
        UserProfile profile = profiles.get(0);
        return profile.containsAttribute("department") &&
               "IT".equals(profile.getAttribute("department"));
    }
}

LDAP Authentication Configuration

@Configuration
public class LdapAuthConfig {

    @Bean
    public Config ldapConfig() {
        // LDAP configuration
        LdapProfile ldapProfile = new LdapProfile();
        ldapProfile.setId("cn=admin,dc=example,dc=com");
        ldapProfile.setPassword("admin_password");

        LdaptiveAuthenticator ldapAuthenticator = 
            new LdaptiveAuthenticator("ldap://localhost:389",
                                     "dc=example,dc=com",
                                     "cn=%s,ou=users,dc=example,dc=com");

        UsernamePasswordCredentials credentials = 
            new UsernamePasswordCredentials("admin", "password");
        
        IndirectBasicAuthClient ldapClient = 
            new IndirectBasicAuthClient(ldapAuthenticator);

        Clients clients = new Clients("http://localhost:8080/callback", 
                                     ldapClient);
        
        return new Config(clients);
    }
}

Profile Attribute Management

@Service
public class ProfileService {

    public void handleUserProfile(HttpServletRequest request) {
        JEEContext context = new JEEContext(request, null);
        ProfileManager manager = new ProfileManager(context);
        
        Optional<UserProfile> profile = manager.get(true);
        if (profile.isPresent()) {
            UserProfile userProfile = profile.get();
            
            // Get basic information
            String userId = userProfile.getId();
            String displayName = userProfile.getDisplayName();
            String email = (String) userProfile.getAttribute("email");
            
            // Check roles
            Set<String> roles = userProfile.getRoles();
            boolean isAdmin = roles.contains("ADMIN");
            
            // Check permissions
            Set<String> permissions = userProfile.getPermissions();
            boolean canRead = permissions.contains("READ");
            
            // Custom attributes
            String department = (String) userProfile.getAttribute("department");
            
            // Business logic processing
            processUserData(userId, displayName, email, department, isAdmin);
        }
    }

    private void processUserData(String userId, String displayName, 
                               String email, String department, boolean isAdmin) {
        // User data processing logic
    }
}

Session Management and Logout

@Controller
public class AuthController {

    @RequestMapping("/login")
    public String login(HttpServletRequest request, 
                       @RequestParam(required = false) String client) {
        JEEContext context = new JEEContext(request, null);
        SecurityLogic<Object, JEEContext> securityLogic = 
            DefaultSecurityLogic.INSTANCE;
        
        if (client != null) {
            // Login with specific client
            return "redirect:/callback?client_name=" + client;
        }
        
        return "login";
    }

    @RequestMapping("/logout")
    public String logout(HttpServletRequest request, 
                        HttpServletResponse response) {
        JEEContext context = new JEEContext(request, response);
        ProfileManager manager = new ProfileManager(context);
        
        // Local logout
        manager.logout();
        
        // Invalidate session
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        
        return "redirect:/";
    }

    @RequestMapping("/profile")
    public String profile(HttpServletRequest request, Model model) {
        JEEContext context = new JEEContext(request, null);
        ProfileManager manager = new ProfileManager(context);
        
        Optional<UserProfile> profile = manager.get(true);
        if (profile.isPresent()) {
            model.addAttribute("profile", profile.get());
            model.addAttribute("attributes", profile.get().getAttributes());
        }
        
        return "profile";
    }
}