Skip to main content

Overview

The Authorization Service includes a comprehensive audit logging system that automatically tracks user activities, system events, and security-relevant operations. The system uses Aspect-Oriented Programming (AOP) to intercept method calls and log them asynchronously without cluttering business logic.

Architecture

Components

Key Components:
  1. @AuditLog annotation - Marks methods to be audited
  2. AuditLogAspect - AOP aspect that intercepts annotated methods
  3. AuditService - Application service for audit operations
  4. ActivityLogDomain - Domain model for audit logs
  5. AuditRepositoryPort - Persistence interface

Hexagonal Architecture

The audit system follows the same hexagonal architecture:
audit/
├── domain/
│   ├── model/
│   │   └── ActivityLogDomain.java
│   └── port/
│       ├── in/  (AuditUseCasePort)
│       └── out/ (AuditRepositoryPort)
├── application/
│   └── services/
│       └── AuditService.java
└── adapter/
    ├── in/
    │   ├── aop/
    │   │   └── AuditLogAspect.java
    │   └── web/
    │       └── controller/
    │           └── AuditController.java
    └── out/
        └── jpa/
            ├── AuditRepositoryAdapter.java
            └── entity/
                └── ActivityLog.java

The @AuditLog Annotation

The @AuditLog annotation (shared/annotation/AuditLog.java:10) marks methods for auditing:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
    String module();   // Functional module (e.g., "Users", "Roles")
    String action();   // Action performed (e.g., "CREATE", "UPDATE")
}

Usage Example

@Service
public class UserService implements UserUseCasePort {
    
    @Override
    @AuditLog(module = "Users", action = "CREATE")
    public UserResponse create(CreateUserRequest request) {
        // Business logic to create user
        UserDomain user = // ... create user
        return userMapper.toResponse(user);
    }
    
    @Override
    @AuditLog(module = "Users", action = "ASSIGN_ROLE")
    public void assignRole(UUID userId, UUID roleId) {
        // Business logic to assign role
        UserDomain user = userRepository.findById(new UserId(userId))
            .orElseThrow();
        RoleDomain role = roleRepository.findById(new RoleId(roleId))
            .orElseThrow();
        
        user.addRole(role);
        userRepository.save(user);
    }
    
    @Override
    @AuditLog(module = "Users", action = "DELETE")
    public void deactivate(UUID id) {
        // Business logic to deactivate user
        userRepository.updateEnabled(new UserId(id), false);
    }
}

AuditLogAspect

The AuditLogAspect (audit/adapter/in/aop/AuditLogAspect.java:27) intercepts methods annotated with @AuditLog.

Aspect Configuration

@Aspect
@Component
@RequiredArgsConstructor
public class AuditLogAspect {
    
    private final AuditUseCasePort auditUseCasePort;
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @AfterReturning(pointcut = "@annotation(auditLog)", returning = "result")
    public void logActivitySuccess(JoinPoint joinPoint, 
                                   AuditLog auditLog, 
                                   Object result) {
        saveLog(joinPoint, auditLog, "SUCCESS", null);
    }
    
    @AfterThrowing(pointcut = "@annotation(auditLog)", throwing = "exception")
    public void logActivityFailure(JoinPoint joinPoint, 
                                   AuditLog auditLog, 
                                   Exception exception) {
        saveLog(joinPoint, auditLog, "FAILURE", exception.getMessage());
    }
}

What Gets Logged

Success Case (@AfterReturning):
  • Logs when the annotated method completes successfully
  • Status: SUCCESS
  • No error details
Failure Case (@AfterThrowing):
  • Logs when the annotated method throws an exception
  • Status: FAILURE
  • Includes error message

Log Creation

The saveLog method (audit/adapter/in/aop/AuditLogAspect.java:43-68) assembles the audit log:
@Async
public void saveLog(JoinPoint joinPoint, 
                    AuditLog auditLog, 
                    String status, 
                    String errorDetails) {
    try {
        // 1. Get current authenticated user
        String userId = getCurrentUser();
        
        // 2. Serialize method arguments
        String details = getMethodArgs(joinPoint);
        
        // 3. Get client IP address
        String ipAddress = getClientIp();
        
        // 4. Append error details if failure
        if (errorDetails != null) {
            details += " | Error: " + errorDetails;
        }
        
        // 5. Create domain object
        ActivityLogDomain log = ActivityLogDomain.builder()
            .userId(userId)
            .module(auditLog.module())
            .action(auditLog.action())
            .details(details)
            .ipAddress(ipAddress)
            .status(status)
            .timestamp(LocalDateTime.now())
            .build();
        
        // 6. Save asynchronously
        auditUseCasePort.logActivity(log);
        
    } catch (Exception e) {
        log.error("Error saving audit log", e);
    }
}

Context Extraction

Current User

Extracts the authenticated user from Spring Security context (audit/adapter/in/aop/AuditLogAspect.java:70-76):
private String getCurrentUser() {
    Authentication authentication = SecurityContextHolder
        .getContext()
        .getAuthentication();
    
    if (authentication != null && authentication.isAuthenticated()) {
        return authentication.getName();  // User email from JWT
    }
    return "ANONYMOUS";
}

Method Arguments

Serializes the first argument (typically the request DTO) to JSON (audit/adapter/in/aop/AuditLogAspect.java:78-90):
private String getMethodArgs(JoinPoint joinPoint) {
    try {
        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0) {
            // Serialize first argument (usually the request body)
            return objectMapper.writeValueAsString(args[0]);
        }
    } catch (Exception e) {
        log.warn("Could not serialize method arguments", e);
    }
    return "{}";
}
Example:
{
  "email": "john.doe@example.com",
  "name": "John",
  "lastName": "Doe"
}

Client IP Address

Extracts the client’s IP address, considering proxies (audit/adapter/in/aop/AuditLogAspect.java:92-114):
private String getClientIp() {
    try {
        ServletRequestAttributes attributes = 
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            
            // Check X-Forwarded-For header (for proxies/load balancers)
            String ip = request.getHeader("X-Forwarded-For");
            
            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();  // Direct connection
            }
            return ip;
        }
    } catch (Exception e) {
        log.warn("Could not get client IP", e);
    }
    return "UNKNOWN";
}

ActivityLogDomain

The domain model for audit logs:
public class ActivityLogDomain {
    private Long id;                  // Auto-generated ID
    private String userId;            // User email or "ANONYMOUS"
    private String module;            // Functional module
    private String action;            // Action performed
    private String details;           // JSON of method arguments + errors
    private String ipAddress;         // Client IP address
    private String status;            // "SUCCESS" or "FAILURE"
    private LocalDateTime timestamp;  // When the action occurred
}

AuditService

The application service (audit/application/services/AuditService.java:18) implements audit use cases.

Logging Activity

@Service
@RequiredArgsConstructor
public class AuditService implements AuditUseCasePort {
    
    private final AuditRepositoryPort auditRepositoryPort;
    
    @Override
    @Async  // Runs asynchronously to avoid blocking
    public void logActivity(ActivityLogDomain activityLog) {
        auditRepositoryPort.save(activityLog);
    }
}
Key Feature: The @Async annotation ensures audit logging doesn’t slow down the main business logic. Logs are saved in a separate thread pool.

Retrieving Logs

@Override
public PaginatedResponse<ActivityLogDomain> retrieveLogs(
    String module, 
    LocalDate date, 
    Pageable pageable
) {
    if (date == null) {
        date = LocalDate.now();
    }
    
    LocalDateTime start = date.atStartOfDay();
    LocalDateTime end = date.atTime(23, 59, 59);
    
    Page<ActivityLogDomain> page = auditRepositoryPort.findLogs(
        module, start, end, pageable
    );
    
    return new PaginatedResponse<>(
        page.getContent(),
        page.getNumber(),
        page.getSize(),
        page.getTotalElements(),
        page.getTotalPages(),
        page.isLast()
    );
}

Audit API

Audit logs can be queried via REST API:

Retrieve Logs

GET /api/audit/logs?module=Users&date=2026-03-04&page=0&size=20
Authorization: Bearer {token}
Response:
{
  "content": [
    {
      "id": 1,
      "userId": "admin@example.com",
      "module": "Users",
      "action": "CREATE",
      "details": "{\"email\":\"john.doe@example.com\",\"name\":\"John\",\"lastName\":\"Doe\"}",
      "ipAddress": "192.168.1.100",
      "status": "SUCCESS",
      "timestamp": "2026-03-04T10:15:30"
    },
    {
      "id": 2,
      "userId": "manager@example.com",
      "module": "Users",
      "action": "ASSIGN_ROLE",
      "details": "{\"userId\":\"550e8400-e29b-41d4-a716-446655440000\",\"roleId\":\"123e4567-e89b-12d3-a456-426614174000\"}",
      "ipAddress": "10.0.0.50",
      "status": "SUCCESS",
      "timestamp": "2026-03-04T11:20:45"
    },
    {
      "id": 3,
      "userId": "operator@example.com",
      "module": "Users",
      "action": "DELETE",
      "details": "{\"id\":\"123e4567-e89b-12d3-a456-426614174000\"} | Error: User not found",
      "ipAddress": "172.16.0.25",
      "status": "FAILURE",
      "timestamp": "2026-03-04T12:30:00"
    }
  ],
  "pageNumber": 0,
  "pageSize": 20,
  "totalElements": 3,
  "totalPages": 1,
  "last": true
}

Query Parameters

ParameterTypeDescriptionDefault
moduleStringFilter by module nameAll modules
dateDate (yyyy-MM-dd)Filter by dateToday
pageIntegerPage number (0-based)0
sizeIntegerItems per page20

Persistence

The AuditRepositoryAdapter (audit/adapter/out/jpa/AuditRepositoryAdapter.java) implements the persistence port:
@Component
@RequiredArgsConstructor
public class AuditRepositoryAdapter implements AuditRepositoryPort {
    
    private final AuditLogJPARepository auditLogJPARepository;
    
    @Override
    public ActivityLogDomain save(ActivityLogDomain domain) {
        ActivityLog entity = AuditMapper.fromDomain(domain);
        ActivityLog saved = auditLogJPARepository.save(entity);
        return AuditMapper.toDomain(saved);
    }
    
    @Override
    public Page<ActivityLogDomain> findLogs(
        String module,
        LocalDateTime start,
        LocalDateTime end,
        Pageable pageable
    ) {
        Page<ActivityLog> page = auditLogJPARepository
            .findByModuleAndTimestampBetween(module, start, end, pageable);
        
        return page.map(AuditMapper::toDomain);
    }
}
JPA Entity (ActivityLog):
@Entity
@Table(name = "activity_logs")
public class ActivityLog {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String userId;
    
    @Column(nullable = false)
    private String module;
    
    @Column(nullable = false)
    private String action;
    
    @Column(columnDefinition = "TEXT")
    private String details;
    
    private String ipAddress;
    
    @Column(nullable = false)
    private String status;
    
    @Column(nullable = false)
    private LocalDateTime timestamp;
}

Asynchronous Processing

Audit logging uses Spring’s @Async to avoid blocking (audit/application/services/AuditService.java:23):
@Async
public void logActivity(ActivityLogDomain activityLog) {
    auditRepositoryPort.save(activityLog);
}
Benefits:
  • Business logic doesn’t wait for log persistence
  • Improved response times
  • Audit failures don’t fail the main operation
Configuration (requires @EnableAsync on application class):
@SpringBootApplication
@EnableAsync
public class AutorizationModuleApplication {
    // ...
}

What Gets Audited

Typical operations that should be audited:

User Management

  • CREATE - User creation
  • UPDATE - User modification
  • DELETE - User deactivation
  • ASSIGN_ROLE - Role assignment
  • REVOKE_ROLE - Role revocation
  • ACTIVATE - Account activation
  • DEACTIVATE - Account deactivation

Role Management

  • CREATE - Role creation
  • UPDATE - Role modification
  • DELETE - Role deletion
  • ASSIGN_PERMISSION - Permission assignment
  • REVOKE_PERMISSION - Permission revocation

Authentication

  • LOGIN - Successful login
  • LOGIN_FAILED - Failed login attempt
  • LOGOUT - User logout

Permission Management

  • CREATE - Permission creation
  • UPDATE - Permission modification
  • DELETE - Permission deletion

Security Considerations

Sensitive Data: The audit system serializes method arguments to JSON. Be careful not to log sensitive data like passwords. Use DTOs that exclude sensitive fields.
Example:
// Bad - logs password
public record CreateUserRequest(
    String email,
    String password,  // Will be logged!
    String name
) {}

// Good - password excluded from serialization
public record CreateUserRequest(
    String email,
    
    @JsonIgnore
    String password,  // Won't be logged
    
    String name
) {}

Best Practices

Comprehensive Coverage

Audit all security-relevant operations: authentication, authorization changes, data modifications

Consistent Naming

Use consistent module and action names for easier filtering and reporting

Retention Policy

Implement log retention policies based on compliance requirements (e.g., keep for 90 days)

Monitoring

Set up alerts for suspicious patterns (e.g., multiple failed logins, mass deletions)

Compliance

The audit system helps meet compliance requirements:
  • GDPR: Track access to personal data
  • SOX: Audit financial system access
  • HIPAA: Log healthcare data access
  • PCI DSS: Monitor payment card data access

Next Steps

Audit API

Explore the audit log query endpoints

Authentication

Learn how user identity is captured

RBAC

Understand what operations can be audited

Configuration

Configure audit logging settings

Build docs developers (and LLMs) love