feat(ap4): defend forwarded identity headers

This commit is contained in:
donghyeon-ka
2026-07-25 14:59:47 +09:00
parent 452aa4808a
commit 2d58dea5e1
11 changed files with 116 additions and 34 deletions
@@ -1,44 +1,56 @@
package com.example.keycloakpattern;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.LinkedHashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EdgeIdentityController {
private final byte[] internalAuthToken;
EdgeIdentityController(@Value("${edge.internal-auth-token}") String internalAuthToken) {
if (!hasText(internalAuthToken)) {
throw new IllegalStateException("edge.internal-auth-token must be configured");
}
this.internalAuthToken = internalAuthToken.getBytes(StandardCharsets.UTF_8);
}
@GetMapping("/edge/me")
ResponseEntity<Map<String, Object>> currentUser(HttpServletRequest request) {
String authRequestUser = request.getHeader("X-Auth-Request-User");
String forwardedUser = request.getHeader("X-Forwarded-User");
String user = hasText(authRequestUser) ? authRequestUser : forwardedUser;
if (!hasText(user)) {
if (!hasText(authRequestUser) || !hasValidInternalToken(request)) {
return ResponseEntity.status(401).body(Map.of(
"error",
"trusted edge identity header is required"
"trusted edge authentication is required"
));
}
Map<String, Object> response = new LinkedHashMap<>();
response.put("pattern", "AP4-edge-forward-auth");
response.put("user", user);
response.put("email", firstNonBlank(
request.getHeader("X-Auth-Request-Email"),
request.getHeader("X-Forwarded-Email")
));
response.put("identityHeader", hasText(authRequestUser)
? "X-Auth-Request-User"
: "X-Forwarded-User");
response.put("user", authRequestUser);
response.put("email", request.getHeader("X-Auth-Request-Email"));
response.put("identityHeader", "X-Auth-Request-User");
return ResponseEntity.ok(response);
}
private static String firstNonBlank(String first, String second) {
return hasText(first) ? first : second;
private boolean hasValidInternalToken(HttpServletRequest request) {
String suppliedToken = request.getHeader("X-Internal-Auth-Token");
if (!hasText(suppliedToken)) {
return false;
}
return MessageDigest.isEqual(
internalAuthToken,
suppliedToken.getBytes(StandardCharsets.UTF_8)
);
}
private static boolean hasText(String value) {
@@ -1,6 +1,9 @@
server:
port: ${SERVER_PORT:8081}
edge:
internal-auth-token: ${EDGE_INTERNAL_AUTH_TOKEN:}
spring:
application:
name: keycloak-pattern-api
@@ -11,7 +11,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@SpringBootTest(properties = "edge.internal-auth-token=test-internal-edge-token")
@AutoConfigureMockMvc
class ApiSecurityTest {
@@ -42,18 +42,34 @@ class ApiSecurityTest {
}
@Test
void edgeEndpointRejectsMissingIdentityHeader() throws Exception {
void edgeEndpointRejectsMissingTrustedHeaders() throws Exception {
mockMvc.perform(get("/edge/me"))
.andExpect(status().isUnauthorized());
}
@Test
void edgeEndpointCurrentlyTrustsForwardedUserHeader() throws Exception {
void edgeEndpointRejectsForgedIdentityWithoutInternalToken() throws Exception {
mockMvc.perform(get("/edge/me")
.header("X-Forwarded-User", "regular-user")
.header("X-Forwarded-Email", "regular-user@example.test"))
.header("X-Auth-Request-User", "spoofed-admin"))
.andExpect(status().isUnauthorized());
}
@Test
void edgeEndpointRejectsWrongInternalToken() throws Exception {
mockMvc.perform(get("/edge/me")
.header("X-Auth-Request-User", "spoofed-admin")
.header("X-Internal-Auth-Token", "wrong-token"))
.andExpect(status().isUnauthorized());
}
@Test
void edgeEndpointAcceptsIdentityFromTrustedEdge() throws Exception {
mockMvc.perform(get("/edge/me")
.header("X-Auth-Request-User", "regular-user")
.header("X-Auth-Request-Email", "regular-user@example.test")
.header("X-Internal-Auth-Token", "test-internal-edge-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.user").value("regular-user"))
.andExpect(jsonPath("$.identityHeader").value("X-Forwarded-User"));
.andExpect(jsonPath("$.identityHeader").value("X-Auth-Request-User"));
}
}