<\!DOCTYPE html> Security & Auth - Dione Developer Course

🎯 What You'll Learn

  • How Dione's custom authentication system actually works
  • Session management and security patterns
  • Multi-tenant security isolation mechanisms
  • Financial compliance requirements and implementation
  • Common security pitfalls and how to avoid them

Security Architecture

⚠️ Financial Services Security

This is a financial services platform handling real money and regulated transactions. Security isn't just about preventing hacking - it's about compliance, audit trails, and regulatory requirements.

Security Layers Overview

Dione Security Architecture
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 Network Layer                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Firewall  β”‚    β”‚ Load Balancerβ”‚   β”‚  SSL/TLS    β”‚  β”‚
β”‚  β”‚   Rules     β”‚    β”‚  (Rate Limit) β”‚   β”‚ Termination β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Application Security Layer                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Custom Auth β”‚    β”‚  Session    β”‚   β”‚   RBAC      β”‚  β”‚
β”‚  β”‚  System     β”‚    β”‚ Management  β”‚   β”‚ Authorizationβ”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               Data Security Layer                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Encryption  β”‚    β”‚ Multi-Tenantβ”‚   β”‚ Audit       β”‚  β”‚
β”‚  β”‚ at Rest     β”‚    β”‚ Isolation   β”‚   β”‚ Logging     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Security Principles

πŸ”

Defense in Depth

Multiple security layers - network, application, and data protection

🏒

Multi-Tenant Isolation

Complete data isolation between organizations using Organization ID

πŸ“‹

Compliance First

Built to meet PCI DSS, AML, and financial regulatory requirements

πŸ“Š

Complete Audit Trail

Every action is logged with user context for regulatory compliance

Authentication System

Dione implements a custom authentication system tailored for multi-tenant financial services:

Authentication Flow

LoginAndProfileHandlerImpl.java - Complete Authentication
@Service
public class LoginAndProfileHandlerImpl implements LoginAndProfileHandlerInterface {
    
    @Autowired
    private NGOPCusDaoInterface nGOPCusDaoRef;
    
    @Autowired
    private CacheDataConsumer cacheDataConsumer;
    
    @Autowired
    private AuditService auditService;
    
    public PojoMap createNGOPSession(Holder responseStatusHeader,
                                    AuthRequest authRequest, String locale, String source) {
        
        String email = authRequest.getEmail();
        String orgCode = authRequest.getOrgCode();
        
        LOG.info("Authentication attempt: {} for org: {} from source: {}", email, orgCode, source);
        
        try {
            // 1. Get organization ID from org code
            Integer orgId = getOrganizationId(orgCode);
            if (orgId == null) {
                auditService.logFailedLogin(email, orgCode, "INVALID_ORG", source);
                throw new NGOPBaseException("Invalid organization", "INVALID_ORG");
            }
            
            // 2. Find customer by email and organization
            Customer customer = nGOPCusDaoRef.getCustomerByEmail(email, orgId);
            if (customer == null) {
                auditService.logFailedLogin(email, orgCode, "USER_NOT_FOUND", source);
                throw new NGOPBaseException("Invalid credentials", "AUTH_FAILED");
            }
            
            // 3. Check account status
            if (!"ACTIVE".equals(customer.getAccountStatus())) {
                auditService.logFailedLogin(email, orgCode, "ACCOUNT_" + customer.getAccountStatus(), source);
                throw new NGOPBaseException("Account is " + customer.getAccountStatus().toLowerCase(), 
                                           "ACCOUNT_" + customer.getAccountStatus());
            }
            
            // 4. Verify password
            if (!verifyPassword(authRequest.getPassword(), customer.getPasswordHash(), customer.getSalt())) {
                auditService.logFailedLogin(email, orgCode, "INVALID_PASSWORD", source);
                // Increment failed login attempts
                incrementFailedLoginAttempts(customer);
                throw new NGOPBaseException("Invalid credentials", "AUTH_FAILED");
            }
            
            // 5. Check for too many failed attempts
            if (customer.getFailedLoginAttempts() >= MAX_FAILED_ATTEMPTS) {
                auditService.logFailedLogin(email, orgCode, "ACCOUNT_LOCKED", source);
                throw new NGOPBaseException("Account locked due to multiple failed attempts", "ACCOUNT_LOCKED");
            }
            
            // 6. Reset failed login attempts on successful login
            resetFailedLoginAttempts(customer);
            
            // 7. Create session data
            String sessionId = SessionManager.generateSessionId();
            PojoMap sessionData = new PojoMap();
            sessionData.put("sessionID", sessionId);
            sessionData.put("customerID", customer.getCustomerId());
            sessionData.put("orgID", orgId);
            sessionData.put("orgCode", orgCode);
            sessionData.put("email", customer.getEmail());
            sessionData.put("firstName", customer.getFirstName());
            sessionData.put("lastName", customer.getLastName());
            sessionData.put("kycStatus", customer.getKycStatus());
            sessionData.put("accountStatus", customer.getAccountStatus());
            sessionData.put("lastActivity", System.currentTimeMillis());
            sessionData.put("loginTime", System.currentTimeMillis());
            sessionData.put("source", source);
            sessionData.put("locale", locale);
            
            // 8. Store session in cache with timeout
            cacheDataConsumer.put(sessionId, sessionData, SESSION_TIMEOUT_SECONDS);
            
            // 9. Update customer's last login
            customer.setLastLoginDate(new Date());
            customer.setLastLoginSource(source);
            nGOPCusDaoRef.updateCustomer(customer);
            
            // 10. Audit successful login
            auditService.logSuccessfulLogin(customer.getCustomerId(), orgId, source);
            
            LOG.info("Successful authentication for customer: {} org: {}", customer.getCustomerId(), orgCode);
            
            return sessionData;
            
        } catch (NGOPBaseException e) {
            throw e;
        } catch (Exception e) {
            LOG.error("System error during authentication for: " + email, e);
            auditService.logFailedLogin(email, orgCode, "SYSTEM_ERROR", source);
            throw new NGOPBaseException("System error during authentication", "SYSTEM_ERROR");
        }
    }
    
    private boolean verifyPassword(String plainPassword, String hashedPassword, String salt) {
        try {
            // Use PBKDF2 with SHA-256
            String computedHash = PBKDF2Util.hash(plainPassword, salt, PBKDF2_ITERATIONS);
            return computedHash.equals(hashedPassword);
        } catch (Exception e) {
            LOG.error("Password verification error", e);
            return false;
        }
    }
    
    private void incrementFailedLoginAttempts(Customer customer) {
        customer.setFailedLoginAttempts(customer.getFailedLoginAttempts() + 1);
        customer.setLastFailedLogin(new Date());
        nGOPCusDaoRef.updateCustomer(customer);
    }
}

Password Security

PBKDF2Util.java - Password Hashing
public class PBKDF2Util {
    
    private static final int PBKDF2_ITERATIONS = 10000;
    private static final int SALT_LENGTH = 32;
    private static final int HASH_LENGTH = 32;
    
    public static String generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[SALT_LENGTH];
        random.nextBytes(salt);
        return Base64.getEncoder().encodeToString(salt);
    }
    
    public static String hash(String password, String salt, int iterations) {
        try {
            byte[] saltBytes = Base64.getDecoder().decode(salt);
            
            PBEKeySpec spec = new PBEKeySpec(
                password.toCharArray(), 
                saltBytes, 
                iterations, 
                HASH_LENGTH * 8
            );
            
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            SecretKey key = factory.generateSecret(spec);
            
            return Base64.getEncoder().encodeToString(key.getEncoded());
            
        } catch (Exception e) {
            throw new RuntimeException("Password hashing failed", e);
        }
    }
    
    public static boolean verify(String password, String hash, String salt) {
        String computedHash = hash(password, salt, PBKDF2_ITERATIONS);
        return computedHash.equals(hash);
    }
}

Session Management

Session Storage with Infinispan

Sessions are stored in Infinispan cache with automatic expiration:

SessionManager.java - Session Operations
@Component
public class SessionManager {
    
    @Autowired
    private CacheDataConsumer cacheDataConsumer;
    
    @Autowired
    private AuditService auditService;
    
    private static final int SESSION_TIMEOUT_SECONDS = 1800; // 30 minutes
    private static final int SESSION_WARNING_SECONDS = 300;  // 5 minutes before expiry
    
    public static String generateSessionId() {
        return UUID.randomUUID().toString() + "-" + System.currentTimeMillis();
    }
    
    public PojoMap validateSession(String sessionId) {
        if (StringUtils.isEmpty(sessionId)) {
            throw new NGOPBaseException("Session ID required", "SESSION_REQUIRED");
        }
        
        try {
            // Get session from cache
            PojoMap sessionData = cacheDataConsumer.get(sessionId);
            
            if (sessionData == null) {
                auditService.logSessionEvent(sessionId, "SESSION_NOT_FOUND");
                throw new NGOPBaseException("Session expired or invalid", "SESSION_INVALID");
            }
            
            // Check session timeout
            Long lastActivity = sessionData.getLong("lastActivity");
            long currentTime = System.currentTimeMillis();
            
            if (currentTime - lastActivity > (SESSION_TIMEOUT_SECONDS * 1000)) {
                // Session expired
                cacheDataConsumer.remove(sessionId);
                auditService.logSessionEvent(sessionId, "SESSION_EXPIRED");
                throw new NGOPBaseException("Session expired", "SESSION_EXPIRED");
            }
            
            // Update last activity
            sessionData.put("lastActivity", currentTime);
            cacheDataConsumer.put(sessionId, sessionData, SESSION_TIMEOUT_SECONDS);
            
            // Check if session is close to expiry (for warning)
            if (currentTime - lastActivity > ((SESSION_TIMEOUT_SECONDS - SESSION_WARNING_SECONDS) * 1000)) {
                sessionData.put("sessionWarning", true);
            }
            
            return sessionData;
            
        } catch (NGOPBaseException e) {
            throw e;
        } catch (Exception e) {
            LOG.error("Session validation error for: " + sessionId, e);
            throw new NGOPBaseException("Session validation failed", "SESSION_ERROR");
        }
    }
    
    public void invalidateSession(String sessionId, String reason) {
        try {
            PojoMap sessionData = cacheDataConsumer.get(sessionId);
            if (sessionData != null) {
                Integer customerId = sessionData.getInteger("customerID");
                auditService.logSessionInvalidation(customerId, sessionId, reason);
            }
            
            cacheDataConsumer.remove(sessionId);
            LOG.info("Session invalidated: {} reason: {}", sessionId, reason);
            
        } catch (Exception e) {
            LOG.error("Error invalidating session: " + sessionId, e);
        }
    }
    
    public void extendSession(String sessionId) {
        try {
            PojoMap sessionData = validateSession(sessionId);
            sessionData.put("lastActivity", System.currentTimeMillis());
            cacheDataConsumer.put(sessionId, sessionData, SESSION_TIMEOUT_SECONDS);
            
        } catch (Exception e) {
            LOG.warn("Failed to extend session: " + sessionId, e);
        }
    }
    
    public List getActiveSessions(Integer customerId) {
        // This would be expensive in production - only for admin/debugging
        List activeSessions = new ArrayList<>();
        
        try {
            Set allKeys = cacheDataConsumer.getAllKeys();
            for (String key : allKeys) {
                PojoMap sessionData = cacheDataConsumer.get(key);
                if (sessionData != null && customerId.equals(sessionData.getInteger("customerID"))) {
                    activeSessions.add(key);
                }
            }
        } catch (Exception e) {
            LOG.error("Error getting active sessions for customer: " + customerId, e);
        }
        
        return activeSessions;
    }
}

Session Security Features

  • Secure Session IDs: UUID + timestamp for uniqueness
  • Automatic Expiration: 30-minute timeout with activity extension
  • Concurrent Session Limiting: Prevent multiple active sessions
  • Session Hijacking Protection: IP address validation
  • Audit Trail: All session events logged

Authorization Patterns

Role-Based Access Control

AuthorizationService.java - RBAC Implementation
@Service
public class AuthorizationService {
    
    public boolean hasPermission(PojoMap sessionData, String permission) {
        try {
            Integer customerId = sessionData.getInteger("customerID");
            Integer orgId = sessionData.getInteger("orgID");
            
            // Get customer roles
            List roles = getRolesForCustomer(customerId, orgId);
            
            // Check if any role has the required permission
            for (String role : roles) {
                if (roleHasPermission(role, permission, orgId)) {
                    return true;
                }
            }
            
            return false;
            
        } catch (Exception e) {
            LOG.error("Authorization check failed for permission: " + permission, e);
            return false; // Fail secure
        }
    }
    
    public void checkPermission(PojoMap sessionData, String permission) {
        if (!hasPermission(sessionData, permission)) {
            Integer customerId = sessionData.getInteger("customerID");
            auditService.logAccessDenied(customerId, permission);
            throw new NGOPBaseException("Access denied", "ACCESS_DENIED");
        }
    }
    
    private List getRolesForCustomer(Integer customerId, Integer orgId) {
        // Standard roles in Dione
        List roles = new ArrayList<>();
        
        Customer customer = customerDAO.getCustomerById(customerId);
        if (customer != null) {
            // Basic customer role
            roles.add("CUSTOMER");
            
            // KYC-based roles
            if ("VERIFIED".equals(customer.getKycStatus())) {
                roles.add("VERIFIED_CUSTOMER");
            }
            
            // Account status roles
            if ("PREMIUM".equals(customer.getAccountTier())) {
                roles.add("PREMIUM_CUSTOMER");
            }
            
            // Check for admin roles
            CustomerRole adminRole = customerRoleDAO.getAdminRole(customerId, orgId);
            if (adminRole != null) {
                roles.add(adminRole.getRoleName());
            }
        }
        
        return roles;
    }
    
    private boolean roleHasPermission(String role, String permission, Integer orgId) {
        // Permission matrix - hardcoded for performance
        Map> rolePermissions = getRolePermissions(orgId);
        
        List permissions = rolePermissions.get(role);
        return permissions != null && permissions.contains(permission);
    }
    
    private Map> getRolePermissions(Integer orgId) {
        Map> permissions = new HashMap<>();
        
        // Customer permissions
        permissions.put("CUSTOMER", Arrays.asList(
            "VIEW_PROFILE", "UPDATE_PROFILE", "VIEW_ACCOUNTS", 
            "CREATE_PAYMENT", "VIEW_PAYMENTS", "VIEW_RATES"
        ));
        
        // Verified customer additional permissions
        permissions.put("VERIFIED_CUSTOMER", Arrays.asList(
            "VIEW_PROFILE", "UPDATE_PROFILE", "VIEW_ACCOUNTS", 
            "CREATE_PAYMENT", "VIEW_PAYMENTS", "VIEW_RATES",
            "LARGE_PAYMENTS", "INTERNATIONAL_TRANSFERS"
        ));
        
        // Premium customer permissions
        permissions.put("PREMIUM_CUSTOMER", Arrays.asList(
            "VIEW_PROFILE", "UPDATE_PROFILE", "VIEW_ACCOUNTS", 
            "CREATE_PAYMENT", "VIEW_PAYMENTS", "VIEW_RATES",
            "LARGE_PAYMENTS", "INTERNATIONAL_TRANSFERS",
            "PREFERRED_RATES", "PRIORITY_SUPPORT"
        ));
        
        // Admin permissions
        permissions.put("CUSTOMER_ADMIN", Arrays.asList(
            "VIEW_ALL_CUSTOMERS", "UPDATE_CUSTOMER_STATUS", 
            "VIEW_ALL_PAYMENTS", "CANCEL_PAYMENTS", "MANUAL_RATES"
        ));
        
        // Organization-specific permissions
        if (orgId == 1) { // Currencies Direct
            permissions.get("VERIFIED_CUSTOMER").add("CD_SPECIAL_RATES");
        }
        
        return permissions;
    }
}

Multi-Tenant Security

Organization ID Isolation

The foundation of Dione's multi-tenant security is Organization ID isolation:

MultiTenantSecurityInterceptor.java
@Component
public class MultiTenantSecurityInterceptor {
    
    /**
     * Ensures all database queries include organization isolation
     */
    public void validateOrganizationAccess(Integer requestedOrgId, PojoMap sessionData) {
        Integer sessionOrgId = sessionData.getInteger("orgID");
        
        if (!sessionOrgId.equals(requestedOrgId)) {
            Integer customerId = sessionData.getInteger("customerID");
            auditService.logSecurityViolation(customerId, 
                "ORG_ACCESS_VIOLATION", 
                "Attempted access to org " + requestedOrgId + " from session org " + sessionOrgId);
            
            throw new NGOPBaseException("Access denied to organization data", "ORG_ACCESS_DENIED");
        }
    }
    
    /**
     * Automatically adds organization filter to all queries
     */
    public String addOrganizationFilter(String baseQuery, Integer orgId) {
        // Add WHERE clause for organization isolation
        if (baseQuery.toUpperCase().contains("WHERE")) {
            return baseQuery + " AND organization_id = " + orgId;
        } else {
            return baseQuery + " WHERE organization_id = " + orgId;
        }
    }
    
    /**
     * Validates that a customer belongs to the session organization
     */
    public void validateCustomerOrganization(Integer customerId, PojoMap sessionData) {
        Integer orgId = sessionData.getInteger("orgID");
        
        Customer customer = customerDAO.getCustomerById(customerId);
        if (customer == null || !orgId.equals(customer.getOrganizationId())) {
            auditService.logSecurityViolation(
                sessionData.getInteger("customerID"),
                "CUSTOMER_ORG_VIOLATION",
                "Attempted access to customer " + customerId + " from wrong organization"
            );
            
            throw new NGOPBaseException("Customer not found", "CUSTOMER_NOT_FOUND");
        }
    }
}

Data Isolation Examples

Organization-Aware DAO Pattern
@Repository
public class CustomerDAOImpl implements CustomerDAO {
    
    // CORRECT - Always includes organization filter
    public List getCustomersByStatus(String status, Integer orgId) {
        String query = "SELECT * FROM customers WHERE account_status = ? AND organization_id = ?";
        return jdbcTemplate.query(query, 
            new Object[]{status, orgId}, 
            new CustomerRowMapper());
    }
    
    // CORRECT - Organization ID validation
    public Customer getCustomerById(Integer customerId, Integer orgId) {
        String query = "SELECT * FROM customers WHERE customer_id = ? AND organization_id = ?";
        List results = jdbcTemplate.query(query, 
            new Object[]{customerId, orgId}, 
            new CustomerRowMapper());
        
        return results.isEmpty() ? null : results.get(0);
    }
    
    // WRONG - Missing organization filter (security vulnerability)
    public Customer getCustomerByIdUnsafe(Integer customerId) {
        String query = "SELECT * FROM customers WHERE customer_id = ?";
        // This would return customers from ANY organization!
        List results = jdbcTemplate.query(query, 
            new Object[]{customerId}, 
            new CustomerRowMapper());
        
        return results.isEmpty() ? null : results.get(0);
    }
    
    // CORRECT - Update with organization validation
    public void updateCustomer(Customer customer, Integer orgId) {
        // Verify customer belongs to the organization
        Customer existing = getCustomerById(customer.getCustomerId(), orgId);
        if (existing == null) {
            throw new NGOPBaseException("Customer not found in organization", "CUSTOMER_NOT_FOUND");
        }
        
        String query = "UPDATE customers SET first_name = ?, last_name = ?, " +
                      "phone = ?, modified_date = ? " +
                      "WHERE customer_id = ? AND organization_id = ?";
        
        jdbcTemplate.update(query, 
            customer.getFirstName(), customer.getLastName(), customer.getPhone(),
            new Date(), customer.getCustomerId(), orgId);
    }
}

Financial Compliance

Audit Logging

AuditService.java - Compliance Logging
@Service
public class AuditService {
    
    @Autowired
    private AuditLogDAO auditLogDAO;
    
    public void logUserAction(Integer customerId, String action, String details, String ipAddress) {
        AuditLog log = new AuditLog();
        log.setCustomerId(customerId);
        log.setAction(action);
        log.setDetails(details);
        log.setIpAddress(ipAddress);
        log.setTimestamp(new Date());
        log.setSource("USER_ACTION");
        
        auditLogDAO.save(log);
        
        // Also log to compliance system if required
        if (isHighRiskAction(action)) {
            complianceLogger.logHighRiskAction(log);
        }
    }
    
    public void logPaymentTransaction(Integer customerId, Integer paymentId, 
                                     String action, BigDecimal amount, String currency) {
        AuditLog log = new AuditLog();
        log.setCustomerId(customerId);
        log.setAction("PAYMENT_" + action);
        log.setPaymentId(paymentId);
        log.setAmount(amount);
        log.setCurrency(currency);
        log.setTimestamp(new Date());
        log.setSource("PAYMENT_SYSTEM");
        
        auditLogDAO.save(log);
        
        // Regulatory reporting for large amounts
        if (amount.compareTo(new BigDecimal("10000")) >= 0) {
            regulatoryReportingService.reportLargeTransaction(log);
        }
    }
    
    public void logSecurityEvent(Integer customerId, String eventType, String details) {
        AuditLog log = new AuditLog();
        log.setCustomerId(customerId);
        log.setAction("SECURITY_" + eventType);
        log.setDetails(details);
        log.setTimestamp(new Date());
        log.setSource("SECURITY_SYSTEM");
        log.setSeverity("HIGH");
        
        auditLogDAO.save(log);
        
        // Immediate notification for security events
        securityAlertService.sendAlert(log);
    }
    
    private boolean isHighRiskAction(String action) {
        return Arrays.asList(
            "LARGE_PAYMENT", "INTERNATIONAL_TRANSFER", "ACCOUNT_STATUS_CHANGE",
            "KYC_STATUS_CHANGE", "PASSWORD_RESET", "EMAIL_CHANGE"
        ).contains(action);
    }
}

PCI DSS Compliance

  • No Card Data Storage: Payment card data never stored in Dione
  • Encrypted Data Transmission: All sensitive data encrypted in transit
  • Access Logging: All access to payment data logged
  • Network Segmentation: Payment processing isolated

Encryption & Data Protection

Sensitive Data Encryption

EncryptionService.java - Data Protection
@Service
public class EncryptionService {
    
    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int IV_LENGTH = 12;
    private static final int TAG_LENGTH = 16;
    
    @Value("${dione.encryption.key}")
    private String encryptionKey;
    
    public String encryptSensitiveData(String plainText) {
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec keySpec = new SecretKeySpec(Base64.getDecoder().decode(encryptionKey), "AES");
            
            // Generate random IV
            byte[] iv = new byte[IV_LENGTH];
            new SecureRandom().nextBytes(iv);
            GCMParameterSpec paramSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
            
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, paramSpec);
            byte[] encryptedData = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
            
            // Combine IV and encrypted data
            byte[] encryptedWithIv = new byte[IV_LENGTH + encryptedData.length];
            System.arraycopy(iv, 0, encryptedWithIv, 0, IV_LENGTH);
            System.arraycopy(encryptedData, 0, encryptedWithIv, IV_LENGTH, encryptedData.length);
            
            return Base64.getEncoder().encodeToString(encryptedWithIv);
            
        } catch (Exception e) {
            LOG.error("Encryption failed", e);
            throw new RuntimeException("Data encryption failed");
        }
    }
    
    public String decryptSensitiveData(String encryptedData) {
        try {
            byte[] encryptedWithIv = Base64.getDecoder().decode(encryptedData);
            
            // Extract IV and encrypted data
            byte[] iv = new byte[IV_LENGTH];
            byte[] encrypted = new byte[encryptedWithIv.length - IV_LENGTH];
            System.arraycopy(encryptedWithIv, 0, iv, 0, IV_LENGTH);
            System.arraycopy(encryptedWithIv, IV_LENGTH, encrypted, 0, encrypted.length);
            
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec keySpec = new SecretKeySpec(Base64.getDecoder().decode(encryptionKey), "AES");
            GCMParameterSpec paramSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
            
            cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
            byte[] decryptedData = cipher.doFinal(encrypted);
            
            return new String(decryptedData, StandardCharsets.UTF_8);
            
        } catch (Exception e) {
            LOG.error("Decryption failed", e);
            throw new RuntimeException("Data decryption failed");
        }
    }
    
    public String hashPII(String personalData) {
        // One-way hash for PII that doesn't need to be recovered
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(personalData.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
        } catch (Exception e) {
            throw new RuntimeException("PII hashing failed");
        }
    }
}

Database Encryption

Customer Entity with Encrypted Fields
@Entity
@Table(name = "customers")
public class Customer {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "customer_id")
    private Integer customerId;
    
    // Regular fields
    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    @Column(name = "email")
    private String email;
    
    // Encrypted sensitive fields
    @Column(name = "phone_encrypted")
    private String phoneEncrypted;
    
    @Column(name = "date_of_birth_encrypted")
    private String dateOfBirthEncrypted;
    
    @Column(name = "ssn_hash") // One-way hash, never decrypted
    private String ssnHash;
    
    // Transient fields for decrypted data
    @Transient
    private String phone;
    
    @Transient
    private Date dateOfBirth;
    
    // Encryption/decryption methods
    @PostLoad
    public void decryptSensitiveData() {
        if (phoneEncrypted != null) {
            this.phone = encryptionService.decryptSensitiveData(phoneEncrypted);
        }
        if (dateOfBirthEncrypted != null) {
            String dateStr = encryptionService.decryptSensitiveData(dateOfBirthEncrypted);
            this.dateOfBirth = Date.valueOf(dateStr);
        }
    }
    
    @PrePersist
    @PreUpdate
    public void encryptSensitiveData() {
        if (phone != null) {
            this.phoneEncrypted = encryptionService.encryptSensitiveData(phone);
        }
        if (dateOfBirth != null) {
            this.dateOfBirthEncrypted = encryptionService.encryptSensitiveData(dateOfBirth.toString());
        }
    }
}

Security Debugging

Common Security Issues

Security Debugging Commands
# Check active sessions for a customer
echo "GET session:*" | redis-cli | grep "customer:12345"

# Review recent audit logs
psql -U dione_user -d dione_db -c \
  "SELECT * FROM audit_log WHERE action LIKE 'SECURITY_%' ORDER BY timestamp DESC LIMIT 20;"

# Check failed login attempts
psql -U dione_user -d dione_db -c \
  "SELECT email, failed_login_attempts, last_failed_login FROM customers WHERE failed_login_attempts > 0;"

# Monitor for organization access violations
tail -f /opt/jboss/standalone/log/server.log | grep "ORG_ACCESS_VIOLATION"

# Check encryption key configuration
echo $DIONE_ENCRYPTION_KEY | base64 -d | wc -c  # Should be 32 bytes

# Verify SSL certificate
openssl s_client -connect api.currenciesdirect.com:443 -servername api.currenciesdirect.com

Security Monitoring Queries

Security Monitoring SQL
-- Find suspicious login patterns
SELECT customer_id, COUNT(*) as failed_attempts, 
       MAX(timestamp) as last_attempt
FROM audit_log 
WHERE action = 'FAILED_LOGIN' 
  AND timestamp > DATEADD(hour, -1, GETDATE())
GROUP BY customer_id
HAVING COUNT(*) > 5;

-- Check for cross-organization access attempts
SELECT customer_id, action, details, timestamp
FROM audit_log 
WHERE action = 'ORG_ACCESS_VIOLATION'
  AND timestamp > DATEADD(day, -7, GETDATE())
ORDER BY timestamp DESC;

-- Monitor large payment transactions
SELECT customer_id, payment_id, amount, currency, timestamp
FROM audit_log 
WHERE action = 'PAYMENT_CREATED' 
  AND amount > 50000
  AND timestamp > DATEADD(day, -1, GETDATE())
ORDER BY amount DESC;

-- Find accounts with recent security events
SELECT DISTINCT customer_id, 
       COUNT(*) as security_events,
       MAX(timestamp) as last_event
FROM audit_log 
WHERE action LIKE 'SECURITY_%'
  AND timestamp > DATEADD(day, -30, GETDATE())
GROUP BY customer_id
ORDER BY security_events DESC;

Common Security Issues

Security Anti-Patterns to Avoid

🚨 Critical Security Mistakes

These mistakes can lead to data breaches or compliance violations:

1. Missing Organization ID Filters

❌ WRONG - Missing org filter
// This exposes data from ALL organizations!
List payments = paymentDAO.getPaymentsByCustomer(customerId);
βœ… CORRECT - With org isolation
// Safe: Only returns payments from the customer's organization
Integer orgId = sessionData.getInteger("orgID");
List payments = paymentDAO.getPaymentsByCustomer(customerId, orgId);

2. Insufficient Session Validation

❌ WRONG - No session validation
public PaymentResponse createPayment(CreatePaymentRequest request) {
    // No session validation - anyone can call this!
    return paymentService.createPayment(request);
}
βœ… CORRECT - Proper session validation
public PaymentResponse createPayment(CreatePaymentRequest request) {
    PojoMap sessionData = sessionManager.validateSession(request.getSessionID());
    authorizationService.checkPermission(sessionData, "CREATE_PAYMENT");
    
    return paymentService.createPayment(request, sessionData);
}

3. Logging Sensitive Data

❌ WRONG - Sensitive data in logs
// Never log passwords, card numbers, or PII!
LOG.info("Login attempt: {} with password: {}", email, password);
LOG.debug("Customer data: {}", customer.toString()); // Contains encrypted fields
βœ… CORRECT - Safe logging
LOG.info("Login attempt for user: {} from IP: {}", email, request.getRemoteAddr());
LOG.debug("Processing customer: {} org: {}", customer.getCustomerId(), customer.getOrganizationId());

Security Checklist

  • βœ… Every API call validates session
  • βœ… Every database query includes organization_id filter
  • βœ… Sensitive data is encrypted at rest
  • βœ… All user actions are audited
  • βœ… Passwords are properly hashed with salt
  • βœ… Sessions expire after inactivity
  • βœ… Failed login attempts are limited
  • βœ… No sensitive data in log files
  • βœ… HTTPS enforced for all communications
  • βœ… Input validation on all parameters

⚠️ Security Best Practices

  • Fail Secure - When in doubt, deny access
  • Defense in Depth - Multiple security layers
  • Least Privilege - Minimum required permissions only
  • Audit Everything - Log all security-relevant events
  • Regular Reviews - Periodic security assessments
  • Stay Updated - Keep security libraries current