MATIH Platform is in active MVP development. Documentation reflects current implementation status.
2. Architecture
IAM Architecture

IAM Service Internal Architecture

Production - Port 8081 - JWT, RBAC, MFA, API Keys, OAuth2

The IAM service is the security foundation of the MATIH platform. Every authenticated request that flows through the system was authorized by a JWT token issued by this service. This page documents the internal architecture, domain model, token lifecycle, and security patterns implemented within the IAM service.


2.3.B.1Internal Architecture

API Layer (Controllers)
AuthControllerUserControllerRoleControllerPermissionControllerApiKeyControllerMfaControllerOAuth2ControllerSessionController
Service Layer (Business Logic)
AuthenticationServiceUserServiceRoleServiceRbacServicePermissionEvaluatorApiKeyServiceMfaServiceTokenServicePasswordPolicyService
Security Layer (Filters)
SecurityFilterJwtAuthenticationFilterTenantContextFilterRbacFilterBruteForceProtectionFilter
Repository Layer (Persistence)
UserRepositoryRoleRepositoryPermissionRepositoryApiKeyRepositorySessionRepositoryAuditLogRepository

The IAM service follows the standard layered architecture enforced by commons-java, with additional security-specific layers:

Security Filter Chain

Every request passes through an ordered filter chain before reaching any controller:

// Filter execution order (Spring Security filter chain)
1. SecurityFilter          (HIGHEST_PRECEDENCE + 10)
   - Validates request headers for injection attacks
   - Checks for path traversal and null bytes
   - Rejects oversized payloads
 
2. JwtAuthenticationFilter (HIGHEST_PRECEDENCE + 20)
   - Extracts Bearer token from Authorization header
   - Validates JWT signature, expiry, issuer
   - Extracts userId, tenantId, roles from claims
   - Sets SecurityContextHolder
 
3. TenantContextFilter     (HIGHEST_PRECEDENCE + 30)
   - Reads X-Tenant-ID header (set by gateway)
   - Calls TenantContextHolder.setTenantId()
   - Clears context in finally block
 
4. RbacFilter              (HIGHEST_PRECEDENCE + 40)
   - Reads required permissions from @RequirePermission annotation
   - Evaluates user roles against required permissions
   - Returns 403 if insufficient permissions

2.3.B.2Domain Model

User Entity

@Entity
@Table(name = "users")
public class User {
    @Id
    private UUID id;
    private String email;               // Unique, indexed
    private String passwordHash;         // BCrypt, 12 rounds
    private String firstName;
    private String lastName;
    private String tenantId;            // Primary tenant
    private UserStatus status;          // ACTIVE, SUSPENDED, LOCKED, PENDING
    private boolean mfaEnabled;
    private String mfaSecret;           // TOTP secret (encrypted at rest)
    private int failedLoginAttempts;    // Reset on successful login
    private Instant lockedUntil;        // Account lockout expiry
    private Instant lastLoginAt;
    private Instant createdAt;
    private Instant updatedAt;
 
    @ManyToMany
    private Set<Role> roles;            // Tenant-scoped roles
}

Role and Permission Model

The RBAC model uses a three-level hierarchy:

Platform Roles (global scope)
  - platform_admin: Full platform access
  - tenant_creator: Can create new tenants

Tenant Roles (tenant-scoped)
  - tenant_admin: Full access within tenant
  - data_analyst: Query and dashboard access
  - data_engineer: Pipeline and catalog access
  - ml_engineer: ML service access
  - dashboard_viewer: Read-only dashboard access

Resource Permissions (fine-grained)
  - dashboard:read, dashboard:write, dashboard:delete
  - query:execute, query:cancel
  - model:train, model:deploy, model:delete
  - pipeline:create, pipeline:run, pipeline:delete

Roles inherit permissions hierarchically: tenant_admin inherits all permissions from data_analyst, data_engineer, and ml_engineer.


2.3.B.3Token Lifecycle

Access Token Generation

public String generateAccessToken(UUID userId, String tenantId, Set<String> roles) {
    Instant now = Instant.now();
    return Jwts.builder()
        .setId(UUID.randomUUID().toString())         // jti: unique token ID
        .setSubject(userId.toString())                // sub: user identifier
        .setIssuer("matih-platform")                  // iss: token issuer
        .setIssuedAt(Date.from(now))                  // iat: issued timestamp
        .setExpiration(Date.from(now.plus(15, MINUTES))) // exp: 15-min expiry
        .claim("type", "access")                      // Token type
        .claim("tenant_id", tenantId)                 // Tenant scope
        .claim("roles", String.join(",", roles))      // Permission set
        .signWith(signingKey, SignatureAlgorithm.HS256) // HMAC-SHA256
        .compact();
}

Token Validation Flow

When a downstream service receives a request with a JWT:

1. Extract token from Authorization: Bearer <token>
2. Parse JWT without validation (extract header)
3. Validate signature against platform signing key
4. Check expiry (exp claim vs current time)
5. Check issuer (iss == "matih-platform")
6. Check token type (type == "access")
7. Check token not blacklisted (Redis lookup: blacklist:{jti})
8. Extract claims: sub, tenant_id, roles
9. Set SecurityContextHolder with authenticated principal
10. Set TenantContextHolder with tenant_id

Token Blacklisting

When a user logs out or a token is revoked, the token's jti is added to the Redis blacklist with a TTL equal to the token's remaining validity:

public void blacklistToken(String jti, Instant expiry) {
    long ttlSeconds = Duration.between(Instant.now(), expiry).getSeconds();
    if (ttlSeconds > 0) {
        redisTemplate.opsForValue().set(
            "blacklist:" + jti,
            "revoked",
            Duration.ofSeconds(ttlSeconds)
        );
    }
}

This ensures blacklisted tokens are automatically cleaned up from Redis when they would have expired naturally.


2.3.B.4Password Security

Password Hashing

Passwords are hashed using BCrypt with 12 rounds (2^12 = 4096 iterations):

private final BCryptPasswordEncoder encoder =
    new BCryptPasswordEncoder(12);
 
public String hashPassword(String rawPassword) {
    return encoder.encode(rawPassword);
}
 
public boolean verifyPassword(String rawPassword, String hash) {
    return encoder.matches(rawPassword, hash);
}

Password Policy

The PasswordPolicyService enforces configurable password requirements:

PolicyDefault ValueConfigurable
Minimum length8 charactersYes
Maximum length128 charactersYes
Require uppercaseYesYes
Require lowercaseYesYes
Require digitYesYes
Require special characterYesYes
Password historyLast 5 passwordsYes
Maximum age90 daysYes

Account Lockout

After 5 consecutive failed login attempts, the account is locked for 30 minutes:

public void handleFailedLogin(User user) {
    user.setFailedLoginAttempts(user.getFailedLoginAttempts() + 1);
    if (user.getFailedLoginAttempts() >= MAX_FAILED_ATTEMPTS) {
        user.setStatus(UserStatus.LOCKED);
        user.setLockedUntil(Instant.now().plus(LOCKOUT_DURATION));
        auditLogger.log("ACCOUNT_LOCKED", user.getId(), user.getTenantId());
    }
}

2.3.B.5Database Schema

The IAM service uses its own PostgreSQL database (iam) with the following core tables:

TablePurposeIndexes
usersUser accountsemail (unique), tenant_id, status
rolesRole definitionsname + tenant_id (unique)
permissionsPermission catalogcode (unique)
user_rolesUser-to-role mappinguser_id + role_id (composite)
role_permissionsRole-to-permission mappingrole_id + permission_id (composite)
api_keysAPI key recordskey_hash (unique), tenant_id, user_id
sessionsActive sessionsuser_id, tenant_id, expires_at
mfa_settingsMFA configuration per useruser_id (unique)
password_historyPrevious password hashesuser_id, created_at
login_attemptsLogin attempt trackingemail, ip_address, attempted_at

Related Sections