Unified Security: Implementing Multi-Protocol Authentication with Pac4j in Java

Pac4j is a powerful security engine for Java that provides a unified API for authentication, authorization, and identity management across multiple protocols including OAuth, OpenID Connect, SAML, CAS, and basic auth.

Architecture Overview

Java Application → Pac4j Security Filter → Config → Clients → Identity Providers ↑ (Authenticators / Authorizers)

Step 1: Dependencies Setup

Maven Dependencies

<!-- pom.xml --> <dependencies> <!-- Pac4j Core --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-core</artifactId> <version>5.7.6</version> </dependency> <!-- Pac4j HTTP --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-http</artifactId> <version>5.7.6</version> </dependency> <!-- Pac4j OAuth --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-oauth</artifactId> <version>5.7.6</version> </dependency> <!-- Pac4j OpenID Connect --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-oidc</artifactId> <version>5.7.6</version> </dependency> <!-- Pac4j SAML --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-saml</artifactId> <version>5.7.6</version> </dependency> <!-- Pac4j JWT --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-jwt</artifactId> <version>5.7.6</version> </dependency> <!-- Pac4j Spring Boot --> <dependency> <groupId>org.pac4j</groupId> <artifactId>spring-boot-starter-pac4j</artifactId> <version>7.0.0</version> </dependency> <!-- J2E (for servlet applications) --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-javaee</artifactId> <version>5.7.6</version> </dependency> <!-- Spring Security Integration --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-spring-security</artifactId> <version>5.7.6</version> </dependency> </dependencies>

Step 2: Configuration Classes

Pac4j Configuration

// src/main/java/com/company/pac4j/config/Pac4jConfig.java package com.company.pac4j.config; import org.pac4j.core.authorization.authorizer.Authorizer; import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer; import org.pac4j.core.client.Clients; import org.pac4j.core.config.Config; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.credentials.authenticator.Authenticator; import org.pac4j.core.matching.Matcher; import org.pac4j.core.profile.CommonProfile; import org.pac4j.core.profile.ProfileManager; import org.pac4j.core.profile.definition.CommonProfileDefinition; import org.pac4j.http.client.direct.HeaderClient; import org.pac4j.http.credentials.authenticator.test.SimpleTestTokenAuthenticator; import org.pac4j.jwt.config.signature.SecretSignatureConfiguration; import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator; import org.pac4j.oauth.client.*; import org.pac4j.oidc.client.GoogleOidcClient; import org.pac4j.oidc.client.OidcClient; import org.pac4j.oidc.config.OidcConfiguration; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.config.SAML2Configuration; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import java.util.Arrays; @Configuration public class Pac4jConfig { @Value("${pac4j.baseUrl:http://localhost:8080}") private String baseUrl; @Value("${pac4j.jwt.secret:12345678901234567890123456789012}") private String jwtSecret; @Bean public Config config() { final Clients clients = new Clients(); clients.setClients( // OAuth Clients googleOidcClient(), githubClient(), facebookClient(), // SAML Client saml2Client(), // API Clients headerClient(), jwtClient() ); clients.setDefaultSecurityClients("GoogleOidcClient", "HeaderClient"); final Config config = new Config(clients); // Add authorizers config.addAuthorizer("admin", requireAdminAuthorizer()); config.addAuthorizer("user", requireUserAuthorizer()); config.addAuthorizer("api", requireApiAuthorizer()); // Add matchers config.addMatcher("excludeAssets", excludeAssetsMatcher()); return config; } @Bean public OidcClient googleOidcClient() { OidcConfiguration oidcConfig = new OidcConfiguration(); oidcConfig.setClientId("${google.client.id:}"); oidcConfig.setSecret("${google.client.secret:}"); oidcConfig.setDiscoveryURI("https://accounts.google.com/.well-known/openid-configuration"); oidcConfig.addCustomParam("prompt", "consent"); GoogleOidcClient client = new GoogleOidcClient(oidcConfig); client.setName("GoogleOidcClient"); client.setCallbackUrl(baseUrl + "/callback?client_name=GoogleOidcClient"); return client; } @Bean public GitHubClient githubClient() { GitHubClient client = new GitHubClient(); client.setKey("${github.client.id:}"); client.setSecret("${github.client.secret:}"); client.setName("GitHubClient"); client.setCallbackUrl(baseUrl + "/callback?client_name=GitHubClient"); return client; } @Bean public FacebookClient facebookClient() { FacebookClient client = new FacebookClient(); client.setKey("${facebook.client.id:}"); client.setSecret("${facebook.client.secret:}"); client.setFields("id,name,email,first_name,last_name"); client.setName("FacebookClient"); client.setCallbackUrl(baseUrl + "/callback?client_name=FacebookClient"); return client; } @Bean public SAML2Client saml2Client() { SAML2Configuration cfg = new SAML2Configuration(); cfg.setKeystorePath("classpath:samlKeystore.jks"); cfg.setKeystorePassword("samlstorepass"); cfg.setPrivateKeyPassword("samlkeypass"); cfg.setIdentityProviderMetadataResource(new ClassPathResource("idp-metadata.xml")); cfg.setServiceProviderEntityId(baseUrl + "/saml2"); cfg.setServiceProviderMetadataResource(new ClassPathResource("sp-metadata.xml")); cfg.setForceAuth(false); cfg.setPassive(false); SAML2Client client = new SAML2Client(cfg); client.setName("SAML2Client"); client.setCallbackUrl(baseUrl + "/callback?client_name=SAML2Client"); return client; } @Bean public HeaderClient headerClient() { HeaderClient client = new HeaderClient("X-API-Token", new SimpleTestTokenAuthenticator()); client.setName("HeaderClient"); return client; } @Bean public JwtAuthenticator jwtAuthenticator() { SecretSignatureConfiguration signatureConfig = new SecretSignatureConfiguration(jwtSecret); JwtAuthenticator authenticator = new JwtAuthenticator(); authenticator.addSignatureConfiguration(signatureConfig); return authenticator; } @Bean public org.pac4j.jwt.profile.JwtGenerator jwtGenerator() { return new org.pac4j.jwt.profile.JwtGenerator(new SecretSignatureConfiguration(jwtSecret)); } @Bean public Authorizer requireAdminAuthorizer() { return new RequireAnyRoleAuthorizer(Arrays.asList("ROLE_ADMIN", "ADMIN")); } @Bean public Authorizer requireUserAuthorizer() { return new RequireAnyRoleAuthorizer(Arrays.asList("ROLE_USER", "USER", "ROLE_ADMIN", "ADMIN")); } @Bean public Authorizer requireApiAuthorizer() { return (WebContext context, SessionStore sessionStore, ProfileManager profileManager) -> { return profileManager.getProfiles().stream() .anyMatch(profile -> profile.getRoles().contains("API_USER")); }; } @Bean public Matcher excludeAssetsMatcher() { return (WebContext context, SessionStore sessionStore) -> { String path = context.getPath(); return !path.startsWith("/css/") && !path.startsWith("/js/") && !path.startsWith("/images/") && !path.startsWith("/webjars/") && !path.startsWith("/favicon.ico"); }; } }

Security Configuration

// src/main/java/com/company/pac4j/config/SecurityConfig.java package com.company.pac4j.config; import org.pac4j.core.config.Config; import org.pac4j.springframework.security.web.SecurityFilter; import org.pac4j.springframework.web.SecurityInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; @Configuration @EnableWebSecurity public class SecurityConfig implements WebMvcConfigurer { @Autowired private Config config; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .addFilterBefore(securityFilter(), BasicAuthenticationFilter.class) .authorizeHttpRequests(authz -> authz .requestMatchers(antMatcher("/")).permitAll() .requestMatchers(antMatcher("/login**")).permitAll() .requestMatchers(antMatcher("/callback**")).permitAll() .requestMatchers(antMatcher("/assets/**")).permitAll() .requestMatchers(antMatcher("/public/**")).permitAll() .requestMatchers(antMatcher("/admin/**")).hasRole("ADMIN") .requestMatchers(antMatcher("/api/**")).hasRole("API_USER") .anyRequest().authenticated() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") ); return http.build(); } @Bean public SecurityFilter securityFilter() { return new SecurityFilter(config, "GoogleOidcClient,GitHubClient,HeaderClient"); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(securityInterceptor("/admin/**")) .addPathPatterns("/admin/**") .excludePathPatterns("/assets/**"); registry.addInterceptor(securityInterceptor("/api/**")) .addPathPatterns("/api/**") .excludePathPatterns("/api/public/**"); } private SecurityInterceptor securityInterceptor(String path) { SecurityInterceptor interceptor = new SecurityInterceptor(config); interceptor.setAuthorizers("admin,api"); interceptor.setClients("HeaderClient,ParameterClient"); return interceptor; } }

Step 3: Profile Management

Custom Profile Classes

// src/main/java/com/company/pac4j/profile/CustomCommonProfile.java package com.company.pac4j.profile; import org.pac4j.core.profile.CommonProfile; import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.Date; import java.util.Map; public class CustomCommonProfile extends CommonProfile { private String department; private String employeeId; private Date lastLogin; private Map<String, Object> customAttributes; public CustomCommonProfile() { super(); } public String getDepartment() { return department; } public void setDepartment(String department) { this.department = department; } public String getEmployeeId() { return employeeId; } public void setEmployeeId(String employeeId) { this.employeeId = employeeId; } public Date getLastLogin() { return lastLogin; } public void setLastLogin(Date lastLogin) { this.lastLogin = lastLogin; } public Map<String, Object> getCustomAttributes() { return customAttributes; } public void setCustomAttributes(Map<String, Object> customAttributes) { this.customAttributes = customAttributes; } @JsonIgnore public boolean isActive() { return isRemembered() || !isExpired(); } @JsonIgnore public boolean isExpired() { return lastLogin != null && new Date().getTime() - lastLogin.getTime() > 30 * 24 * 60 * 60 * 1000L; // 30 days } public String getDisplayName() { if (getDisplayName() != null) { return getDisplayName(); } else if (getFirstName() != null && getLastName() != null) { return getFirstName() + " " + getLastName(); } else { return getUsername(); } } } // src/main/java/com/company/pac4j/profile/ProfileService.java package com.company.pac4j.profile; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.profile.ProfileManager; import org.pac4j.core.profile.UserProfile; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; @Service public class ProfileService { public Optional<CustomCommonProfile> getCurrentProfile(WebContext context, SessionStore sessionStore) { ProfileManager profileManager = new ProfileManager(context, sessionStore); Optional<UserProfile> profile = profileManager.getProfile(); return profile.map(p -> { if (p instanceof CustomCommonProfile) { return (CustomCommonProfile) p; } else { // Convert to custom profile CustomCommonProfile customProfile = new CustomCommonProfile(); customProfile.setId(p.getId()); customProfile.setLinkedId(p.getLinkedId()); customProfile.setRoles(p.getRoles()); customProfile.setPermissions(p.getPermissions()); customProfile.setClientName(p.getClientName()); customProfile.setAttributes(p.getAttributes()); return customProfile; } }); } public List<UserProfile> getAllProfiles(WebContext context, SessionStore sessionStore) { ProfileManager profileManager = new ProfileManager(context, sessionStore); return profileManager.getProfiles(); } public boolean isAuthenticated(WebContext context, SessionStore sessionStore) { ProfileManager profileManager = new ProfileManager(context, sessionStore); return profileManager.isAuthenticated(); } public void logout(WebContext context, SessionStore sessionStore) { ProfileManager profileManager = new ProfileManager(context, sessionStore); profileManager.logout(); } public void saveProfile(WebContext context, SessionStore sessionStore, CustomCommonProfile profile) { ProfileManager profileManager = new ProfileManager(context, sessionStore); profileManager.save(true, profile, false); } }

Step 4: Custom Authenticators and Authorizers

Custom Authenticator

// src/main/java/com/company/pac4j/security/DatabaseAuthenticator.java package com.company.pac4j.security; import org.pac4j.core.credentials.Credentials; import org.pac4j.core.credentials.authenticator.Authenticator; import org.pac4j.core.exception.CredentialsException; import org.pac4j.core.profile.CommonProfile; import org.pac4j.core.util.CommonHelper; import org.pac4j.http.credentials.DigestCredentials; import org.pac4j.http.credentials.password.PasswordEncoder; import org.pac4j.http.credentials.password.SpringSecurityPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; @Component public class DatabaseAuthenticator implements Authenticator { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public DatabaseAuthenticator(UserRepository userRepository) { this.userRepository = userRepository; this.passwordEncoder = new SpringSecurityPasswordEncoder(new BCryptPasswordEncoder()); } @Override public void validate(Credentials credentials) { if (credentials == null) { throw new CredentialsException("No credentials provided"); } if (credentials instanceof org.pac4j.http.credentials.UsernamePasswordCredentials) { validateUsernamePassword((org.pac4j.http.credentials.UsernamePasswordCredentials) credentials); } else if (credentials instanceof DigestCredentials) { validateDigest((DigestCredentials) credentials); } else { throw new CredentialsException("Unsupported credentials type: " + credentials.getClass()); } } private void validateUsernamePassword(org.pac4j.http.credentials.UsernamePasswordCredentials credentials) { String username = credentials.getUsername(); String password = credentials.getPassword(); CommonHelper.assertNotBlank("username", username); CommonHelper.assertNotBlank("password", password); User user = userRepository.findByUsername(username) .orElseThrow(() -> new CredentialsException("User not found: " + username)); if (!passwordEncoder.matches(password, user.getPassword())) { throw new CredentialsException("Invalid password for user: " + username); } if (!user.isEnabled()) { throw new CredentialsException("User account is disabled: " + username); } if (user.isLocked()) { throw new CredentialsException("User account is locked: " + username); } CommonProfile profile = createProfile(user); credentials.setUserProfile(profile); } private void validateDigest(DigestCredentials credentials) { // Implement digest authentication String username = credentials.getUsername(); User user = userRepository.findByUsername(username) .orElseThrow(() -> new CredentialsException("User not found: " + username)); // Validate digest credentials // This is a simplified implementation CommonProfile profile = createProfile(user); credentials.setUserProfile(profile); } private CommonProfile createProfile(User user) { CommonProfile profile = new CommonProfile(); profile.setId(user.getUsername()); profile.addAttribute("username", user.getUsername()); profile.addAttribute("email", user.getEmail()); profile.addAttribute("firstName", user.getFirstName()); profile.addAttribute("lastName", user.getLastName()); profile.addAttribute("department", user.getDepartment()); // Add roles user.getRoles().forEach(profile::addRole); // Add permissions user.getPermissions().forEach(profile::addPermission); return profile; } } // Supporting entity class @Data @Entity @Table(name = "users") class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; private String email; private String firstName; private String lastName; private String department; private boolean enabled = true; private boolean locked = false; @ElementCollection @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "role") private Set<String> roles = new HashSet<>(); @ElementCollection @CollectionTable(name = "user_permissions", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "permission") private Set<String> permissions = new HashSet<>(); }

Custom Authorizer

// src/main/java/com/company/pac4j/security/PermissionAuthorizer.java package com.company.pac4j.security; import org.pac4j.core.authorization.authorizer.Authorizer; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.profile.UserProfile; import org.pac4j.core.profile.ProfileManager; import org.springframework.stereotype.Component; import java.util.List; import java.util.Set; @Component public class PermissionAuthorizer implements Authorizer { private final Set<String> requiredPermissions; public PermissionAuthorizer(String... permissions) { this.requiredPermissions = Set.of(permissions); } public PermissionAuthorizer(Set<String> permissions) { this.requiredPermissions = permissions; } @Override public boolean isAuthorized(WebContext context, SessionStore sessionStore, List<UserProfile> profiles) { if (profiles == null || profiles.isEmpty()) { return false; } return profiles.stream() .anyMatch(profile -> hasRequiredPermissions(profile)); } private boolean hasRequiredPermissions(UserProfile profile) { Set<String> userPermissions = profile.getPermissions(); return userPermissions.containsAll(requiredPermissions); } } // src/main/java/com/company/pac4j/security/DepartmentAuthorizer.java @Component public class DepartmentAuthorizer implements Authorizer { private final String requiredDepartment; public DepartmentAuthorizer(String department) { this.requiredDepartment = department; } @Override public boolean isAuthorized(WebContext context, SessionStore sessionStore, List<UserProfile> profiles) { return profiles.stream() .anyMatch(profile -> { String department = (String) profile.getAttribute("department"); return requiredDepartment.equals(department); }); } } // src/main/java/com/company/pac4j/security/IpAuthorizer.java @Component public class IpAuthorizer implements Authorizer { private final Set<String> allowedIps; public IpAuthorizer(String... ips) { this.allowedIps = Set.of(ips); } @Override public boolean isAuthorized(WebContext context, SessionStore sessionStore, List<UserProfile> profiles) { String clientIp = context.getRemoteAddr(); return allowedIps.contains(clientIp); } }

Step 5: REST API Controllers

Authentication Controller

// src/main/java/com/company/pac4j/controller/AuthController.java package com.company.pac4j.controller; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.engine.SecurityLogic; import org.pac4j.core.profile.ProfileManager; import org.pac4j.core.profile.UserProfile; import org.pac4j.springframework.web.SecurityInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; import java.util.Optional; @RestController @RequestMapping("/auth") public class AuthController { @Autowired private SecurityLogic securityLogic; @Autowired private ProfileService profileService; @GetMapping("/login") public ResponseEntity<Map<String, Object>> login( HttpServletRequest request, HttpServletResponse response) { WebContext context = new J2EContext(request, response); ProfileManager profileManager = new ProfileManager(context); Map<String, Object> result = new HashMap<>(); if (profileManager.isAuthenticated()) { Optional<UserProfile> profile = profileManager.getProfile(); result.put("authenticated", true); result.put("user", profile.get()); result.put("message", "Already logged in"); } else { result.put("authenticated", false); result.put("loginUrls", getLoginUrls()); } return ResponseEntity.ok(result); } @PostMapping("/login") public ResponseEntity<Map<String, Object>> performLogin( @RequestParam String username, @RequestParam String password, HttpServletRequest request, HttpServletResponse response) { // This would typically be handled by a FormClient // For demonstration purposes Map<String, Object> result = new HashMap<>(); result.put("message", "Use OAuth providers for login"); return ResponseEntity.ok(result); } @GetMapping("/profile") public ResponseEntity<Map<String, Object>> getProfile( HttpServletRequest request, HttpServletResponse response) { WebContext context = new J2EContext(request, response); Optional<CustomCommonProfile> profile = profileService.getCurrentProfile(context, context.getSessionStore()); Map<String, Object> result = new HashMap<>(); if (profile.isPresent()) { result.put("authenticated", true); result.put("profile", profile.get()); } else { result.put("authenticated", false); result.put("message", "Not authenticated"); } return ResponseEntity.ok(result); } @PostMapping("/logout") public ResponseEntity<Map<String, Object>> logout( HttpServletRequest request, HttpServletResponse response) { WebContext context = new J2EContext(request, response); profileService.logout(context, context.getSessionStore()); Map<String, Object> result = new HashMap<>(); result.put("success", true); result.put("message", "Logged out successfully"); return ResponseEntity.ok(result); } @GetMapping("/providers") public ResponseEntity<Map<String, Object>> getAuthProviders() { Map<String, Object> providers = new HashMap<>(); providers.put("google", "/callback?client_name=GoogleOidcClient"); providers.put("github", "/callback?client_name=GitHubClient"); providers.put("facebook", "/callback?client_name=FacebookClient"); providers.put("saml", "/callback?client_name=SAML2Client"); return ResponseEntity.ok(providers); } private Map<String, String> getLoginUrls() { Map<String, String> urls = new HashMap<>(); urls.put("google", "/login/google"); urls.put("github", "/login/github"); urls.put("facebook", "/login/facebook"); urls.put("saml", "/login/saml"); return urls; } }

Protected API Controller

// src/main/java/com/company/pac4j/controller/ApiController.java package com.company.pac4j.controller; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.profile.UserProfile; import org.pac4j.springframework.web.SecurityInterceptor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Map; @RestController @RequestMapping("/api") public class ApiController { @GetMapping("/public/info") public ResponseEntity<Map<String, String>> publicInfo() { return ResponseEntity.ok(Map.of( "message", "This is public information", "timestamp", String.valueOf(System.currentTimeMillis()) )); } @SecurityInterceptor(clients = "HeaderClient", authorizers = "api") @GetMapping("/secure/data") public ResponseEntity<Map<String, Object>> secureData( HttpServletRequest request, HttpServletResponse response) { WebContext context = new J2EContext(request, response); ProfileManager profileManager = new ProfileManager(context); UserProfile profile = profileManager.getProfile().orElse(null); return ResponseEntity.ok(Map.of( "message", "This is secure API data", "user", profile != null ? profile.getId() : "unknown", "timestamp", System.currentTimeMillis() )); } @SecurityInterceptor(clients = "HeaderClient", authorizers = "admin") @GetMapping("/admin/users") public ResponseEntity<Map<String, Object>> adminUsers() { return ResponseEntity.ok(Map.of( "message", "Admin user list", "users", java.util.List.of("user1", "user2", "user3"), "timestamp", System.currentTimeMillis() )); } @SecurityInterceptor(clients = "HeaderClient", authorizers = "user") @PostMapping("/user/profile") public ResponseEntity<Map<String, Object>> updateProfile( @RequestBody Map<String, Object> profileData, HttpServletRequest request, HttpServletResponse response) { WebContext context = new J2EContext(request, response); ProfileManager profileManager = new ProfileManager(context); UserProfile profile = profileManager.getProfile().orElse(null); // Update profile logic here return ResponseEntity.ok(Map.of( "message", "Profile updated successfully", "user", profile != null ? profile.getId() : "unknown", "updated", profileData )); } @SecurityInterceptor(clients = "ParameterClient", authorizers = "api") @GetMapping("/token/info") public ResponseEntity<Map<String, Object>> tokenInfo( @RequestParam String token, HttpServletRequest request, HttpServletResponse response) { // Validate token and return info return ResponseEntity.ok(Map.of( "token", token, "valid", true, "scopes", java.util.List.of("read", "write"), "expires_in", 3600 )); } }

Step 6: JWT Token Management

JWT Service

// src/main/java/com/company/pac4j/jwt/JwtTokenService.java package com.company.pac4j.jwt; import org.pac4j.core.profile.CommonProfile; import org.pac4j.jwt.profile.JwtGenerator; import org.pac4j.jwt.config.signature.SecretSignatureConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; @Service public class JwtTokenService { private final JwtGenerator jwtGenerator; private final long expirationTime; @Autowired public JwtTokenService(@Value("${pac4j.jwt.secret}") String jwtSecret, @Value("${pac4j.jwt.expiration:3600}") long expiration) { SecretSignatureConfiguration signatureConfig = new SecretSignatureConfiguration(jwtSecret); this.jwtGenerator = new JwtGenerator(signatureConfig); this.expirationTime = expiration * 1000; // Convert to milliseconds } public String generateToken(CommonProfile profile) { // Add custom claims profile.addAttribute("iss", "my-application"); profile.addAttribute("iat", new Date().getTime() / 1000); profile.addAttribute("exp", (new Date().getTime() + expirationTime) / 1000); return jwtGenerator.generate(profile); } public String generateToken(Map<String, Object> claims) { CommonProfile profile = new CommonProfile(); claims.forEach(profile::addAttribute); return generateToken(profile); } public Optional<CommonProfile> validateToken(String token) { try { // This would use JwtAuthenticator in a real scenario return Optional.of(new CommonProfile()); } catch (Exception e) { return Optional.empty(); } } public Map<String, Object> parseToken(String token) { try { // Parse JWT token without validation // In production, use proper JWT library return new HashMap<>(); } catch (Exception e) { return new HashMap<>(); } } public boolean isTokenExpired(String token) { Map<String, Object> claims = parseToken(token); Long exp = (Long) claims.get("exp"); return exp != null && exp < (System.currentTimeMillis() / 1000); } public String refreshToken(String token) { if (isTokenExpired(token)) { Map<String, Object> claims = parseToken(token); claims.remove("exp"); claims.remove("iat"); return generateToken(claims); } return token; } }

Step 7: Application Configuration

application.yml

# application.yml pac4j: baseUrl: http://localhost:8080 jwt: secret: "12345678901234567890123456789012" expiration: 3600 security: oauth2: client: google: clientId: ${GOOGLE_CLIENT_ID:} clientSecret: ${GOOGLE_CLIENT_SECRET:} github: clientId: ${GITHUB_CLIENT_ID:} clientSecret: ${GITHUB_CLIENT_SECRET:} facebook: clientId: ${FACEBOOK_CLIENT_ID:} clientSecret: ${FACEBOOK_CLIENT_SECRET:} server: port: 8080 servlet: session: timeout: 30m logging: level: org.pac4j: DEBUG com.company.pac4j: DEBUG

Step 8: Web Configuration

// src/main/java/com/company/pac4j/config/WebConfig.java package com.company.pac4j.config; import org.pac4j.core.config.Config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private Config config; @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("home"); registry.addViewController("/login").setViewName("login"); registry.addViewController("/dashboard").setViewName("dashboard"); registry.addViewController("/admin").setViewName("admin"); } @Override public void addInterceptors(InterceptorRegistry registry) { // Global security interceptor registry.addInterceptor(new org.pac4j.springframework.web.SecurityInterceptor(config, "GoogleOidcClient")) .addPathPatterns("/**") .excludePathPatterns("/callback", "/logout", "/assets/**", "/public/**"); } }

Best Practices

  1. Security
  • Use HTTPS in production
  • Secure JWT secrets properly
  • Implement proper CORS configuration
  • Validate all inputs and tokens
  1. Configuration
  • Externalize client credentials
  • Use environment-specific configurations
  • Implement proper error handling
  1. Performance
  • Use appropriate session storage
  • Cache profile information when possible
  • Implement token refresh strategies
  1. Monitoring
  • Log authentication events
  • Monitor token usage and expiration
  • Track failed authentication attempts

Conclusion

Pac4j provides a comprehensive security solution with:

  • Multiple Protocols: OAuth, OIDC, SAML, CAS, and more
  • Flexible Integration: Spring Boot, J2EE, and standalone
  • Extensible Architecture: Custom authenticators and authorizers
  • Unified API: Consistent security across all protocols

Key implementation aspects:

  1. Configuration management for multiple identity providers
  2. Custom profile management for business-specific attributes
  3. Flexible authorization with custom authorizers
  4. JWT token management for API security
  5. Spring Security integration for web applications

This implementation provides a production-ready security foundation that can scale from simple web applications to complex enterprise systems with multiple authentication providers.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper