feat: add JWT authentication primitives

This commit is contained in:
dualfroz committed 2026-08-23 23:10:22 +02:00
1 parent fcbef8490f
commit 1f09418d7b
4 files changed
+152

No files matched your search

@@ -0,0 +1,6 @@
package com.slotline.security;
import com.slotline.user.Role;
public record AuthenticatedUser(Long id, String username, Role role, Long organizationId) {
}
@@ -0,0 +1,50 @@
package com.slotline.security;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
boolean bearer = header != null && header.startsWith(BEARER_PREFIX);
if (bearer && SecurityContextHolder.getContext().getAuthentication() == null) {
try {
AuthenticatedUser principal = jwtService.parse(header.substring(BEARER_PREFIX.length()));
List<SimpleGrantedAuthority> authorities =
List.of(new SimpleGrantedAuthority("ROLE_" + principal.role().name()));
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(principal, null, authorities);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException invalidToken) {
SecurityContextHolder.clearContext();
}
}
chain.doFilter(request, response);
}
}
@@ -0,0 +1,55 @@
package com.slotline.security;
import com.slotline.config.SlotlineProperties;
import com.slotline.user.Role;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
@Service
public class JwtService {
private final SecretKey signingKey;
private final Duration expiration;
private final String issuer;
public JwtService(SlotlineProperties properties) {
this.signingKey = Keys.hmacShaKeyFor(properties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8));
this.expiration = properties.getJwt().getExpiration();
this.issuer = properties.getJwt().getIssuer();
}
public String issue(Long userId, String username, Role role, Long organizationId) {
Instant now = Instant.now();
return Jwts.builder()
.issuer(issuer)
.subject(username)
.claim("uid", userId)
.claim("role", role.name())
.claim("org", organizationId)
.issuedAt(Date.from(now))
.expiration(Date.from(now.plus(expiration)))
.signWith(signingKey)
.compact();
}
public AuthenticatedUser parse(String token) {
Claims claims = Jwts.parser()
.verifyWith(signingKey)
.requireIssuer(issuer)
.build()
.parseSignedClaims(token)
.getPayload();
Long userId = ((Number) claims.get("uid")).longValue();
Role role = Role.valueOf(claims.get("role", String.class));
Long organizationId = ((Number) claims.get("org")).longValue();
return new AuthenticatedUser(userId, claims.getSubject(), role, organizationId);
}
}
@@ -0,0 +1,41 @@
package com.slotline.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RestAuthenticationHandlers {
private final ObjectMapper objectMapper;
public RestAuthenticationHandlers(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, exception) ->
write(response, HttpStatus.UNAUTHORIZED, "Authentication required", request.getRequestURI());
}
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, exception) ->
write(response, HttpStatus.FORBIDDEN, "Access denied", request.getRequestURI());
}
private void write(HttpServletResponse response, HttpStatus status, String detail, String path) throws IOException {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, detail);
problem.setTitle(status.getReasonPhrase());
problem.setProperty("path", path);
response.setStatus(status.value());
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
objectMapper.writeValue(response.getOutputStream(), problem);
}
}