What is Jakarta Security?
Think of security like a building's access system:
- Authentication: Checking your ID at the entrance (Who are you?)
- Authorization: Key card access to specific floors (What can you do?)
- Identity Store: The database of employees and their access levels
Jakarta Security is the standard security API for Jakarta EE applications. It provides:
- Authentication mechanisms (form login, HTTP Basic, custom)
- Identity stores (LDAP, database, custom)
- Authorization (role-based access control)
- Security context for programmatic security
| Concept | Question | Example |
|---|---|---|
| Authentication | Who are you? | Login with username/password |
| Authorization | What can you access? | Admin can delete, User can only view |
| Principal | Who is logged in? | The current user object |
| Role | What group? | ADMIN, USER, MANAGER |
Basic Authentication Setup
Form-Based Authentication
import jakarta.security.enterprise.authentication.mechanism.http.*;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
@FormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage = "/login.html",
errorPage = "/login-error.html"
)
)
public class ApplicationConfig {
// Configuration class, no code needed
}
HTTP Basic Authentication
@ApplicationScoped
@BasicAuthenticationMechanismDefinition(
realmName = "MyAppRealm"
)
public class BasicAuthConfig {
// Browser shows login popup
}
Custom Authentication Mechanism
import jakarta.security.enterprise.authentication.mechanism.http.*;
import jakarta.security.enterprise.credential.*;
import jakarta.security.enterprise.identitystore.*;
@ApplicationScoped
public class JwtAuthenticationMechanism
implements HttpAuthenticationMechanism {
@Inject
private IdentityStoreHandler identityStoreHandler;
@Inject
private JwtService jwtService;
@Override
public AuthenticationStatus validateRequest(
HttpServletRequest request,
HttpServletResponse response,
HttpMessageContext context) {
// Extract JWT from Authorization header
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
// Validate and extract claims from JWT
Claims claims = jwtService.validateToken(token);
String username = claims.getSubject();
Set<String> roles = claims.get("roles", Set.class);
// Notify container of successful authentication
return context.notifyContainerAboutLogin(
new CallerPrincipal(username),
roles
);
} catch (JwtException e) {
// Invalid token
return context.responseUnauthorized();
}
}
// No token provided - check if resource is protected
if (context.isProtected()) {
return context.responseUnauthorized();
}
// Allow anonymous access to unprotected resources
return context.doNothing();
}
}
Identity Stores
Database Identity Store
@ApplicationScoped
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "java:comp/DefaultDataSource",
callerQuery = "SELECT password FROM users WHERE username = ?",
groupsQuery = "SELECT role FROM user_roles WHERE username = ?",
hashAlgorithm = Pbkdf2PasswordHash.class,
hashAlgorithmParameters = {
"Pbkdf2PasswordHash.Iterations=210000",
"Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512",
"Pbkdf2PasswordHash.SaltSizeBytes=32"
}
)
public class DatabaseIdentityStoreConfig {
// Users and roles stored in database
}
LDAP Identity Store
@ApplicationScoped
@LdapIdentityStoreDefinition(
url = "ldap://ldap.example.com:389",
bindDn = "cn=admin,dc=example,dc=com",
bindDnPassword = "adminPassword",
callerSearchBase = "ou=users,dc=example,dc=com",
callerNameAttribute = "uid",
groupSearchBase = "ou=groups,dc=example,dc=com",
groupMemberAttribute = "member"
)
public class LdapIdentityStoreConfig {
// Enterprise LDAP authentication
}
Custom Identity Store
@ApplicationScoped
public class CustomIdentityStore implements IdentityStore {
@Inject
private UserRepository userRepository;
@Inject
private PasswordHash passwordHash;
@Override
public CredentialValidationResult validate(Credential credential) {
if (credential instanceof UsernamePasswordCredential) {
UsernamePasswordCredential upc = (UsernamePasswordCredential) credential;
String username = upc.getCaller();
String password = upc.getPasswordAsString();
// Look up user in database
User user = userRepository.findByUsername(username);
if (user != null && passwordHash.verify(password, user.getPasswordHash())) {
// Valid credentials
return new CredentialValidationResult(
username,
user.getRoles() // Set<String> of roles
);
}
}
return CredentialValidationResult.INVALID_RESULT;
}
@Override
public Set<ValidationType> validationTypes() {
return EnumSet.of(ValidationType.VALIDATE, ValidationType.PROVIDE_GROUPS);
}
@Override
public int priority() {
return 100; // Higher priority = checked first
}
}
Securing Endpoints
Servlet Security
@WebServlet("/admin/*")
@ServletSecurity(
@HttpConstraint(rolesAllowed = "ADMIN")
)
public class AdminServlet extends HttpServlet {
// Only users with ADMIN role can access
}
// Different constraints for different methods
@WebServlet("/data")
@ServletSecurity(
httpMethodConstraints = {
@HttpMethodConstraint(value = "GET", rolesAllowed = {"USER", "ADMIN"}),
@HttpMethodConstraint(value = "POST", rolesAllowed = "ADMIN"),
@HttpMethodConstraint(value = "DELETE", rolesAllowed = "ADMIN")
}
)
public class DataServlet extends HttpServlet {
// GET: USER or ADMIN, POST/DELETE: ADMIN only
}
JAX-RS Security
import jakarta.annotation.security.*;
@Path("/api")
@RequestScoped
public class SecuredResource {
@Inject
private SecurityContext securityContext;
// Anyone can access
@GET
@Path("/public")
@PermitAll
public String publicEndpoint() {
return "Public data";
}
// Must be authenticated (any role)
@GET
@Path("/user")
@RolesAllowed({"USER", "ADMIN"})
public String userEndpoint() {
String username = securityContext.getCallerPrincipal().getName();
return "Hello, " + username;
}
// Admin only
@GET
@Path("/admin")
@RolesAllowed("ADMIN")
public String adminEndpoint() {
return "Admin data";
}
// No one can access (useful for deprecation)
@GET
@Path("/deprecated")
@DenyAll
public String deprecatedEndpoint() {
return "Never accessible";
}
}
EJB Security
@Stateless
@DeclareRoles({"USER", "ADMIN", "MANAGER"})
public class SecuredService {
@Resource
private SessionContext ctx;
@RolesAllowed("USER")
public String getUserData() {
return "User data";
}
@RolesAllowed({"ADMIN", "MANAGER"})
public void deleteData(Long id) {
// Admin or Manager can delete
}
@PermitAll
public String getPublicData() {
// Programmatic security check
if (ctx.isCallerInRole("ADMIN")) {
return "Full data for admin";
}
return "Limited data";
}
@RolesAllowed("ADMIN")
@RunAs("SYSTEM") // Execute as SYSTEM role
public void systemOperation() {
// Runs with SYSTEM privileges
}
}
Security Context
import jakarta.security.enterprise.*;
@RequestScoped
public class UserInfoService {
@Inject
private SecurityContext securityContext;
public UserInfo getCurrentUser() {
// Get current principal (logged-in user)
Principal principal = securityContext.getCallerPrincipal();
if (principal == null) {
return null; // Not authenticated
}
String username = principal.getName();
// Check roles
boolean isAdmin = securityContext.isCallerInRole("ADMIN");
boolean isUser = securityContext.isCallerInRole("USER");
// Get all principals (custom principal types)
Set<Principal> principals = securityContext.getPrincipalsByType(Principal.class);
return new UserInfo(username, isAdmin, isUser);
}
public boolean hasPermission(String resource, String action) {
// Custom permission check
if (securityContext.isCallerInRole("ADMIN")) {
return true; // Admins can do anything
}
if ("read".equals(action)) {
return securityContext.isCallerInRole("USER");
}
return false;
}
}
Programmatic Authentication
@Path("/auth")
public class AuthResource {
@Inject
private SecurityContext securityContext;
@POST
@Path("/login")
public Response login(LoginRequest loginRequest) {
// Programmatic authentication
AuthenticationStatus status = securityContext.authenticate(
request,
response,
AuthenticationParameters.withParams()
.credential(new UsernamePasswordCredential(
loginRequest.getUsername(),
loginRequest.getPassword()
))
);
if (status == AuthenticationStatus.SUCCESS) {
return Response.ok("Login successful").build();
}
return Response.status(401).entity("Invalid credentials").build();
}
}
Password Hashing
import jakarta.security.enterprise.identitystore.*;
@ApplicationScoped
public class UserService {
@Inject
private Pbkdf2PasswordHash passwordHash;
@Inject
private UserRepository userRepo;
public void createUser(String username, String password) {
// Initialize with parameters
Map<String, String> params = new HashMap<>();
params.put("Pbkdf2PasswordHash.Iterations", "210000");
params.put("Pbkdf2PasswordHash.Algorithm", "PBKDF2WithHmacSHA512");
params.put("Pbkdf2PasswordHash.SaltSizeBytes", "32");
passwordHash.initialize(params);
// Hash the password
String hashedPassword = passwordHash.generate(password.toCharArray());
// Store user with hashed password
User user = new User();
user.setUsername(username);
user.setPassword(hashedPassword); // Store hash, never plain text!
userRepo.save(user);
}
public boolean verifyPassword(String username, String password) {
User user = userRepo.findByUsername(username);
if (user == null) {
return false;
}
// Verify password against stored hash
return passwordHash.verify(password.toCharArray(), user.getPassword());
}
}
Remember Me
@ApplicationScoped
@FormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage = "/login.html",
errorPage = "/login-error.html"
)
)
@RememberMe(
cookieMaxAgeSeconds = 604800, // 7 days
cookieSecureOnly = true,
cookieHttpOnly = true,
isRememberMeExpression = "#{self.isRememberMe(httpServletRequest)}"
)
public class RememberMeConfig {
public boolean isRememberMe(HttpServletRequest request) {
return "true".equals(request.getParameter("rememberMe"));
}
}
// Custom RememberMe Identity Store
@ApplicationScoped
public class CustomRememberMeIdentityStore
implements RememberMeIdentityStore {
@Inject
private TokenRepository tokenRepo;
@Override
public CredentialValidationResult validate(RememberMeCredential credential) {
String token = credential.getToken();
RememberMeToken storedToken = tokenRepo.findByToken(token);
if (storedToken != null && !storedToken.isExpired()) {
return new CredentialValidationResult(
storedToken.getUsername(),
storedToken.getRoles()
);
}
return CredentialValidationResult.INVALID_RESULT;
}
@Override
public String generateLoginToken(CallerPrincipal principal, Set<String> groups) {
String token = UUID.randomUUID().toString();
tokenRepo.save(new RememberMeToken(token, principal.getName(), groups));
return token;
}
@Override
public void removeLoginToken(String token) {
tokenRepo.deleteByToken(token);
}
}
web.xml Security Constraints
<!-- WEB-INF/web.xml -->
<web-app>
<!-- Security constraint -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Admin Area</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>ADMIN</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- Login configuration -->
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/login-error.html</form-error-page>
</form-login-config>
</login-config>
<!-- Security roles -->
<security-role>
<role-name>ADMIN</role-name>
</security-role>
<security-role>
<role-name>USER</role-name>
</security-role>
</web-app>
Common Security Patterns
Method-Level Security with Interceptor
// Custom security annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@InterceptorBinding
public @interface Secured {
String[] roles() default {};
}
// Security interceptor
@Interceptor
@Secured
@Priority(Interceptor.Priority.APPLICATION)
public class SecurityInterceptor {
@Inject
private SecurityContext securityContext;
@AroundInvoke
public Object checkSecurity(InvocationContext ctx) throws Exception {
Secured secured = ctx.getMethod().getAnnotation(Secured.class);
String[] requiredRoles = secured.roles();
for (String role : requiredRoles) {
if (securityContext.isCallerInRole(role)) {
return ctx.proceed(); // Has required role
}
}
throw new SecurityException("Access denied");
}
}
// Usage
@ApplicationScoped
public class ReportService {
@Secured(roles = {"ADMIN", "MANAGER"})
public Report generateSensitiveReport() {
// Only ADMIN or MANAGER can call this
}
}
Audit Logging
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 100)
public class SecurityAuditInterceptor {
@Inject
private SecurityContext securityContext;
@Inject
private AuditService auditService;
@AroundInvoke
public Object audit(InvocationContext ctx) throws Exception {
String user = securityContext.getCallerPrincipal() != null
? securityContext.getCallerPrincipal().getName()
: "anonymous";
String method = ctx.getMethod().getName();
String className = ctx.getTarget().getClass().getSimpleName();
try {
Object result = ctx.proceed();
auditService.logSuccess(user, className, method);
return result;
} catch (Exception e) {
auditService.logFailure(user, className, method, e.getMessage());
throw e;
}
}
}
Best Practices
DO:
- Use HTTPS everywhere - transport-guarantee="CONFIDENTIAL"
- Hash passwords - Use Pbkdf2PasswordHash with strong params
- Use HttpOnly and Secure cookies - Prevent XSS attacks
- Implement proper logout - Invalidate sessions and tokens
- Use role-based access control - @RolesAllowed annotations
- Validate all inputs - Prevent injection attacks
- Log security events - Audit trail for compliance
- Use CSRF protection - For form submissions
DON'T:
- Don't store plain text passwords - Always hash!
- Don't trust client-side validation - Always validate server-side
- Don't expose sensitive data in errors - Generic error messages
- Don't use GET for sensitive operations - Use POST
- Don't hardcode secrets - Use environment variables
- Don't ignore security headers - X-Frame-Options, CSP, etc.
- Don't use weak session IDs - Let container manage sessions
Summary
- Jakarta Security: Standard security API for Jakarta EE
- Authentication: Verifying user identity (login)
- Authorization: Controlling access (roles, permissions)
- Identity Store: Where user credentials are stored
- SecurityContext: Access current user and roles
- @RolesAllowed: Declarative role-based security
- @FormAuthenticationMechanismDefinition: Form login
- @DatabaseIdentityStoreDefinition: DB user store
- Pbkdf2PasswordHash: Secure password hashing
- Custom mechanisms: JWT, OAuth, etc.