Skip to main content

JWT Security

JSON Web Tokens (JWT) are the primary authentication mechanism in the Authorization Service. Proper JWT configuration is critical for system security.

JWT Secret Management

The JWT_SECRET environment variable is used to sign and verify all tokens. This secret must be:
  • At least 256 bits (32 bytes) for HS256 algorithm
  • Random and unpredictable - use cryptographically secure random generation
  • Never committed to version control - use secrets management systems
  • Rotated periodically - implement a key rotation strategy

Generating a Secure Secret

Using OpenSSL:
openssl rand -base64 32
Using Node.js:
require('crypto').randomBytes(32).toString('base64')
Using Python:
import secrets
import base64
base64.b64encode(secrets.token_bytes(32)).decode('utf-8')
Never use simple strings like “my-secret-key” or “password123”. These can be easily brute-forced and compromise your entire authentication system.

Token Expiration Strategy

The JWT_EXPIRATION_MS controls how long tokens remain valid. Consider the security/usability tradeoff:
EnvironmentRecommended ExpirationRationale
Development24 hours (86400000ms)Convenience for testing
Staging4 hours (14400000ms)Balance testing and security
Production1 hour (3600000ms)Minimize token exposure
Mobile Apps7 days with refreshUser experience on trusted devices
Implementation in code (JwtUtil.java:24-27):
public JwtUtil(@Value("${jwt.secret}") String secret,
               @Value("${jwt.expiration-ms:3600000}") long expirationMs) {
    this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    this.expirationMs = expirationMs;
}

Token Claims

The service includes the following claims in each JWT:
  • sub (subject) - User’s email address
  • userId - Internal user ID
  • email - User’s email (duplicate for convenience)
  • roles - Array of role names
  • permissions - Flattened array of all permissions from all roles
  • iat (issued at) - Token creation timestamp
  • exp (expiration) - Token expiration timestamp
Token generation (JwtUtil.java:30-56):
return Jwts.builder()
        .setSubject(email)
        .claim("roles", roles)
        .claim("permissions", permissions)
        .claim("userId", user.getUserId().id())
        .claim("email", user.getEmail().value())
        .setIssuedAt(now)
        .setExpiration(exp)
        .signWith(key)
        .compact();
Permissions are flattened into the token to avoid additional database lookups on each request. This improves performance but means permission changes require a new token.

Token Validation

The JwtAuthenticationFilter validates tokens on every protected request:
  1. Extract token from Authorization: Bearer <token> header
  2. Verify signature using the shared secret
  3. Check expiration timestamp
  4. Load user details and permissions
  5. Set Spring Security context
Validation logic (JwtUtil.java:72-79):
public boolean validateToken(String token) {
    try {
        Claims claims = extractAllClaims(token);
        return !claims.getExpiration().before(new Date());
    } catch (Exception e) {
        return false;
    }
}

Password Security

Password Hashing

All passwords are hashed using BCrypt with default work factor (10 rounds). BCrypt is specifically designed for password hashing and includes:
  • Automatic salt generation - Each password gets a unique salt
  • Adaptive cost - Can increase work factor as hardware improves
  • Slow by design - Resistant to brute-force attacks
Configuration (SecurityConfig.java:54-56):
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Password Policy Recommendations

While the service doesn’t enforce password complexity by default, implement these policies at the application layer:
  • Minimum length: 12 characters
  • Complexity: Mix of uppercase, lowercase, numbers, and symbols
  • No common passwords: Check against lists like “Have I Been Pwned”
  • No password reuse: Track password history
  • Regular rotation: Encourage (but don’t force) periodic changes
Example validation:
@NotBlank(message = "Password is required")
@Size(min = 12, max = 128, message = "Password must be between 12 and 128 characters")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
         message = "Password must contain uppercase, lowercase, digit, and special character")
private String password;

Role-Based Access Control (RBAC)

Authorization Model

The service implements a hierarchical RBAC system:
User → Roles → Permissions → Modules
  • Users can have multiple roles
  • Roles contain multiple permissions
  • Permissions are grouped by modules (USERS, ROLES, AUDIT, etc.)

Method-Level Security

Use Spring Security’s @PreAuthorize annotation to protect controller methods:
@PreAuthorize("hasAuthority('USER_READ')")
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
    // Implementation
}

@PreAuthorize("hasAuthority('USER_DELETE')")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    // Implementation
}

Dynamic Permission Checking

For complex authorization logic, inject AuthPrincipal in your service layer:
public void updateUser(Long userId, UpdateRequest request) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    AuthPrincipal principal = (AuthPrincipal) auth.getPrincipal();
    
    // Users can update their own profile
    // Admins can update any profile
    if (!userId.equals(principal.getUserId()) && 
        !principal.getAuthorities().contains("USER_ADMIN")) {
        throw new AccessDeniedException("Cannot update other users");
    }
    
    // Proceed with update
}

HTTPS Configuration

Why HTTPS is Essential

NEVER deploy the Authorization Service over HTTP in production. Without HTTPS:
  • JWT tokens can be intercepted and stolen
  • Passwords are transmitted in plaintext
  • Users are vulnerable to man-in-the-middle attacks

Enabling HTTPS in Spring Boot

Generate a keystore (for testing only):
keytool -genkeypair -alias authorization-service \
  -keyalg RSA -keysize 2048 -storetype PKCS12 \
  -keystore keystore.p12 -validity 3650 \
  -storepass changeit
Add to application.properties:
server.port=8443
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=changeit
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=authorization-service

Production HTTPS Deployment

For production, use a reverse proxy (Nginx, Apache) with a certificate from:
  • Let’s Encrypt (free, automated)
  • Your organization’s PKI
  • Commercial CA (DigiCert, Sectigo, etc.)
Example Nginx configuration:
server {
    listen 443 ssl http2;
    server_name auth.example.com;
    
    ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
    
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name auth.example.com;
    return 301 https://$server_name$request_uri;
}

CORS Configuration

If your frontend is hosted on a different domain, configure CORS carefully:
@Configuration
public class CorsConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("https://app.example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
Never use setAllowedOrigins("*") with setAllowCredentials(true). This is a security vulnerability.

Database Security

Principle of Least Privilege

Create a dedicated database user with minimal permissions:
CREATE USER auth_service WITH PASSWORD 'strong_random_password';
GRANT CONNECT ON DATABASE autorization_db TO auth_service;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO auth_service;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO auth_service;

Connection Security

For remote databases, use SSL/TLS:
spring.datasource.url=jdbc:postgresql://db.example.com:5432/autorization_db?ssl=true&sslmode=require

Sensitive Data Protection

Consider encrypting sensitive columns:
  • Email addresses
  • Phone numbers
  • Personal information
Use database-level encryption (PostgreSQL pgcrypto) or application-level encryption.

Audit Logging

The service includes comprehensive audit logging using AOP (@AuditLog annotation): What is logged:
  • User performing the action
  • Action type (CREATE, UPDATE, DELETE, etc.)
  • Module (USERS, ROLES, PERMISSIONS, etc.)
  • IP address
  • Timestamp
  • Method arguments (sanitized)
  • Success/failure status
Example (ActivityLogEntity):
@AuditLog(module = "USERS", action = "CREATE")
public UserResponse createUser(CreateUserRequest request) {
    // Implementation
}
Audit logs are stored in the activity_logs table and can be queried via:
GET /api/audit-logs?module=USERS&startDate=2024-01-01&endDate=2024-12-31

Security Checklist

Before deploying to production:
  • Strong JWT_SECRET configured (at least 256 bits)
  • Appropriate JWT_EXPIRATION_MS set (≤ 1 hour recommended)
  • HTTPS enabled with valid certificate
  • Database credentials stored in secrets manager
  • Database user has minimal required permissions
  • CORS configured with specific origins (no wildcards)
  • spring.jpa.show-sql=false in production
  • Password policy enforced at application layer
  • Audit logging enabled and monitored
  • Rate limiting implemented (consider Spring Cloud Gateway)
  • Security headers configured (CSP, HSTS, X-Frame-Options)
  • Dependency scanning enabled (Snyk, OWASP Dependency-Check)
  • Regular security updates applied

Security Incident Response

If you suspect a security breach:
  1. Rotate JWT secret immediately - invalidates all existing tokens
  2. Force password reset for affected users
  3. Review audit logs - check activity_logs table for suspicious activity
  4. Check database access logs - identify unauthorized queries
  5. Update dependencies - patch any known vulnerabilities
  6. Notify users - inform affected parties per your disclosure policy

Next Steps

Configuration Guide

Learn how to configure environment variables and database settings

Error Handling

Understand error responses and common troubleshooting scenarios

Build docs developers (and LLMs) love