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 :
@AuditLog annotation - Marks methods to be audited
AuditLogAspect - AOP aspect that intercepts annotated methods
AuditService - Application service for audit operations
ActivityLogDomain - Domain model for audit logs
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);
}
}
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
Parameter Type Description Default moduleString Filter by module name All modules dateDate (yyyy-MM-dd) Filter by date Today pageInteger Page number (0-based) 0 sizeInteger Items per page 20
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