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
- Security
- Use HTTPS in production
- Secure JWT secrets properly
- Implement proper CORS configuration
- Validate all inputs and tokens
- Configuration
- Externalize client credentials
- Use environment-specific configurations
- Implement proper error handling
- Performance
- Use appropriate session storage
- Cache profile information when possible
- Implement token refresh strategies
- 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:
- Configuration management for multiple identity providers
- Custom profile management for business-specific attributes
- Flexible authorization with custom authorizers
- JWT token management for API security
- 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.