auth
All checks were successful
Build Org Backend / build (push) Successful in 12s

This commit is contained in:
2026-05-09 00:36:49 +01:00
parent c8937357e5
commit aa31cc37d9
7 changed files with 36 additions and 83 deletions

View File

@@ -4,7 +4,6 @@ package org.zaine.app.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.zaine.app.security.JwtUtil;
import org.zaine.app.security.RequiresAuth;
import org.zaine.app.user.User;
import org.zaine.app.user.UserRepository;
import org.springframework.http.*;
@@ -17,7 +16,6 @@ import java.util.Map;
@RestController
@RequestMapping("/api/auth")
@RequiresAuth
@Tag(name = "Authentication")
public class AuthController {

View File

@@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/comments")
@RequiresAuth
@Tag(name = "Comments", description = "Endpoints for managing comments")
public class CommentsController {
@@ -86,7 +87,6 @@ public class CommentsController {
value = "",
consumes = "application/json"
)
@RequiresAuth
public void addComment(@RequestBody CreateCommentDTO dto) {
if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {

View File

@@ -1,31 +0,0 @@
package org.zaine.app.security;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ApiKeyAuthService {
private final String apiKey;
public ApiKeyAuthService(@Value("${org.auth.api-key:}") String apiKey) {
this.apiKey = apiKey == null ? "" : apiKey.trim();
}
public boolean isConfigured() {
return !apiKey.isBlank();
}
public boolean isValid(String candidate) {
if (!isConfigured() || candidate == null || candidate.isBlank()) {
return false;
}
byte[] expected = apiKey.getBytes(StandardCharsets.UTF_8);
byte[] provided = candidate.trim().getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(expected, provided);
}
}

View File

@@ -19,14 +19,11 @@ import java.util.List;
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
public static final String API_KEY_HEADER = "X-Org-Api-Key";
private final ApiKeyAuthService apiKeyAuthService;
private final JwtUtil jwtUtil;
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
public JwtAuthFilter(ApiKeyAuthService apiKeyAuthService,
RequestMappingHandlerMapping requestMappingHandlerMapping) {
this.apiKeyAuthService = apiKeyAuthService;
public JwtAuthFilter(JwtUtil jwtUtil, RequestMappingHandlerMapping requestMappingHandlerMapping) {
this.jwtUtil = jwtUtil;
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
}
@@ -39,17 +36,26 @@ public class JwtAuthFilter extends OncePerRequestFilter {
return;
}
String apiKey = request.getHeader(API_KEY_HEADER);
if (apiKeyAuthService.isValid(apiKey)) {
var auth = new UsernamePasswordAuthenticationToken("api-key", null, List.of());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header");
return;
}
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Missing or invalid " + API_KEY_HEADER + " header");
String token = authHeader.substring(7);
if (!jwtUtil.isTokenValid(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token");
return;
}
String username = jwtUtil.extractUsername(token);
var auth = new UsernamePasswordAuthenticationToken(username, null, List.of());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
}
private boolean endpointRequiresAuth(HttpServletRequest request) {

View File

@@ -1,27 +0,0 @@
package org.zaine.app.security;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class ApiKeyAuthServiceTest {
@Test
void rejectsAllKeysWhenNoApiKeyIsConfigured() {
ApiKeyAuthService service = new ApiKeyAuthService("");
assertFalse(service.isConfigured());
assertFalse(service.isValid("anything"));
}
@Test
void acceptsOnlyConfiguredApiKey() {
ApiKeyAuthService service = new ApiKeyAuthService("secret-key");
assertTrue(service.isConfigured());
assertTrue(service.isValid("secret-key"));
assertFalse(service.isValid("wrong-key"));
assertFalse(service.isValid(null));
}
}

View File

@@ -6,6 +6,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@@ -23,11 +24,14 @@ class JwtAuthFilterTest {
@BeforeEach
void setUp() {
handlerMapping = new TestHandlerMapping();
filter = new JwtAuthFilter(new ApiKeyAuthService("secret-key"), handlerMapping);
JwtUtil jwtUtil = new JwtUtil();
ReflectionTestUtils.setField(jwtUtil, "secret", "test-secret-key-that-is-at-least-32-characters");
ReflectionTestUtils.setField(jwtUtil, "expirationMs", 86_400_000L);
filter = new JwtAuthFilter(jwtUtil, handlerMapping);
}
@Test
void rejectsProtectedEndpointWithoutApiKey() throws Exception {
void rejectsProtectedEndpointWithoutBearerToken() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/secure");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicBoolean chainCalled = new AtomicBoolean(false);
@@ -40,9 +44,12 @@ class JwtAuthFilterTest {
}
@Test
void allowsProtectedEndpointWithValidApiKey() throws Exception {
void allowsProtectedEndpointWithValidBearerToken() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/secure");
request.addHeader(JwtAuthFilter.API_KEY_HEADER, "secret-key");
JwtUtil jwtUtil = new JwtUtil();
ReflectionTestUtils.setField(jwtUtil, "secret", "test-secret-key-that-is-at-least-32-characters");
ReflectionTestUtils.setField(jwtUtil, "expirationMs", 86_400_000L);
request.addHeader("Authorization", "Bearer " + jwtUtil.generateToken("zaine"));
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicBoolean chainCalled = new AtomicBoolean(false);
handlerMapping.setHandler(handlerFor("secure"));
@@ -54,7 +61,7 @@ class JwtAuthFilterTest {
}
@Test
void allowsPublicEndpointWithoutApiKey() throws Exception {
void allowsPublicEndpointWithoutBearerToken() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/public");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicBoolean chainCalled = new AtomicBoolean(false);