Lightweight Yet Secure: Implementing Robust Security in Javalin Java Applications

Javalin has emerged as a popular lightweight web framework for Java and Kotlin, known for its simplicity and performance. While it's minimal by design, building secure Javalin applications requires careful implementation of authentication, authorization, and security best practices. Let's explore how to build enterprise-grade security in Javalin applications.

Javalin Security Architecture

Javalin's minimalist approach means security is implemented through handlers, middleware, and extensions rather than built-in frameworks. This provides flexibility while requiring explicit security implementation.

Basic Security Setup

1. Dependencies Configuration

<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>5.6.2</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>

2. Security Configuration Class

public class SecurityConfig {
private final String JWT_SECRET = System.getenv("JWT_SECRET");
private final long JWT_EXPIRATION_MS = 86400000; // 24 hours
// Password hashing
public String hashPassword(String password) {
return BCrypt.withDefaults().hashToString(12, password.toCharArray());
}
public boolean verifyPassword(String password, String hashedPassword) {
BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), hashedPassword);
return result.verified;
}
// JWT Token generation and validation
public String generateToken(User user) {
return JWT.create()
.withSubject(user.getUsername())
.withClaim("userId", user.getId())
.withClaim("roles", user.getRoles())
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS))
.sign(Algorithm.HMAC512(JWT_SECRET));
}
public DecodedJWT validateToken(String token) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC512(JWT_SECRET))
.build();
return verifier.verify(token);
} catch (JWTVerificationException e) {
throw new AuthenticationException("Invalid JWT token", e);
}
}
public static SecurityConfig create() {
return new SecurityConfig();
}
}

Authentication Implementation

1. User Model and Service

public class User {
private Long id;
private String username;
private String email;
private String passwordHash;
private List<String> roles = new ArrayList<>();
private boolean enabled = true;
// Constructors, getters, setters
public User() {}
public User(String username, String email, String password, List<String> roles) {
this.username = username;
this.email = email;
this.roles = roles;
}
public boolean hasRole(String role) {
return roles.contains(role);
}
public boolean hasAnyRole(String... roles) {
return Arrays.stream(roles).anyMatch(this.roles::contains);
}
}
@Service
public class UserService {
private final Map<String, User> users = new ConcurrentHashMap<>();
private final SecurityConfig securityConfig;
public UserService(SecurityConfig securityConfig) {
this.securityConfig = securityConfig;
initializeDefaultUsers();
}
private void initializeDefaultUsers() {
// Create default admin user
User admin = new User("admin", "[email protected]", "admin123", 
List.of("ADMIN", "USER"));
admin.setPasswordHash(securityConfig.hashPassword("admin123"));
users.put(admin.getUsername(), admin);
// Create regular user
User user = new User("user", "[email protected]", "user123", List.of("USER"));
user.setPasswordHash(securityConfig.hashPassword("user123"));
users.put(user.getUsername(), user);
}
public Optional<User> findByUsername(String username) {
return Optional.ofNullable(users.get(username));
}
public User createUser(String username, String email, String password, List<String> roles) {
if (users.containsKey(username)) {
throw new UserAlreadyExistsException("User already exists: " + username);
}
User user = new User(username, email, password, roles);
user.setPasswordHash(securityConfig.hashPassword(password));
users.put(username, user);
return user;
}
public boolean validateCredentials(String username, String password) {
return findByUsername(username)
.map(user -> securityConfig.verifyPassword(password, user.getPasswordHash()))
.orElse(false);
}
}

2. Authentication Handlers

public class AuthHandler {
private final UserService userService;
private final SecurityConfig securityConfig;
private final ObjectMapper objectMapper;
public AuthHandler(UserService userService, SecurityConfig securityConfig) {
this.userService = userService;
this.securityConfig = securityConfig;
this.objectMapper = new ObjectMapper();
}
public void register(Context ctx) {
try {
RegisterRequest request = objectMapper.readValue(ctx.body(), RegisterRequest.class);
// Validate input
if (request.getUsername() == null || request.getPassword() == null) {
ctx.status(400).json(Map.of("error", "Username and password are required"));
return;
}
User user = userService.createUser(
request.getUsername(),
request.getEmail(),
request.getPassword(),
List.of("USER") // Default role
);
ctx.status(201).json(Map.of(
"message", "User registered successfully",
"userId", user.getId()
));
} catch (UserAlreadyExistsException e) {
ctx.status(409).json(Map.of("error", "Username already exists"));
} catch (Exception e) {
ctx.status(500).json(Map.of("error", "Registration failed"));
}
}
public void login(Context ctx) {
try {
LoginRequest request = objectMapper.readValue(ctx.body(), LoginRequest.class);
if (userService.validateCredentials(request.getUsername(), request.getPassword())) {
User user = userService.findByUsername(request.getUsername()).orElseThrow();
String token = securityConfig.generateToken(user);
ctx.json(Map.of(
"token", token,
"type", "Bearer",
"expiresIn", 86400,
"user", Map.of(
"username", user.getUsername(),
"email", user.getEmail(),
"roles", user.getRoles()
)
));
} else {
ctx.status(401).json(Map.of("error", "Invalid credentials"));
}
} catch (Exception e) {
ctx.status(500).json(Map.of("error", "Login failed"));
}
}
public void logout(Context ctx) {
// In stateless JWT, logout is handled client-side by token removal
// For server-side token invalidation, you'd need a token blacklist
ctx.json(Map.of("message", "Logged out successfully"));
}
}
// DTOs for authentication
class LoginRequest {
private String username;
private String password;
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
class RegisterRequest {
private String username;
private String email;
private String password;
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}

JWT Authentication Middleware

public class JwtAuthMiddleware {
private final SecurityConfig securityConfig;
private final UserService userService;
public JwtAuthMiddleware(SecurityConfig securityConfig, UserService userService) {
this.securityConfig = securityConfig;
this.userService = userService;
}
public Handler create() {
return ctx -> {
String authHeader = ctx.header("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
ctx.status(401).json(Map.of("error", "Missing or invalid Authorization header"));
return;
}
String token = authHeader.substring(7);
try {
DecodedJWT decodedJWT = securityConfig.validateToken(token);
String username = decodedJWT.getSubject();
User user = userService.findByUsername(username)
.orElseThrow(() -> new AuthenticationException("User not found"));
// Attach user to context for use in endpoints
ctx.attribute("user", user);
} catch (AuthenticationException e) {
ctx.status(401).json(Map.of("error", "Invalid token"));
return;
}
};
}
}

Role-Based Access Control

1. Role Authorization Handler

public class RoleAuthHandler {
public static Handler requireRole(String... roles) {
return ctx -> {
User user = ctx.attribute("user");
if (user == null) {
ctx.status(401).json(Map.of("error", "Authentication required"));
return;
}
boolean hasRequiredRole = Arrays.stream(roles)
.anyMatch(user::hasRole);
if (!hasRequiredRole) {
ctx.status(403).json(Map.of(
"error", "Insufficient permissions",
"requiredRoles", Arrays.asList(roles),
"userRoles", user.getRoles()
));
return;
}
};
}
public static Handler requireAnyRole(String... roles) {
return requireRole(roles);
}
public static Handler requireAllRoles(String... roles) {
return ctx -> {
User user = ctx.attribute("user");
if (user == null) {
ctx.status(401).json(Map.of("error", "Authentication required"));
return;
}
boolean hasAllRoles = Arrays.stream(roles)
.allMatch(user::hasRole);
if (!hasAllRoles) {
ctx.status(403).json(Map.of(
"error", "Insufficient permissions",
"requiredRoles", Arrays.asList(roles),
"userRoles", user.getRoles()
));
return;
}
};
}
}

2. Permission-Based Authorization

public class PermissionHandler {
private final Map<String, Set<String>> rolePermissions = Map.of(
"ADMIN", Set.of("users:read", "users:write", "users:delete", "products:read", "products:write", "products:delete"),
"MANAGER", Set.of("users:read", "products:read", "products:write"),
"USER", Set.of("products:read", "orders:read", "orders:write")
);
public static Handler requirePermission(String permission) {
return ctx -> {
User user = ctx.attribute("user");
if (user == null) {
ctx.status(401).json(Map.of("error", "Authentication required"));
return;
}
boolean hasPermission = user.getRoles().stream()
.flatMap(role -> getPermissionsForRole(role).stream())
.anyMatch(perm -> perm.equals(permission));
if (!hasPermission) {
ctx.status(403).json(Map.of(
"error", "Insufficient permissions",
"requiredPermission", permission
));
return;
}
};
}
private Set<String> getPermissionsForRole(String role) {
return rolePermissions.getOrDefault(role, Set.of());
}
}

Secure Javalin Application Setup

public class SecureJavalinApp {
private final SecurityConfig securityConfig;
private final UserService userService;
private final JwtAuthMiddleware jwtAuthMiddleware;
private final AuthHandler authHandler;
public SecureJavalinApp() {
this.securityConfig = SecurityConfig.create();
this.userService = new UserService(securityConfig);
this.jwtAuthMiddleware = new JwtAuthMiddleware(securityConfig, userService);
this.authHandler = new AuthHandler(userService, securityConfig);
}
public Javalin createApp() {
return Javalin.create(config -> {
config.http.defaultContentType = "application/json";
config.bundledPlugins.enableDevLogging();
// Security headers
config.plugins.enableCors(cors -> {
cors.addRule(CorsPluginConfig.CorsRule::anyOrigin);
});
})
.beforeMatched(ctx -> {
// Add security headers to all responses
ctx.header("X-Content-Type-Options", "nosniff");
ctx.header("X-Frame-Options", "DENY");
ctx.header("X-XSS-Protection", "1; mode=block");
ctx.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
})
// Public routes
.post("/api/auth/register", authHandler::register)
.post("/api/auth/login", authHandler::login)
.post("/api/auth/logout", authHandler::logout)
// Protected routes - require authentication
.before("/api/secure/*", jwtAuthMiddleware.create())
// User management (Admin only)
.get("/api/secure/users", 
ctx -> {
// Get users logic
List<User> users = userService.getAllUsers();
ctx.json(users);
},
RoleAuthHandler.requireRole("ADMIN")
)
// Product routes with different permission levels
.get("/api/secure/products", 
ctx -> {
// Get products logic
ctx.json(List.of("product1", "product2"));
},
PermissionHandler.requirePermission("products:read")
)
.post("/api/secure/products", 
ctx -> {
// Create product logic
ctx.status(201).json(Map.of("message", "Product created"));
},
RoleAuthHandler.requireAnyRole("ADMIN", "MANAGER")
)
.delete("/api/secure/products/{id}", 
ctx -> {
// Delete product logic
String productId = ctx.pathParam("id");
ctx.json(Map.of("message", "Product " + productId + " deleted"));
},
RoleAuthHandler.requireRole("ADMIN")
)
// User profile (accessible by own user or admin)
.get("/api/secure/profile/{username}", 
ctx -> {
String username = ctx.pathParam("username");
User currentUser = ctx.attribute("user");
// Users can only access their own profile, unless they're admin
if (!currentUser.getUsername().equals(username) && 
!currentUser.hasRole("ADMIN")) {
ctx.status(403).json(Map.of("error", "Can only access own profile"));
return;
}
User profileUser = userService.findByUsername(username).orElseThrow();
ctx.json(Map.of(
"username", profileUser.getUsername(),
"email", profileUser.getEmail()
// Don't expose sensitive data
));
}
)
// Error handling
.exception(AuthenticationException.class, (e, ctx) -> {
ctx.status(401).json(Map.of("error", "Authentication failed: " + e.getMessage()));
})
.exception(AuthorizationException.class, (e, ctx) -> {
ctx.status(403).json(Map.of("error", "Access denied: " + e.getMessage()));
})
.exception(Exception.class, (e, ctx) -> {
ctx.status(500).json(Map.of("error", "Internal server error"));
});
}
public static void main(String[] args) {
SecureJavalinApp app = new SecureJavalinApp();
app.createApp().start(7000);
}
}

Advanced Security Features

1. Rate Limiting Middleware

public class RateLimitMiddleware {
private final Map<String, RequestCounter> requestCounts = new ConcurrentHashMap<>();
private final int maxRequestsPerMinute = 100;
public Handler create() {
return ctx -> {
String clientIp = ctx.ip();
String key = clientIp + ":" + ctx.method() + ":" + ctx.path();
RequestCounter counter = requestCounts.computeIfAbsent(
key, k -> new RequestCounter());
if (!counter.allowRequest()) {
ctx.status(429).json(Map.of(
"error", "Rate limit exceeded",
"retryAfter", counter.getRetryAfterSeconds() + " seconds"
));
return;
}
};
}
private class RequestCounter {
private final Queue<Long> requests = new LinkedList<>();
public synchronized boolean allowRequest() {
long currentTime = System.currentTimeMillis();
long oneMinuteAgo = currentTime - 60000;
// Remove old requests
while (!requests.isEmpty() && requests.peek() < oneMinuteAgo) {
requests.poll();
}
if (requests.size() >= maxRequestsPerMinute) {
return false;
}
requests.offer(currentTime);
return true;
}
public int getRetryAfterSeconds() {
if (requests.isEmpty()) return 0;
long oldestRequest = requests.peek();
return (int) ((oldestRequest + 61000 - System.currentTimeMillis()) / 1000);
}
}
}

2. Input Validation and Sanitization

public class ValidationHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
public static Handler validateJson(Class<?> clazz) {
return ctx -> {
try {
String body = ctx.body();
if (body == null || body.trim().isEmpty()) {
ctx.status(400).json(Map.of("error", "Request body is required"));
return;
}
Object value = new ObjectMapper().readValue(body, clazz);
ctx.attribute("validatedBody", value);
} catch (Exception e) {
ctx.status(400).json(Map.of("error", "Invalid JSON: " + e.getMessage()));
return;
}
};
}
public static Handler sanitizeInput() {
return ctx -> {
// Sanitize path parameters
ctx.pathParamMap().replaceAll((k, v) -> sanitize(v));
// Sanitize query parameters
ctx.queryParamMap().replaceAll((k, v) -> 
v.stream().map(ValidationHandler::sanitize).collect(Collectors.toList()));
};
}
private static String sanitize(String input) {
if (input == null) return null;
// Basic HTML tag removal
return input.replaceAll("<[^>]*>", "")
.replaceAll("javascript:", "")
.replaceAll("on\\w+=", "");
}
}

3. CSRF Protection

public class CsrfProtectionHandler {
private final Set<String> safeMethods = Set.of("GET", "HEAD", "OPTIONS");
public Handler create() {
return ctx -> {
if (safeMethods.contains(ctx.method())) {
return;
}
String csrfToken = ctx.header("X-CSRF-Token");
String sessionToken = ctx.sessionAttribute("csrfToken");
if (csrfToken == null || !csrfToken.equals(sessionToken)) {
ctx.status(403).json(Map.of("error", "CSRF token validation failed"));
return;
}
};
}
public Handler generateCsrfToken() {
return ctx -> {
if (ctx.method().equals("GET")) {
String token = generateRandomToken();
ctx.sessionAttribute("csrfToken", token);
ctx.header("X-CSRF-Token", token);
}
};
}
private String generateRandomToken() {
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
}

Testing Security Implementation

class JavalinSecurityTest {
private Javalin app;
private final String BASE_URL = "http://localhost:7001";
@BeforeEach
void setUp() {
SecureJavalinApp appBuilder = new SecureJavalinApp();
this.app = appBuilder.createApp();
app.start(7001);
}
@AfterEach
void tearDown() {
app.stop();
}
@Test
void shouldRejectUnauthenticatedAccess() {
given()
.contentType(ContentType.JSON)
.when()
.get(BASE_URL + "/api/secure/products")
.then()
.statusCode(401);
}
@Test
void shouldAuthenticateWithValidCredentials() {
String token = given()
.contentType(ContentType.JSON)
.body("{\"username\": \"user\", \"password\": \"user123\"}")
.when()
.post(BASE_URL + "/api/auth/login")
.then()
.statusCode(200)
.extract()
.path("token");
assertNotNull(token);
}
@Test
void shouldEnforceRoleBasedAccess() {
String userToken = loginAndGetToken("user", "user123");
given()
.header("Authorization", "Bearer " + userToken)
.contentType(ContentType.JSON)
.when()
.get(BASE_URL + "/api/secure/users")
.then()
.statusCode(403);
}
private String loginAndGetToken(String username, String password) {
return given()
.contentType(ContentType.JSON)
.body(String.format("{\"username\": \"%s\", \"password\": \"%s\"}", username, password))
.when()
.post(BASE_URL + "/api/auth/login")
.then()
.extract()
.path("token");
}
}

Security Best Practices for Javalin

  1. Use HTTPS - Always in production
  2. Secure Headers - Implement CSP, HSTS, and other security headers
  3. Input Validation - Validate and sanitize all user inputs
  4. Password Hashing - Use bcrypt with appropriate work factors
  5. JWT Security - Use strong secrets, short expiration times
  6. Rate Limiting - Prevent brute force and DoS attacks
  7. Dependency Scanning - Regularly update dependencies for security patches
  8. Logging and Monitoring - Track authentication attempts and security events

Conclusion

Building secure Javalin applications requires a deliberate approach to authentication, authorization, and security middleware. While Javalin doesn't include built-in security features like Spring Security, its flexible handler-based architecture allows for clean, customizable security implementations.

Key advantages of this approach:

  • Lightweight - Minimal overhead compared to full security frameworks
  • Flexible - Customize security to match exact requirements
  • Transparent - Clear control flow and easy debugging
  • Performant - Efficient security checks with minimal overhead

By implementing the patterns shown here—JWT authentication, role-based access control, input validation, and security middleware—you can build robust, secure Javalin applications that meet enterprise security standards while maintaining the framework's simplicity and performance benefits.


Leave a Reply

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


Macro Nepal Helper