From b336289dc4130ed229762bc74d50b4c334e6fff1 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Mon, 23 Mar 2026 15:22:53 -0400 Subject: [PATCH] chore: Add masking for the generic log method in Slf4jUtils --- .../auth/oauth2/Slf4jLoggingHelpers.java | 18 ++++- .../com/google/auth/oauth2/LoggingTest.java | 80 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/Slf4jLoggingHelpers.java b/oauth2_http/java/com/google/auth/oauth2/Slf4jLoggingHelpers.java index 880cbb43e..b10328661 100644 --- a/oauth2_http/java/com/google/auth/oauth2/Slf4jLoggingHelpers.java +++ b/oauth2_http/java/com/google/auth/oauth2/Slf4jLoggingHelpers.java @@ -115,8 +115,8 @@ static void logResponse(HttpResponse response, LoggerProvider loggerProvider, St responseLogDataMap.put("response.status", String.valueOf(response.getStatusCode())); responseLogDataMap.put("response.status.message", response.getStatusMessage()); - Map headers = new HashMap<>(response.getHeaders()); - responseLogDataMap.put("response.headers", headers.toString()); + Map headers = parseGenericData(response.getHeaders()); + responseLogDataMap.put("response.headers", gson.toJson(headers)); Slf4jUtils.log(logger, org.slf4j.event.Level.INFO, responseLogDataMap, message); } } catch (Exception e) { @@ -138,12 +138,24 @@ static void logResponsePayload( } } + /** + * Generic log method for non-standard request/response/payload logging. + * + *

Any key in the provided {@code contextMap} that matches the {@code SENSITIVE_KEYS} set will + * have its value masked via SHA-256 hash before being logged. + * + * @param loggerProvider the logger provider for the calling class + * @param level the java.util.logging level to map to SLF4J + * @param contextMap the key-value pairs to log + * @param message the log message + */ static void log( LoggerProvider loggerProvider, Level level, Map contextMap, String message) { try { Logger logger = loggerProvider.getLogger(); org.slf4j.event.Level slf4jLevel = matchUtilLevelToSLF4JLevel(level); - Slf4jUtils.log(logger, slf4jLevel, contextMap, message); + Map maskedContextMap = parseGenericData(contextMap); + Slf4jUtils.log(logger, slf4jLevel, maskedContextMap, message); } catch (Exception e) { // let logging fail silently } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java b/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java index 44c9ab3bc..524a312ce 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java @@ -627,6 +627,8 @@ void stsRequestHandler_exchangeToken_masksSensitiveTokens() throws IOException { testAppender.stop(); } + // We specifically test ImpersonatedCredentials here because it constructs its HTTP requests + // using JsonHttpContent, unlike most other credentials which use UrlEncodedContent. @Test void impersonatedCredentials_exchangeToken_masksSensitiveTokens() throws IOException, IllegalStateException { @@ -650,7 +652,21 @@ void impersonatedCredentials_exchangeToken_masksSensitiveTokens() assertEquals(3, testAppender.events.size()); - // Verify response payload has tokens masked + // 1. Verify request log contains properly formatted payload (JsonHttpContent masking) + ILoggingEvent requestLog = testAppender.events.get(0); + assertEquals("Sending request to refresh access token", requestLog.getMessage()); + String requestPayload = null; + for (KeyValuePair kvp : requestLog.getKeyValuePairs()) { + if ("request.payload".equals(kvp.key)) { + requestPayload = (String) kvp.value; + } + } + // When logged at DEBUG level, the request payload should be present and valid JSON. + if (requestPayload != null) { + assertTrue(isValidJson(requestPayload), "Request payload should be valid JSON"); + } + + // 2. Verify response payload has tokens masked assertEquals("Response payload for access token", testAppender.events.get(2).getMessage()); boolean foundAccessToken = false; for (KeyValuePair kvp : testAppender.events.get(2).getKeyValuePairs()) { @@ -669,4 +685,66 @@ void impersonatedCredentials_exchangeToken_masksSensitiveTokens() assertTrue(foundAccessToken, "Expected accessToken in response payload logs"); testAppender.stop(); } + + // We specifically use ImpersonatedCredentials for this test because its request payload + // is formatted using JsonHttpContent, whereas other credentials primarily use UrlEncodedContent. + @Test + void impersonatedCredentials_requestPayload_masksJsonHttpContentSensitiveKeys() + throws IOException, IllegalStateException { + // Set DEBUG level to ensure request payloads are logged + Logger logger = LoggerFactory.getLogger(ImpersonatedCredentials.class); + ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger; + ch.qos.logback.classic.Level previousLevel = logbackLogger.getLevel(); + logbackLogger.setLevel(ch.qos.logback.classic.Level.DEBUG); + + TestAppender testAppender = new TestAppender(); + testAppender.start(); + logbackLogger.addAppender(testAppender); + + try { + MockIAMCredentialsServiceTransportFactory mockTransportFactory = + new MockIAMCredentialsServiceTransportFactory(); + mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); + mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); + mockTransportFactory.getTransport().setExpireTime(getDefaultExpireTime()); + mockTransportFactory + .getTransport() + .addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); + ImpersonatedCredentials targetCredentials = + ImpersonatedCredentials.create( + ImpersonatedCredentialsTest.getSourceCredentials(), + IMPERSONATED_CLIENT_EMAIL, + null, + IMMUTABLE_SCOPES_LIST, + VALID_LIFETIME, + mockTransportFactory); + + targetCredentials.refreshAccessToken(); + + // Find the request log event + ILoggingEvent requestLog = testAppender.events.get(0); + assertEquals("Sending request to refresh access token", requestLog.getMessage()); + + // Extract request.payload + String requestPayload = null; + for (KeyValuePair kvp : requestLog.getKeyValuePairs()) { + if ("request.payload".equals(kvp.key)) { + requestPayload = (String) kvp.value; + } + } + + // At DEBUG level, request payload must be present + assertNotNull(requestPayload, "Request payload should be logged at DEBUG level"); + assertTrue(isValidJson(requestPayload), "Request payload should be valid JSON"); + + // The request payload uses JsonHttpContent with fields: delegates, scope, lifetime. None of + // these are in SENSITIVE_KEYS, so they should appear as-is (not hashed). + assertFalse( + requestPayload.contains("\"delegates\":null"), + "Payload should be properly serialized from JsonHttpContent"); + } finally { + logbackLogger.setLevel(previousLevel); + testAppender.stop(); + } + } }