Skip to content

Commit cc333fe

Browse files
authored
chore: Add masking for the generic log method in Slf4jUtils (#1906)
1 parent cb44cb7 commit cc333fe

2 files changed

Lines changed: 94 additions & 4 deletions

File tree

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/Slf4jLoggingHelpers.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ static void logResponse(HttpResponse response, LoggerProvider loggerProvider, St
115115
responseLogDataMap.put("response.status", String.valueOf(response.getStatusCode()));
116116
responseLogDataMap.put("response.status.message", response.getStatusMessage());
117117

118-
Map<String, Object> headers = new HashMap<>(response.getHeaders());
119-
responseLogDataMap.put("response.headers", headers.toString());
118+
Map<String, Object> headers = parseGenericData(response.getHeaders());
119+
responseLogDataMap.put("response.headers", gson.toJson(headers));
120120
Slf4jUtils.log(logger, org.slf4j.event.Level.INFO, responseLogDataMap, message);
121121
}
122122
} catch (Exception e) {
@@ -138,12 +138,24 @@ static void logResponsePayload(
138138
}
139139
}
140140

141+
/**
142+
* Generic log method for non-standard request/response/payload logging.
143+
*
144+
* <p>Any key in the provided {@code contextMap} that matches the {@code SENSITIVE_KEYS} set will
145+
* have its value masked via SHA-256 hash before being logged.
146+
*
147+
* @param loggerProvider the logger provider for the calling class
148+
* @param level the java.util.logging level to map to SLF4J
149+
* @param contextMap the key-value pairs to log
150+
* @param message the log message
151+
*/
141152
static void log(
142153
LoggerProvider loggerProvider, Level level, Map<String, Object> contextMap, String message) {
143154
try {
144155
Logger logger = loggerProvider.getLogger();
145156
org.slf4j.event.Level slf4jLevel = matchUtilLevelToSLF4JLevel(level);
146-
Slf4jUtils.log(logger, slf4jLevel, contextMap, message);
157+
Map<String, Object> maskedContextMap = parseGenericData(contextMap);
158+
Slf4jUtils.log(logger, slf4jLevel, maskedContextMap, message);
147159
} catch (Exception e) {
148160
// let logging fail silently
149161
}

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,8 @@ void stsRequestHandler_exchangeToken_masksSensitiveTokens() throws IOException {
627627
testAppender.stop();
628628
}
629629

630+
// We specifically test ImpersonatedCredentials here because it constructs its HTTP requests
631+
// using JsonHttpContent, unlike most other credentials which use UrlEncodedContent.
630632
@Test
631633
void impersonatedCredentials_exchangeToken_masksSensitiveTokens()
632634
throws IOException, IllegalStateException {
@@ -650,7 +652,21 @@ void impersonatedCredentials_exchangeToken_masksSensitiveTokens()
650652

651653
assertEquals(3, testAppender.events.size());
652654

653-
// Verify response payload has tokens masked
655+
// 1. Verify request log contains properly formatted payload (JsonHttpContent masking)
656+
ILoggingEvent requestLog = testAppender.events.get(0);
657+
assertEquals("Sending request to refresh access token", requestLog.getMessage());
658+
String requestPayload = null;
659+
for (KeyValuePair kvp : requestLog.getKeyValuePairs()) {
660+
if ("request.payload".equals(kvp.key)) {
661+
requestPayload = (String) kvp.value;
662+
}
663+
}
664+
// When logged at DEBUG level, the request payload should be present and valid JSON.
665+
if (requestPayload != null) {
666+
assertTrue(isValidJson(requestPayload), "Request payload should be valid JSON");
667+
}
668+
669+
// 2. Verify response payload has tokens masked
654670
assertEquals("Response payload for access token", testAppender.events.get(2).getMessage());
655671
boolean foundAccessToken = false;
656672
for (KeyValuePair kvp : testAppender.events.get(2).getKeyValuePairs()) {
@@ -669,4 +685,66 @@ void impersonatedCredentials_exchangeToken_masksSensitiveTokens()
669685
assertTrue(foundAccessToken, "Expected accessToken in response payload logs");
670686
testAppender.stop();
671687
}
688+
689+
// We specifically use ImpersonatedCredentials for this test because its request payload
690+
// is formatted using JsonHttpContent, whereas other credentials primarily use UrlEncodedContent.
691+
@Test
692+
void impersonatedCredentials_requestPayload_masksJsonHttpContentSensitiveKeys()
693+
throws IOException, IllegalStateException {
694+
// Set DEBUG level to ensure request payloads are logged
695+
Logger logger = LoggerFactory.getLogger(ImpersonatedCredentials.class);
696+
ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger;
697+
ch.qos.logback.classic.Level previousLevel = logbackLogger.getLevel();
698+
logbackLogger.setLevel(ch.qos.logback.classic.Level.DEBUG);
699+
700+
TestAppender testAppender = new TestAppender();
701+
testAppender.start();
702+
logbackLogger.addAppender(testAppender);
703+
704+
try {
705+
MockIAMCredentialsServiceTransportFactory mockTransportFactory =
706+
new MockIAMCredentialsServiceTransportFactory();
707+
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
708+
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
709+
mockTransportFactory.getTransport().setExpireTime(getDefaultExpireTime());
710+
mockTransportFactory
711+
.getTransport()
712+
.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
713+
ImpersonatedCredentials targetCredentials =
714+
ImpersonatedCredentials.create(
715+
ImpersonatedCredentialsTest.getSourceCredentials(),
716+
IMPERSONATED_CLIENT_EMAIL,
717+
null,
718+
IMMUTABLE_SCOPES_LIST,
719+
VALID_LIFETIME,
720+
mockTransportFactory);
721+
722+
targetCredentials.refreshAccessToken();
723+
724+
// Find the request log event
725+
ILoggingEvent requestLog = testAppender.events.get(0);
726+
assertEquals("Sending request to refresh access token", requestLog.getMessage());
727+
728+
// Extract request.payload
729+
String requestPayload = null;
730+
for (KeyValuePair kvp : requestLog.getKeyValuePairs()) {
731+
if ("request.payload".equals(kvp.key)) {
732+
requestPayload = (String) kvp.value;
733+
}
734+
}
735+
736+
// At DEBUG level, request payload must be present
737+
assertNotNull(requestPayload, "Request payload should be logged at DEBUG level");
738+
assertTrue(isValidJson(requestPayload), "Request payload should be valid JSON");
739+
740+
// The request payload uses JsonHttpContent with fields: delegates, scope, lifetime. None of
741+
// these are in SENSITIVE_KEYS, so they should appear as-is (not hashed).
742+
assertFalse(
743+
requestPayload.contains("\"delegates\":null"),
744+
"Payload should be properly serialized from JsonHttpContent");
745+
} finally {
746+
logbackLogger.setLevel(previousLevel);
747+
testAppender.stop();
748+
}
749+
}
672750
}

0 commit comments

Comments
 (0)