This document describes the security measures implemented in the Library Management API and how to use them properly.
- JWT Authentication
- CSRF Protection
- XSS Prevention
- Authentication & Authorization
- Security Headers
- Frontend Integration
- Best Practices
This application uses JWT (JSON Web Token) authentication with HttpOnly cookies for secure token storage.
- Login → Server generates JWT with user info and role → Stores in HttpOnly cookie
- Subsequent Requests → Browser automatically sends JWT cookie → Server validates and authorizes
- Logout → Server clears the JWT cookie
The JWT contains the following claims:
{
"sub": "username", // Subject (username)
"role": "ADMIN", // User role (ADMIN, USER, GUEST)
"userId": 123, // User ID
"iss": "Kane Inc.", // Issuer
"iat": 1234567890, // Issued at (timestamp)
"exp": 1234654290 // Expiration (timestamp)
}Token Generation (includes role):
public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", user.getRole().roleType().name());
claims.put("userId", user.getId().value());
return createToken(claims, user.getUsername());
}Token Verification:
public Boolean validateToken(String token) {
try {
return !isTokenExpired(token);
} catch (JwtException e) {
return false;
}
}The JwtRequestFilter runs on every request and:
- Skips public endpoints (
/auth/sign-in,/auth/register,/auth/csrf) - Extracts JWT from the
jwtcookie - Validates token using
JwtService.validateToken() - Sets authentication in
SecurityContextwith user's role - Returns 401 if token is invalid/missing on protected endpoints
Key Methods:
// Extract JWT from cookie
private String extractJwtFromCookie(HttpServletRequest request) {
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if ("jwt".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
// Set authentication with role
private void setAuthentication(String token) {
String username = jwtService.extractUsername(token);
String role = jwtService.extractRole(token);
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role);
UserDetails userDetails = User.builder()
.username(username)
.password("")
.authorities(authority)
.build();
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
// Handle unauthorized access
private void handleUnauthorized(HttpServletResponse response, String message) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
String jsonResponse = String.format(
"{\"error\": \"Unauthorized\", \"message\": \"%s\", \"status\": 401}",
message
);
response.getWriter().write(jsonResponse);
}┌─────────┐ ┌─────────┐ ┌──────────────┐
│ Client │ │ Server │ │ JWT Filter │
└────┬────┘ └────┬────┘ └──────┬───────┘
│ │ │
│ POST /auth/sign-in │ │
├──────────────────────────>│ │
│ │ │
│ │ Validate credentials │
│ │ Generate JWT │
│ │ Set cookie: jwt=<token> │
│<──────────────────────────┤ │
│ │ │
│ GET /api/users (with jwt) │ │
├──────────────────────────>│──────────────────────────>│
│ │ │
│ │ Extract JWT │
│ │ Validate │
│ │ Set Auth │
│ │<────────────────────────────│
│ │ │
│ │ Process request │
│<──────────────────────────┤ │
│ │ │
When JWT is invalid or missing on protected endpoints:
Response:
{
"error": "Unauthorized",
"message": "Invalid or missing JWT token",
"status": 401
}HTTP Status: 401 Unauthorized
AuthController.java:32-37
Cookie jwtCookie = new Cookie("jwt", token.get());
jwtCookie.setHttpOnly(true); // JavaScript cannot access (prevents XSS)
jwtCookie.setSecure(true); // HTTPS only in production
jwtCookie.setPath("/"); // Available for all paths
jwtCookie.setMaxAge(86400); // 1 day (24 hours)| Feature | Implementation | Purpose |
|---|---|---|
| HttpOnly Cookie | setHttpOnly(true) |
Prevents XSS attacks (JavaScript cannot read) |
| Secure Flag | setSecure(true) |
HTTPS only (production) |
| Token Expiration | 24 hours | Limits damage if token is compromised |
| Role-Based Auth | Role in JWT claims | Fine-grained access control |
| Signature Verification | HMAC-SHA256 | Prevents token tampering |
curl -X POST http://localhost:8080/auth/sign-in \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
-c cookies.txt -vcurl -X GET http://localhost:8080/api/users \
-b cookies.txtcurl -X GET http://localhost:8080/api/users
# Response: {"error":"Unauthorized","message":"Invalid or missing JWT token","status":401}curl -X GET http://localhost:8080/api/users \
-H "Cookie: jwt=invalid-token-here"
# Response: {"error":"Unauthorized","message":"Invalid or missing JWT token","status":401}Cross-Site Request Forgery (CSRF) is an attack that forces users to execute unwanted actions on a web application where they're authenticated. Since this app uses cookie-based JWT authentication, CSRF protection is critical.
- CSRF Token Storage: Spring Security stores a CSRF token in a cookie named
XSRF-TOKEN - Token Transmission: Frontend must read this cookie and include it in requests
- Validation: Backend validates the token on state-changing requests (POST, PUT, DELETE)
SecurityConfig.java:31-35
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(requestHandler)
.ignoringRequestMatchers("/auth/sign-in", "/auth/register")
)- withHttpOnlyFalse(): Allows JavaScript to read the CSRF cookie
- ignoringRequestMatchers: Login and registration don't require CSRF (they're the first requests)
After login, call the CSRF endpoint:
// After successful login
const response = await fetch('http://localhost:8080/auth/csrf', {
credentials: 'include'
});
const csrfToken = await response.json();Add the CSRF token to request headers:
fetch('http://localhost:8080/api/users/123', {
method: 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken.token // Include CSRF token
},
body: JSON.stringify(userData)
});// Axios automatically reads XSRF-TOKEN cookie and sends it as X-XSRF-TOKEN header
axios.defaults.withCredentials = true;
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';Cross-Site Scripting (XSS) allows attackers to inject malicious scripts into web pages viewed by other users.
.headers(headers -> headers
.xssProtection(xss -> xss.headerValue("1; mode=block"))
.contentTypeOptions(contentType -> contentType.disable())
.frameOptions(frame -> frame.deny())
.contentSecurityPolicy(csp -> csp.policyDirectives(...))
)Headers Applied:
- X-XSS-Protection: Enables browser XSS filtering
- X-Content-Type-Options: nosniff: Prevents MIME-type sniffing
- X-Frame-Options: DENY: Prevents clickjacking
- Content-Security-Policy: Restricts resource loading sources
The XssFilter wraps all requests and sanitizes parameters by escaping HTML entities:
// Example: "<script>alert('xss')</script>" becomes "<script>alert('xss')</script>"This prevents malicious HTML/JavaScript from being executed.
When rendering data in views, ensure proper encoding:
- Use Jackson's automatic JSON encoding (already in place)
- For HTML templates (if used), use template engine escaping (Thymeleaf auto-escapes)
@NotBlank(message = "Username is required")
@Size(max = 100)
private String username;Jakarta Bean Validation (@Valid, @NotBlank, @Size) provides first-line defense.
AuthController.java:32-37
Cookie jwtCookie = new Cookie("jwt", token.get());
jwtCookie.setHttpOnly(true); // JavaScript cannot access (XSS protection)
jwtCookie.setSecure(true); // HTTPS only (production)
jwtCookie.setPath("/");
jwtCookie.setMaxAge(86400); // 1 daySecurityConfig.java:36-40
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
)POST /auth/register- User self-registration (USER role)POST /auth/sign-in- LoginGET /auth/csrf- Get CSRF token
| Header | Value | Purpose |
|---|---|---|
| X-XSS-Protection | 1; mode=block | Browser XSS filter |
| X-Content-Type-Options | nosniff | Prevent MIME sniffing |
| X-Frame-Options | DENY | Prevent clickjacking |
| Content-Security-Policy | See below | Restrict resource sources |
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
Customization: Update SecurityConfig.java:50-57 if you need to allow external resources (CDNs, analytics, etc.)
// 1. Login
async function login(username, password) {
const response = await fetch('http://localhost:8080/auth/sign-in', {
method: 'POST',
credentials: 'include', // Important: Include cookies
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (response.ok) {
// 2. Get CSRF token
const csrfResponse = await fetch('http://localhost:8080/auth/csrf', {
credentials: 'include'
});
const csrfToken = await csrfResponse.json();
// 3. Store CSRF token (in memory, not localStorage for security)
sessionStorage.setItem('csrf-token', csrfToken.token);
}
}
// 4. Make authenticated requests
async function updateUser(userId, userData) {
const csrfToken = sessionStorage.getItem('csrf-token');
const response = await fetch(`http://localhost:8080/api/users/${userId}`, {
method: 'PUT',
credentials: 'include', // Include JWT cookie
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken // Include CSRF token
},
body: JSON.stringify(userData)
});
return response.json();
}Update WebSecurityConfig.java:14 with your frontend URLs:
.allowedOrigins(
"http://localhost:3000", // Development
"https://yourdomain.com" // Production
)- Always use HTTPS in production (JWT cookies have
Secureflag) - Store CSRF token in sessionStorage, not localStorage
- Include credentials in all requests (
credentials: 'include') - Validate user input on both frontend and backend
- Use parameterized queries (JPA handles this automatically)
- Keep dependencies updated (
mvn versions:display-dependency-updates) - Change default admin password (
admin/admin123) immediately - Use environment variables for secrets (database passwords, JWT secret)
- Don't disable CSRF protection for cookie-based auth
- Don't store JWT in localStorage (XSS vulnerability)
- Don't trust user input - always validate and sanitize
- Don't expose stack traces in production errors
- Don't commit secrets to version control (use .env files)
- Don't use weak passwords - enforce password policies
- Don't skip HTTPS in production
- Change default admin credentials
- Configure production CORS origins
- Enable HTTPS (TLS/SSL)
- Set strong JWT secret (use environment variable)
- Review and update CSP policies
- Enable database encryption at rest
- Set up rate limiting (DDoS protection)
- Configure logging and monitoring
- Perform security audit/penetration testing
- Review dependency vulnerabilities (
mvn dependency-check:check)
- Update dependencies monthly
- Review access logs for suspicious activity
- Rotate JWT secrets periodically
- Audit user permissions quarterly
- Test backup and recovery procedures
Attack: User enters <script>alert('xss')</script> in username field
Mitigation:
XssFiltersanitizes to<script>alert('xss')</script>@Sizevalidation limits input length- CSP blocks inline script execution
- HttpOnly cookies prevent JWT theft
Attack: Malicious site tries to make authenticated request
Mitigation:
- CSRF token required for state-changing operations
- Token stored in cookie (same-site)
- Backend validates token on every request
- Attacker cannot read the token (cross-origin restriction)
Attack: User enters admin' OR '1'='1 in username
Mitigation:
- JPA uses parameterized queries automatically
- Input validation rejects invalid characters
@NotBlankprevents empty/malicious input
Current Gap: Not yet implemented
Recommendation: Add rate limiting (Spring Security + Bucket4j)
# This should FAIL (403 Forbidden)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-b "jwt=<your-jwt-token>" \
-d '{"username":"test"}'
# This should SUCCEED (with CSRF token)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-H "X-XSRF-TOKEN: <csrf-token>" \
-b "jwt=<your-jwt-token>; XSRF-TOKEN=<csrf-token>" \
-d '{"username":"test"}'# Submit XSS payload - should be escaped
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "<script>alert(\"xss\")</script>",
"password": "test123",
"email": "test@test.com",
"phoneNumber": "1234567890"
}'
# Check database - value should be escapedFor security vulnerabilities, please report privately to the development team rather than creating public issues.