Skip to main content

06 - Security as an Architectural Priority

JWT authentication with HttpOnly cookies, refresh token rotation, 2-level RBAC, rate limiting, BCrypt and OWASP Top 10 compliance in Play The Event.

What do you think about this article?

At a Glance

JWT authentication with HttpOnly cookies, refresh token rotation, 2-level RBAC, rate limiting, BCrypt and OWASP Top 10 compliance in Play The Event.

Security and JWT Authentication in Play The Event

Security is not an afterthought in Play The Event. From the very first sprint, the platform was built with a security-first mindset, implementing industry best practices for authentication, authorization, and data protection. When users trust you with their event data, personal information, and financial records, that trust must be earned through rigorous security engineering.

This article covers the complete security architecture: JWT-based authentication with HttpOnly cookies, stateless session management, token rotation, role-based access control (RBAC), security headers, rate limiting, password hashing, and OWASP compliance.

What You'll Learn

  • JWT authentication with HttpOnly cookies (why not localStorage)
  • Stateless session management and its trade-offs
  • Token rotation and refresh token strategy
  • Role-Based Access Control (RBAC) implementation
  • Security headers and CORS configuration
  • Rate limiting and brute force protection
  • BCrypt password hashing and OWASP compliance

JWT Authentication: HttpOnly Cookies

Play The Event uses JSON Web Tokens (JWT) for authentication, but with a critical distinction from many tutorials: tokens are stored in HttpOnly cookies, not in localStorage or sessionStorage.

Why Not localStorage?

Storing JWTs in localStorage makes them accessible to any JavaScript running on the page, including injected scripts from XSS attacks. An attacker who finds a single XSS vulnerability can steal every user's token. HttpOnly cookies are invisible to JavaScript, eliminating this entire attack vector.

Authentication Flow
AUTHENTICATION FLOW

1. LOGIN REQUEST
   POST /api/auth/login
   Body: { "email": "user@example.com", "password": "..." }

2. SERVER VALIDATES CREDENTIALS
   ├── Lookup user by email
   ├── Verify password with BCrypt
   ├── Check account status (active, not locked)
   └── Check failed attempt count

3. TOKEN GENERATION
   ├── Access Token (short-lived: 15 minutes)
   │   ├── Claims: userId, email, roles
   │   ├── Signed with HS512 secret
   │   └── Set in HttpOnly cookie
   ├── Refresh Token (long-lived: 7 days)
   │   ├── Claims: userId, tokenFamily
   │   ├── Signed with separate secret
   │   ├── Stored in database (hashed)
   │   └── Set in HttpOnly cookie
   └── CSRF Token
       └── Returned in response header

4. COOKIE CONFIGURATION
   Set-Cookie: access_token=eyJ...;
     HttpOnly; Secure; SameSite=Strict;
     Path=/api; Max-Age=900

   Set-Cookie: refresh_token=eyJ...;
     HttpOnly; Secure; SameSite=Strict;
     Path=/api/auth/refresh; Max-Age=604800

Token Structure

The JWT access token carries the minimum information needed for authorization decisions. Sensitive data is never stored in the token payload.

JWT Access Token Claims
{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "email": "organizer@example.com",
  "roles": ["ROLE_USER", "ROLE_ORGANIZER"],
  "iat": 1706000000,
  "exp": 1706000900,
  "iss": "playtheevent.com"
}

Stateless Sessions

Play The Event uses stateless session management. The server does not maintain session state between requests. Every request is self-contained, carrying all necessary authentication information in the JWT cookie.

Spring Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http)
            throws Exception {
        return http
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf
                .csrfTokenRepository(
                    CookieCsrfTokenRepository.withHttpOnlyFalse()))
            .cors(cors -> cors
                .configurationSource(corsConfigurationSource()))
            .addFilterBefore(jwtAuthFilter,
                UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**")
                    .hasRole("ADMIN")
                .anyRequest().authenticated())
            .build();
    }
}

Stateless Benefits

  • Horizontal scaling without session replication
  • No server-side session storage needed
  • Each request is independently verifiable
  • Simplified load balancer configuration

Trade-offs

  • Token revocation requires additional mechanisms
  • Token size increases with claims
  • Clock synchronization becomes important
  • Refresh token rotation adds complexity

Token Rotation Strategy

To mitigate the risks of token theft, Play The Event implements a refresh token rotation strategy. Every time a refresh token is used, a new pair of access and refresh tokens is issued, and the old refresh token is invalidated.

Token Rotation Flow
TOKEN ROTATION FLOW

1. Access token expires (after 15 minutes)

2. Client sends refresh request
   POST /api/auth/refresh
   Cookie: refresh_token=eyJ...

3. Server validates refresh token
   ├── Verify signature
   ├── Check expiration
   ├── Lookup in database (is it still valid?)
   └── Verify token family

4. If valid:
   ├── Invalidate old refresh token in DB
   ├── Generate new access token (15 min)
   ├── Generate new refresh token (7 days)
   ├── Store new refresh token hash in DB
   └── Set new cookies

5. If REUSE DETECTED (token already used):
   ├── Invalidate ALL tokens in the family
   ├── Force logout on all devices
   ├── Log security event
   └── Return 401 Unauthorized

SECURITY: If an attacker steals a refresh token
and uses it AFTER the legitimate user, the reuse
detection triggers and protects the account.

Token Family Concept

A "token family" groups all refresh tokens that descend from a single login session. When reuse is detected (the same token used twice), the entire family is invalidated, forcing the user to log in again on all devices. This is a proactive defense against token theft.

Role-Based Access Control (RBAC)

Authorization in Play The Event operates on two levels: system-level roles and event-level roles. A user might be a regular user in the system but an organizer for a specific event.

Two-Level RBAC Model
RBAC MODEL

SYSTEM-LEVEL ROLES
├── ADMIN     → Full system access, user management
├── USER      → Standard user, can create events
└── GUEST     → Limited access, view public content

EVENT-LEVEL ROLES (per event)
├── OWNER      → Full event control, can delete
├── ORGANIZER  → Manage event, participants, budget
├── MODERATOR  → Check-in, participant management
├── PARTICIPANT→ View event, RSVP, expenses
└── GUEST      → View public information only

AUTHORIZATION CHECK FLOW:
1. JWT filter extracts system roles from token
2. Method-level security checks system roles
3. Event-specific endpoint checks event role
4. Both must pass for access to be granted

EXAMPLE:
  GET /api/events/{id}/participants
  ├── System role: USER (minimum)
  ├── Event role: MODERATOR+ (minimum)
  └── Result: 200 OK or 403 Forbidden
Event-Level Authorization Example
@PreAuthorize("hasRole('USER')")
@GetMapping("/events/{eventId}/participants")
public ResponseEntity<List<ParticipantDto>> getParticipants(
        @PathVariable UUID eventId,
        @AuthenticationPrincipal UserDetails user) {

    // Check event-level authorization
    eventAuthorizationService.requireRole(
        eventId,
        user.getUserId(),
        ParticipantRole.MODERATOR  // minimum role required
    );

    List<ParticipantDto> participants =
        getParticipantsQuery.execute(
            new GetParticipantsQuery(eventId));

    return ResponseEntity.ok(participants);
}

Security Headers

Play The Event configures comprehensive security headers to protect against common web vulnerabilities. These headers instruct the browser to enforce security policies on the client side.

Security Headers Configuration
SECURITY HEADERS

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.playtheevent.com

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

Rate Limiting and Brute Force Protection

To protect against brute force attacks and abuse, Play The Event implements rate limiting at multiple levels.

Rate Limiting Rules

Endpoint Limit Window Action on Exceed
POST /auth/login 5 attempts 15 minutes Account lockout (30 min)
POST /auth/register 3 requests 1 hour 429 Too Many Requests
POST /auth/reset-password 3 requests 1 hour 429 Too Many Requests
API (authenticated) 100 requests 1 minute 429 Too Many Requests
API (public) 30 requests 1 minute 429 Too Many Requests

Password Security with BCrypt

User passwords are hashed using BCrypt with a cost factor of 12, which provides a strong balance between security and performance. Passwords are never stored in plain text and are never logged.

Password Handling
PASSWORD SECURITY

HASHING:
  Algorithm: BCrypt
  Cost Factor: 12 (2^12 = 4096 iterations)
  Salt: Automatically generated per password
  Output: $2a$12$... (60 characters)

VALIDATION RULES:
  ├── Minimum 8 characters
  ├── At least 1 uppercase letter
  ├── At least 1 lowercase letter
  ├── At least 1 digit
  ├── At least 1 special character
  └── Not in common password blacklist

ADDITIONAL PROTECTIONS:
  ├── Password change requires current password
  ├── Last 5 passwords cannot be reused
  ├── Password reset via time-limited token (1 hour)
  └── Failed attempts tracked per account

OWASP Compliance

Play The Event is designed with the OWASP Top 10 in mind, addressing each of the most critical web application security risks.

OWASP Top 10 Coverage

  • A01 Broken Access Control: RBAC at system and event levels, method-level security
  • A02 Cryptographic Failures: BCrypt for passwords, HS512 for JWTs, TLS everywhere
  • A03 Injection: Parameterized queries via JPA, input validation, output encoding
  • A04 Insecure Design: Threat modeling, security requirements from Sprint 1
  • A05 Security Misconfiguration: Security headers, CORS policy, error handling
  • A06 Vulnerable Components: Dependabot alerts, regular dependency updates
  • A07 Auth Failures: Token rotation, account lockout, multi-factor (planned)
  • A08 Software Integrity: Signed builds, dependency verification
  • A09 Logging & Monitoring: Security event logging, audit trail
  • A10 SSRF: URL validation, allowlisted external services

In the Next Article

With the security foundation in place, the next article explores the feature-rich event and participant management system: the event CRUD operations, the state machine in action, RSVP workflows, public sharing, check-in and checkout, budget tracking, and more. Visit www.playtheevent.com to see these security measures protecting real user data.

Key Takeaways

  • JWTs stored in HttpOnly cookies prevent XSS-based token theft
  • Stateless sessions enable horizontal scaling without session replication
  • Token rotation with family tracking detects and mitigates token theft
  • Two-level RBAC (system + event) provides fine-grained authorization
  • Rate limiting protects against brute force attacks at multiple endpoint levels
  • BCrypt with cost factor 12 and strict password policies secure user credentials
  • Security headers, CORS, and OWASP compliance form a comprehensive defense

Frequently asked questions

What is the primary approach to security in Play The Event?

The platform was built with a security-first mindset, implementing industry best practices for authentication, authorization, and data protection.

Why are JWTs stored in HttpOnly cookies instead of localStorage or sessionStorage?

Storing JWTs in HttpOnly cookies makes them invisible to JavaScript, eliminating the risk of XSS attacks that can steal user tokens.

What is the purpose of token rotation in Play The Event's security architecture?

Token rotation mitigates the risks of token theft by issuing new pairs of access and refresh tokens every time a refresh token is used.

How are user passwords secured in Play The Event?

User passwords are hashed using BCrypt with a cost factor of 12, providing a strong balance between security and performance.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects