authnHeaders = new HashMap<>();
HttpHeaders requestHeaders = request.getHeaders();
- log.debug("Adding headers, request = {}, required = {}", requestHeaders,
+ log.debug("Adding headers, request = {}, required = {}", requestHeaders.keySet(),
requiredHeaders);
- if (requestHeaders != null) {
- requiredHeaders.forEach(key -> {
- var value = requestHeaders.getFirst(key);
- if (value != null) {
- authnHeaders.put(key, value);
- }
- });
- }
+ requiredHeaders.forEach(key -> {
+ var value = requestHeaders.getFirst(key);
+ if (value != null) {
+ authnHeaders.put(key, value);
+ }
+ });
String token = Objects.requireNonNull(credentials).toString();
return TokenBasedAuthorizationRequest.builder()
.input(new TokenBasedAuthorizationRequest.AuthRequestBody(token,
new TokenBasedAuthorizationRequest.Resource(
- request.getMethodValue(),
+ request.getMethod().name(),
request.getPath().toString(),
authnHeaders
)
@@ -165,14 +163,4 @@ private TokenBasedAuthorizationRequest makeRequestBody(
)
.build();
}
-
- private WebClientResponseException unauthorized() {
- return WebClientResponseException.create(
- HttpStatus.UNAUTHORIZED.value(),
- HttpStatus.UNAUTHORIZED.getReasonPhrase(),
- null,
- USER_NOT_AUTHORIZED.getBytes(StandardCharsets.UTF_8),
- StandardCharsets.UTF_8
- );
- }
}
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java b/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java
index 4541905..f5f623c 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java
@@ -18,16 +18,24 @@
package com.alertavert.opa.security;
+import com.alertavert.opa.jwt.JwtTokenProvider;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.FormatFeature;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.PrettyPrinter;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import java.util.Formattable;
+import java.util.Formatter;
import java.util.Map;
+import java.util.stream.Collectors;
import static com.alertavert.opa.Constants.MAPPER;
-import static com.alertavert.opa.Constants.MAX_TOKEN_LEN_LOG;
/**
@@ -40,17 +48,17 @@
* "input"} object, we use this class to simplify the construction of the JSON body.
*
// {{ formatter:off }}
-
- {
- "input": {
- "api_token": ".... API Token Base-64 encoded ...",
- "resource": {
- "method": "POST",
- "path": "/path/to/resource"
- }
- }
- }
-
+
+ {
+ "input": {
+ "api_token": ".... API Token Base-64 encoded ...",
+ "resource": {
+ "method": "POST",
+ "path": "/path/to/resource"
+ }
+ }
+ }
+
// {{ formatter:on }}
*
* When serializing to String (e.g., in debug logs output) the API Token (JWT) is obfuscated
@@ -62,30 +70,43 @@
@Value
@Builder
@Jacksonized
+@Slf4j
public class TokenBasedAuthorizationRequest {
public record Resource(String method, String path, Map headers) {
}
- public record AuthRequestBody(@JsonProperty("api_token") String token, Resource resource) {
+ public record AuthRequestBody(@JsonProperty("api_token") String token,
+ Resource resource) implements Formattable {
+
+ private static final PrettyPrinter PRETTY_PRINTER = new DefaultPrettyPrinter()
+ .withoutSpacesInObjectEntries();
+
+ @Override
+ public void formatTo(Formatter formatter, int flags, int width, int precision) {
+ // Creates a new Map with all the headers, excluding the Authorization one.
+ Map maskedHeaders = resource.headers().entrySet().stream()
+ .filter(e -> !e.getKey().equalsIgnoreCase(HttpHeaders.AUTHORIZATION))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ try {
+ AuthRequestBody copy = new AuthRequestBody(JwtTokenProvider.maskToken(token),
+ new Resource(resource.method(), resource.path(), maskedHeaders));
+ formatter.format("%s", MAPPER.writer().with(PRETTY_PRINTER)
+ .writeValueAsString(copy));
+ } catch (JsonProcessingException e) {
+ formatter.format("invalid JSON: %s", e.getMessage());
+ }
+ }
}
AuthRequestBody input;
@Override
public String toString() {
- try {
- String token = "";
- if (input.token.length() > 2 * MAX_TOKEN_LEN_LOG) {
- token = input.token.substring(0, MAX_TOKEN_LEN_LOG) + "****" +
- input.token.substring(input.token.length() - MAX_TOKEN_LEN_LOG);
- }
- TokenBasedAuthorizationRequest copy = TokenBasedAuthorizationRequest.builder()
- .input(new AuthRequestBody(token, input.resource))
- .build();
- return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(copy);
- } catch (JsonProcessingException e) {
- throw new RuntimeException(e);
- }
+ return String.format("""
+ {
+ "input": %s
+ }
+ """, input);
}
}
diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java
index 3deb69d..6b4fa9d 100644
--- a/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java
+++ b/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java
@@ -106,7 +106,7 @@ void check() {
}
@Test
- public void checkUnauthorizedFails() {
+ void checkUnauthorizedFails() {
Authentication auth = factory.createAuthentication(
provider.createToken("alice", Lists.list("USER"))
).block();
@@ -121,7 +121,7 @@ public void checkUnauthorizedFails() {
}
@Test
- public void checkUnauthenticatedFails() {
+ void checkUnauthenticatedFails() {
Authentication auth = new UsernamePasswordAuthenticationToken("bob", "pass");
// As this endpoint is not mapped in `routes` (application-test.yaml) it expects by default
@@ -142,7 +142,7 @@ private AuthorizationContext getAuthorizationContextWithHeaders(
ServerHttpRequest request = mock(ServerHttpRequest.class);
RequestPath requestPath = mock(RequestPath.class);
- when(request.getMethodValue()).thenReturn(method.name());
+ when(request.getMethod()).thenReturn(method);
when(request.getPath()).thenReturn(requestPath);
when(requestPath.toString()).thenReturn(path);
@@ -159,7 +159,7 @@ private AuthorizationContext getAuthorizationContextWithHeaders(
}
@Test
- public void authenticatedEndpointBypassesOpa() {
+ void authenticatedEndpointBypassesOpa() {
AuthorizationContext context = getAuthorizationContext(HttpMethod.GET, "/testauth");
assertThat(opaReactiveAuthorizationManager.check(
factory.createAuthentication(
@@ -170,7 +170,7 @@ public void authenticatedEndpointBypassesOpa() {
}
@Test
- public void authenticatedEndpointMatches() {
+ void authenticatedEndpointMatches() {
// In the test configuration (application-test.yaml) we have configured the following
// path matchers: ["/match/*/this", "/match/any/**"].
// Here we test that an authenticated user gains access to them without needing authorization.
@@ -202,7 +202,7 @@ public void authenticatedEndpointMatches() {
}
@Test
- public void testHeaders() {
+ void testHeaders() {
AuthorizationContext context = getAuthorizationContextWithHeaders(HttpMethod.GET, "/whatever",
Map.of("x-test-header", "test-value", HttpHeaders.USER_AGENT, "TestAgent"));
assertThat(opaReactiveAuthorizationManager.check(
diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java
index 48b1a37..d0100eb 100644
--- a/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java
+++ b/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java
@@ -26,7 +26,9 @@
import static com.alertavert.opa.Constants.MAPPER;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertFalse;
class TokenBasedAuthorizationRequestTest {
@@ -48,15 +50,18 @@ void serialize() throws Exception {
@Test
void obfuscatesJwt() {
TokenBasedAuthorizationRequest request = TokenBasedAuthorizationRequest.builder()
- .input(new AuthRequestBody("tokenAAjwtDEF123456.anothertoken.yetanothertoken",
+ .input(new AuthRequestBody("AAjwtDEF123456.ZZZfere43535.yYYYY98764awarkfajser",
new TokenBasedAuthorizationRequest.Resource("POST", "/foo/bar", Map.of()))
)
.build();
- String json = request.toString();
-
- assertThat(json, hasJsonPath("$.input"));
- assertThat(json, hasJsonPath("$.input.api_token", equalTo("tokenA****rtoken")));
- assertThat(json, hasJsonPath("$.input.resource.method", equalTo("POST")));
- assertThat(json, hasJsonPath("$.input.resource.path", equalTo("/foo/bar")));
+ assertThat(request.toString(), containsString("AAjw****jser"));
+ assertFalse(request.toString().matches(".*AAjwtDEF123456\\.ZZZfere43535\\"
+ + ".yYYYY98764awarkfajser.*"));
+ assertThat(request.toString(), containsString("""
+ "method":"POST",
+ """));
+ assertThat(request.toString(), containsString("""
+ "path":"/foo/bar",
+ """));
}
}
diff --git a/webapp-example/build.gradle b/webapp-example/build.gradle
index d79db09..c13259a 100644
--- a/webapp-example/build.gradle
+++ b/webapp-example/build.gradle
@@ -51,8 +51,8 @@ dependencies {
// as to emulate an actual project using jwt-opa externally.
// Uncomment the following line (and comment out the one below) to use the local version
// while developing.
- // implementation project (':jwt-opa')
- implementation "com.alertavert:jwt-opa:${jwtOpaVersion}"
+ implementation project (':jwt-opa')
+// implementation "com.alertavert:jwt-opa:${jwtOpaVersion}"
// For the @PostConstruct annotation
implementation 'javax.annotation:javax.annotation-api:1.3.2'
diff --git a/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java b/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java
index 6200f7e..680691e 100644
--- a/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java
+++ b/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java
@@ -19,15 +19,9 @@
package com.alertavert.opademo.api;
import com.alertavert.opa.jwt.JwtTokenProvider;
-import com.alertavert.opa.security.crypto.KeypairReader;
import com.alertavert.opademo.data.ReactiveUsersRepository;
import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.extern.jackson.Jacksonized;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.bind.annotation.GetMapping;
@@ -36,13 +30,11 @@
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
-import java.security.KeyPair;
import java.util.List;
import java.util.Objects;
import static com.alertavert.opa.Constants.API_TOKEN;
import static com.alertavert.opa.Constants.BEARER_TOKEN;
-import static com.alertavert.opa.Constants.MAX_TOKEN_LEN_LOG;
@Slf4j
@RestController
@@ -69,8 +61,8 @@ public Mono> getToken(@PathVariable String user) {
})
.map(ResponseEntity::ok)
.doOnSuccess(response -> log.debug(
- "API Token successfully created, user = {}, token = {}...", user,
- Objects.requireNonNull(response.getBody()).apiToken.substring(0, MAX_TOKEN_LEN_LOG)))
+ "API Token successfully created, user = {}, token = {}", user,
+ JwtTokenProvider.maskToken(Objects.requireNonNull(response.getBody()).apiToken)))
.onErrorReturn(Exception.class, ResponseEntity.badRequest().build());
}
diff --git a/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java b/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java
index cb09e5b..8a4611b 100644
--- a/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java
+++ b/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java
@@ -38,7 +38,6 @@
import java.util.UUID;
import static com.alertavert.opa.Constants.BASIC_AUTH;
-import static com.alertavert.opa.Constants.MAX_TOKEN_LEN_LOG;
/**
* LoginController
@@ -77,8 +76,9 @@ Mono login(
return new JwtController.ApiToken(u.getUsername(), u.roles(), token);
})
.doOnNext(apiToken ->
- log.debug("User authenticated, user = {}, token = {}...",
- apiToken.username(), apiToken.apiToken().substring(0, MAX_TOKEN_LEN_LOG)));
+ log.debug("User authenticated, user = {}, token = {}",
+ apiToken.username(),
+ JwtTokenProvider.maskToken(apiToken.apiToken())));
}
@GetMapping("/reset/{username}")
diff --git a/webapp-example/src/main/resources/application.yaml b/webapp-example/src/main/resources/application.yaml
index 3150964..d65f3d2 100644
--- a/webapp-example/src/main/resources/application.yaml
+++ b/webapp-example/src/main/resources/application.yaml
@@ -30,7 +30,7 @@ opa:
#
# NOTE: this is a quirk of Spring Boot: configure this with a comma-separated list
# NOT a YAML array, as that will NOT work as expected.
- headers: x-demoapp-auth, Accept-Encoding
+ headers: x-demoapp-auth, Accept-Encoding, Authorization
db:
server: localhost