API Security
Overview
This guide covers comprehensive API security implementation in the Ink platform, including authentication, authorization, rate limiting, input validation, and security best practices.
Target Audience: Backend developers and security engineers
Prerequisites: Understanding of Spring Security, JWT, and REST APIs
Estimated Time: 40-50 minutes
Prerequisites
- Spring Security knowledge
- REST API design principles
- JWT authentication understanding
- Completed Keycloak integration
Security Architecture
Installation Steps
1. Security Dependencies
<!-- filepath: /Users/jetstart/dev/jetrev/ink/pom.xml -->
<!-- ...existing code... -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.7.0</version>
</dependency>
</dependencies>
<!-- ...existing code... -->
2. Method Security Configuration
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/config/MethodSecurityConfig.java
@Configuration
@EnableMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}
}
3. Rate Limiting Configuration
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/config/RateLimitConfig.java
@Configuration
public class RateLimitConfig {
@Bean
public RateLimitInterceptor rateLimitInterceptor() {
return new RateLimitInterceptor();
}
@Bean
public WebMvcConfigurer rateLimitConfigurer(
RateLimitInterceptor rateLimitInterceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
};
}
}
Configuration
Controller Security Annotations
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/controller/UserController.java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Validated
public class UserController {
private final UserService userService;
@GetMapping("/me")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<UserDto> getCurrentUser() {
// ...existing code...
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Page<UserDto>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
// ...existing code...
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserDto> createUser(
@Valid @RequestBody CreateUserRequest request) {
// ...existing code...
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @userSecurity.isOwner(#id)")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
// ...existing code...
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
// ...existing code...
}
}
Custom Security Expression
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/security/UserSecurity.java
@Component("userSecurity")
@RequiredArgsConstructor
public class UserSecurity {
private final UserContextHolder userContextHolder;
public boolean isOwner(Long userId) {
UserContext currentUser = userContextHolder.getCurrentUser();
return currentUser != null &&
currentUser.getUserId().equals(userId.toString());
}
public boolean canAccessResource(Long resourceId) {
UserContext currentUser = userContextHolder.getCurrentUser();
if (currentUser == null) {
return false;
}
// Check ownership or admin role
return currentUser.getRoles().contains("ADMIN") ||
isResourceOwner(currentUser.getUserId(), resourceId);
}
private boolean isResourceOwner(String userId, Long resourceId) {
// ...existing code...
return false;
}
}
Input Validation
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/dto/CreateUserRequest.java
@Data
@Builder
public class CreateUserRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "Username can only contain alphanumeric characters, hyphens, and underscores")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
message = "Password must contain uppercase, lowercase, number, and special character"
)
private String password;
@NotNull(message = "Roles are required")
@Size(min = 1, message = "At least one role is required")
private List<String> roles;
}
Usage Examples
Rate Limiting Implementation
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/security/RateLimitInterceptor.java
@Slf4j
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
@Value("${ink.api.rate-limit.requests-per-minute:100}")
private int requestsPerMinute;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String key = getClientKey(request);
Bucket bucket = resolveBucket(key);
if (bucket.tryConsume(1)) {
return true;
}
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Rate limit exceeded. Try again later.");
return false;
}
private Bucket resolveBucket(String key) {
return cache.computeIfAbsent(key, k -> createBucket());
}
private Bucket createBucket() {
Bandwidth limit = Bandwidth.builder()
.capacity(requestsPerMinute)
.refillGreedy(requestsPerMinute, Duration.ofMinutes(1))
.build();
return Bucket.builder()
.addLimit(limit)
.build();
}
private String getClientKey(HttpServletRequest request) {
String userId = extractUserId(request);
return userId != null ? userId : request.getRemoteAddr();
}
private String extractUserId(HttpServletRequest request) {
// ...existing code...
return null;
}
}
API Key Authentication
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/security/ApiKeyAuthFilter.java
@Component
@RequiredArgsConstructor
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final ApiKeyService apiKeyService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && apiKeyService.isValid(apiKey)) {
Authentication auth = apiKeyService.authenticate(apiKey);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
Audit Logging
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/security/AuditAspect.java
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class AuditAspect {
private final AuditLogRepository auditLogRepository;
private final UserContextHolder userContextHolder;
@Around("@annotation(auditable)")
public Object auditMethod(ProceedingJoinPoint joinPoint, Auditable auditable)
throws Throwable {
UserContext user = userContextHolder.getCurrentUser();
String action = auditable.action();
AuditLog auditLog = AuditLog.builder()
.userId(user != null ? user.getUserId() : "anonymous")
.action(action)
.timestamp(Instant.now())
.method(joinPoint.getSignature().toShortString())
.build();
try {
Object result = joinPoint.proceed();
auditLog.setStatus("SUCCESS");
return result;
} catch (Exception e) {
auditLog.setStatus("FAILURE");
auditLog.setErrorMessage(e.getMessage());
throw e;
} finally {
auditLogRepository.save(auditLog);
}
}
}
Verification
Security Testing
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/security/SecurityTest.java
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class SecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "USER")
void testUserCanAccessOwnProfile() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk());
}
@Test
void testUnauthenticatedAccessDenied() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER")
void testUserCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void testAdminCanAccessAllEndpoints() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
}
Input Validation Testing
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/validation/ValidationTest.java
@SpringBootTest
class ValidationTest {
@Autowired
private Validator validator;
@Test
void testInvalidEmail() {
CreateUserRequest request = CreateUserRequest.builder()
.username("testuser")
.email("invalid-email")
.password("Password123!")
.roles(List.of("USER"))
.build();
Set<ConstraintViolation<CreateUserRequest>> violations =
validator.validate(request);
assertThat(violations).hasSize(1);
assertThat(violations.iterator().next().getMessage())
.contains("Email must be valid");
}
@Test
void testWeakPassword() {
CreateUserRequest request = CreateUserRequest.builder()
.username("testuser")
.email("test@example.com")
.password("weak")
.roles(List.of("USER"))
.build();
Set<ConstraintViolation<CreateUserRequest>> violations =
validator.validate(request);
assertThat(violations).isNotEmpty();
}
}
Troubleshooting
Common Security Issues
Issue: 403 Forbidden despite valid token
// Debug security context
@GetMapping("/debug/security")
public ResponseEntity<Map<String, Object>> debugSecurity() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Map<String, Object> debug = new HashMap<>();
debug.put("authenticated", auth.isAuthenticated());
debug.put("authorities", auth.getAuthorities());
debug.put("principal", auth.getPrincipal());
return ResponseEntity.ok(debug);
}
Issue: Rate limiting not working
// Add logging to rate limiter
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String key = getClientKey(request);
log.debug("Rate limit check for: {}", key);
Bucket bucket = resolveBucket(key);
boolean allowed = bucket.tryConsume(1);
log.debug("Rate limit result: {}, remaining: {}",
allowed, bucket.getAvailableTokens());
return allowed;
}
Code Examples
Custom Permission Evaluator
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/security/CustomPermissionEvaluator.java
@Component
@RequiredArgsConstructor
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final UserRepository userRepository;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
if (authentication == null || targetDomainObject == null) {
return false;
}
String targetType = targetDomainObject.getClass().getSimpleName();
return hasPrivilege(authentication, targetType, permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
if (authentication == null || targetType == null) {
return false;
}
return hasPrivilege(authentication, targetType, permission.toString());
}
private boolean hasPrivilege(Authentication auth,
String targetType,
String permission) {
// ...existing code...
return false;
}
}
CSRF Protection for APIs
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/config/CsrfConfig.java
@Configuration
public class CsrfConfig {
@Bean
public CsrfTokenRepository csrfTokenRepository() {
CookieCsrfTokenRepository repository =
CookieCsrfTokenRepository.withHttpOnlyFalse();
repository.setCookiePath("/");
return repository;
}
}
Best Practices
- Principle of Least Privilege: Grant minimum necessary permissions
- Defense in Depth: Layer multiple security controls
- Input Validation: Always validate and sanitize user input
- Rate Limiting: Prevent abuse with rate limits
- Audit Logging: Log all security-relevant events
- Secure Defaults: Use secure configurations by default
- Regular Updates: Keep dependencies updated
- HTTPS Only: Never transmit sensitive data over HTTP
- Token Expiration: Use short-lived tokens with refresh mechanism
- Error Handling: Don't expose sensitive information in errors
Performance Optimization
Caching Authorization Decisions
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/security/CachedAuthorizationService.java
@Service
@RequiredArgsConstructor
public class CachedAuthorizationService {
@Cacheable(value = "userPermissions", key = "#userId")
public Set<String> getUserPermissions(String userId) {
// ...existing code...
return Set.of();
}
@CacheEvict(value = "userPermissions", key = "#userId")
public void invalidateUserPermissions(String userId) {
// Called when user permissions change
}
}
Related Documentation
Additional Resources
Next Steps: Learn about Unit Testing to test security implementations.