feat(ap4): defend forwarded identity headers
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user