Jakarta Security & Authentication

Securing enterprise Java applications

← Back to Index

What is Jakarta Security?

Think of security like a building's access system:

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.