Security Architecture

Overview

StockEase implements a defense-in-depth security model with multiple layers of protection:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     HTTPS/TLS (Transport Security)      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚    CORS (Cross-Origin Resource Sharing) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   HTTP Security Headers (Security)      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚    Authentication (JWT Tokens)          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Authorization (Role-Based Access)     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     Input Validation & Sanitization     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚      Password Hashing (BCrypt)          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Database Security (Parameterized SQL)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1. Transport Security (HTTPS/TLS)

Implementation

  • Protocol: HTTPS only (TLS 1.2+)
  • Certificate: Managed by Koyeb/CloudFlare
  • Force HTTPS: All HTTP requests redirected to HTTPS

Configuration

# application.properties
server.ssl.enabled=true
server.ssl.key-store-type=PKCS12
# Certificates managed by infrastructure

Benefits

  • Encrypts all data in transit
  • Prevents man-in-the-middle attacks
  • Protects credentials and tokens
  • Required for production APIs

2. Authentication & Authorization

JWT (JSON Web Tokens)

Purpose: Stateless, scalable authentication

Token Structure:

Header.Payload.Signature

Header:
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload:
{
  "sub": "user-id-uuid",
  "username": "john.doe",
  "role": "ADMIN",
  "iat": 1701418200,
  "exp": 1701504600,
  "iss": "stockease-backend",
  "aud": "stockease-frontend"
}

Signature:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret_key
)

Token Generation Flow:

1. User calls POST /api/auth/login
   β”œβ”€β”€ Headers: Content-Type: application/json
   └── Body: { "username": "admin", "password": "admin123" }

2. AuthController receives request
   β”œβ”€β”€ Calls AuthService.authenticate(username, password)
   └── AuthService validates credentials

3. Credential Validation
   β”œβ”€β”€ Find user by username in database
   β”œβ”€β”€ Compare provided password with BCrypt hash
   β”œβ”€β”€ If match β†’ proceed to token generation
   β”œβ”€β”€ If no match β†’ throw AuthenticationException (401)
   └── If user not found β†’ throw AuthenticationException (401)

4. JWT Token Generation
   β”œβ”€β”€ Create payload with:
   β”‚   β”œβ”€β”€ User ID (sub claim)
   β”‚   β”œβ”€β”€ Username
   β”‚   β”œβ”€β”€ Role (ADMIN / USER)
   β”‚   β”œβ”€β”€ Issue time (iat)
   β”‚   β”œβ”€β”€ Expiration time (exp = iat + 24 hours)
   β”‚   β”œβ”€β”€ Issuer (iss)
   β”‚   └── Audience (aud)
   β”œβ”€β”€ Sign with secret key (HS256)
   └── Encode as base64url string

5. Response
   β”œβ”€β”€ Status: 200 OK
   β”œβ”€β”€ Body: { "token": "eyJhbG...", "expiresIn": 86400 }
   └── Client stores token (localStorage / sessionStorage)

### Login Sequence (simplified)

```mermaid
sequenceDiagram
  participant U as User
  participant FE as Frontend
  participant BE as Backend (Spring Boot)
  U->>FE: Submit credentials (username/password)
  FE->>BE: POST /api/auth/login (JSON body)
  BE->>BE: Authenticate (AuthenticationManager)
  BE->>BE: Lookup user (UserRepository)
  BE->>BE: Generate JWT (JwtUtil)
  BE-->>FE: 200 OK + { token }
  FE-->>U: Store token; use for subsequent requests

**Token Usage in Requests**:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…

Every subsequent request must include this header


**Token Validation Flow**:
  1. Request arrives at controller with JWT token

  2. SecurityFilter intercepts request β”œβ”€β”€ Extracts token from Authorization header β”œβ”€β”€ Calls JwtProvider.validateToken(token) └── Passes token to validation logic

  3. JWT Validation β”œβ”€β”€ Verify token format (Header.Payload.Signature) β”œβ”€β”€ Verify signature using secret key β”œβ”€β”€ Check expiration time (exp claim vs current time) β”œβ”€β”€ Extract claims (user ID, role) β”œβ”€β”€ If valid β†’ create Authentication object β”œβ”€β”€ If invalid/expired β†’ throw JwtException (401) └── If malformed β†’ throw JwtException (401)

  4. SecurityContext Set β”œβ”€β”€ Store Authentication in SecurityContext β”œβ”€β”€ Make available to controller via @AuthenticationPrincipal └── Controller can access user info

  5. Request Proceeds β”œβ”€β”€ SecurityFilter passes to next filter β”œβ”€β”€ Request reaches ProductController └── Can access authenticated user info


### Role-Based Access Control (RBAC)

**Roles Defined**:
```java
public enum Role {
    ADMIN,   // Full access: create, read, update, delete
    USER     // Limited access: read only
}

Authorization Rules (implemented):

Endpoint Method ADMIN USER Anonymous
/api/health GET βœ… βœ… βœ…
/api/auth/login POST ❌ ❌ βœ… (public)
/api/products GET βœ… βœ… ❌
/api/products/paged GET βœ… βœ… ❌
/api/products/{id} GET βœ… βœ… ❌
/api/products POST βœ… ❌ ❌
/api/products/{id}/quantity PUT βœ… βœ… ❌
/api/products/{id}/price PUT βœ… βœ… ❌
/api/products/{id}/name PUT βœ… βœ… ❌
/api/products/low-stock GET βœ… βœ… ❌
/api/products/search GET βœ… βœ… ❌
/api/products/total-stock-value GET βœ… βœ… ❌
/api/products/{id} DELETE βœ… ❌ ❌

Notes: The table above reflects the current SecurityConfig in code: login is permitted publicly (/api/auth/login), health is public, product create/delete are admin-only, and most read/update product endpoints allow ADMIN and USER.

API Map (quick reference)

Endpoint Purpose Auth
POST /api/auth/login Return JWT for valid credentials Public (no token)
GET /api/health Liveness/database connectivity Public
GET /api/products List products JWT (ADMIN/USER)
POST /api/products Create product JWT (ADMIN)
PUT /api/products/{id}/quantity Update product quantity JWT (ADMIN/USER)
DELETE /api/products/{id} Delete product JWT (ADMIN)

Authorization Implementation:

@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ProductDTO> createProduct(
    @RequestBody CreateProductRequest req) {
    // Only users with ADMIN role can reach here
    return productService.create(req);
}

// Or using method-level security:
@Service
public class ProductService {
    @Secured("ROLE_ADMIN")
    public void deleteProduct(UUID id) {
        // Only ADMIN can call this method
    }
}

3. Password Security

BCrypt Hashing

Purpose: Store passwords securely, never in plaintext

BCrypt Properties: - One-way hash: Cannot reverse to get original password - Salt included: Each password has unique salt (prevents rainbow tables) - Adaptive: Slower algorithm resists brute force - Configurable strength: Cost factor 10-12 iterations

Password Hashing Flow:

1. User provides password: "MySecurePass123!"

2. BCryptPasswordEncoder
   β”œβ”€β”€ Generate random salt
   β”œβ”€β”€ Apply BCrypt algorithm with salt
   β”œβ”€β”€ Produce hash: $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcg7b3XeKeUxWdeS86E36CHhzPm
   └── Store hash in database

3. Password Verification (login)
   β”œβ”€β”€ User provides password: "MySecurePass123!"
   β”œβ”€β”€ Retrieve stored hash from database
   β”œβ”€β”€ Apply BCrypt with provided password and stored salt
   β”œβ”€β”€ Compare computed hash with stored hash
   β”œβ”€β”€ If match β†’ password correct (401 vs 200)
   β”œβ”€β”€ If no match β†’ authentication failed (401 Unauthorized)
   └── Never store or log plaintext password

Configuration:

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10); // strength 10
    }
}

4. API Security

CORS (Cross-Origin Resource Sharing)

Purpose: Control which origins can access the API

Configuration:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://stockease.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("Content-Type", "Authorization")
            .exposedHeaders("X-Total-Count", "X-Page-Count")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

Production Settings: - βœ… Specific allowed origins (not *) - βœ… Limited HTTP methods - βœ… Credentials allowed for same-site requests - βœ… Limited max-age (3600 seconds)

Security Headers

Headers Set by Spring Security:

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, must-revalidate

What They Prevent: - HSTS: Forces HTTPS-only communication - X-Content-Type-Options: nosniff: Prevents MIME-type sniffing - X-Frame-Options: DENY: Prevents clickjacking - X-XSS-Protection: Enables browser XSS protection

5. Input Validation & Sanitization

Request Validation

Purpose: Prevent malformed data and malicious inputs

Validation Rules:

public class CreateProductRequest {
    @NotNull(message = "Name cannot be null")
    @NotBlank(message = "Name cannot be blank")
    @Size(min = 3, max = 255, message = "Name must be 3-255 characters")
    private String name;
    
    @NotNull(message = "Price cannot be null")
    @DecimalMin(value = "0.01", message = "Price must be > 0")
    @DecimalMax(value = "999999.99", message = "Price cannot exceed 999999.99")
    private BigDecimal price;
    
    @NotNull(message = "Quantity cannot be null")
    @Min(value = 0, message = "Quantity cannot be negative")
    @Max(value = 1000000, message = "Quantity cannot exceed 1,000,000")
    private Integer quantity;
    
    @NotBlank(message = "SKU is required")
    @Pattern(
        regexp = "^[A-Z0-9-]{3,50}$",
        message = "SKU must be 3-50 chars, alphanumeric and hyphens only"
    )
    private String sku;
}

Validation Execution:

1. Request arrives at @PostMapping
2. @Valid annotation triggers validation
3. CreateProductRequest deserialized and validated
4. Validation fails β†’ MethodArgumentNotValidException thrown
5. GlobalExceptionHandler catches exception
6. Returns 400 Bad Request with error details

Response (if validation fails):
{
  "status": 400,
  "error": "Validation Failed",
  "message": "Validation failed for argument 'request'",
  "details": [
    {
      "field": "name",
      "message": "Name must be 3-255 characters"
    },
    {
      "field": "price",
      "message": "Price must be > 0"
    }
  ]
}

SQL Injection Prevention

Parameterized Queries (Spring Data JPA):

// βœ… SAFE: Spring Data prevents SQL injection
productRepository.findBySku(userProvidedSku);

// βœ… SAFE: Native query with parameters
@Query("SELECT p FROM Product p WHERE p.sku = ?1")
Optional<Product> findBySku(String sku);

// ❌ DANGEROUS: String concatenation
Query q = entityManager.createNativeQuery(
    "SELECT * FROM products WHERE sku = '" + userInput + "'"
);
// This allows SQL injection!

6. Database Security

Connection Security

# application.properties
spring.datasource.url=jdbc:postgresql://host:5432/stockease?sslmode=require
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}

Environment Variables (never hardcoded): - DB_USER: Database username - DB_PASSWORD: Database password - JWT_SECRET: JWT signing key

Row-Level Security (Future)

-- Example: Users can only see products they created
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_products ON products
  USING (created_by = current_user_id());

7. Audit Logging

Logged Events:

- User login attempt (success/failure)
- User registration
- Product creation (who, when)
- Product modification (who, when)
- Product deletion (who, when)
- Authorization failures (403 errors)
- API errors (500 errors)

Log Format:

[2025-10-31 10:30:45] INFO  [AuthService] User 'john.doe' logged in successfully
[2025-10-31 10:31:12] INFO  [ProductService] Product 'Widget' created by admin (ID: ...)
[2025-10-31 10:32:00] WARN  [SecurityFilter] Unauthorized access attempt: invalid token
[2025-10-31 10:33:15] ERROR [ProductService] Database error: connection timeout

8. Security Best Practices Implemented

Practice Implementation Status
HTTPS/TLS Koyeb managed certificates βœ…
JWT Tokens HS256 signing with secret key βœ…
Password Hashing BCrypt with cost factor 10 βœ…
RBAC ADMIN/USER roles βœ…
Input Validation @Valid + JSR-303 annotations βœ…
SQL Injection Prevention Parameterized queries (JPA) βœ…
CORS Restricted to specific origins βœ…
Security Headers HSTS, X-Frame-Options, etc. βœ…
Audit Logging Request logging in critical paths βœ…
Secrets Management Environment variables, no hardcoding βœ…
Rate Limiting Per-endpoint rate limiting (future) ⏳
API Key Management Support for API keys (future) ⏳

9. Default Credentials (Development/Testing)

Purpose: Enable quick testing without additional setup

Admin User:

Username: admin
Password: admin123
Role: ADMIN

Regular User:

Username: user
Password: user123
Role: USER

⚠️ WARNING: These credentials are seeded via V3 migration for development only. In production, create secure passwords and remove seed data.

10. Security Testing

Test Coverage

- Authentication tests (valid/invalid credentials)
- Authorization tests (ADMIN vs USER endpoints)
- Password hashing verification
- JWT token validation/expiration
- CORS policy enforcement
- Input validation edge cases
- SQL injection attempts (should fail)

Example Security Test

@Test
public void testUnauthorizedAccessToProductCreation() {
    // User role cannot create products
    mockMvc.perform(post("/api/products")
        .header("Authorization", "Bearer " + userToken)
        .contentType(MediaType.APPLICATION_JSON)
        .content(jsonRequest))
        .andExpect(status().isForbidden()); // 403
}

@Test
public void testInvalidTokenRejection() {
    mockMvc.perform(get("/api/products")
        .header("Authorization", "Bearer invalid-token"))
        .andExpect(status().isUnauthorized()); // 401
}

11. Deployment Security Checklist

Before production deployment: - [ ] Change all default credentials - [ ] Remove seed data (V3 migration) - [ ] Set strong JWT secret (32+ characters) - [ ] Enable HTTPS/TLS - [ ] Configure CORS for production domain only - [ ] Set up audit logging - [ ] Enable database backups - [ ] Review security headers - [ ] Perform security testing - [ ] Set up monitoring/alerting - [ ] Document security policies - [ ] Conduct code review for security issues


Main Architecture Topics

Architecture Decisions (ADRs)

Design Patterns & Practices

Infrastructure & Deployment


Document Version: 1.0
Last Updated: October 31, 2025
Status: Production Ready