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
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.
{
"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.
@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
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.
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
@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
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 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







