Skip to content

Commit fcd4c3c

Browse files
committed
chore: Add additional warnings for sensitive tokens
1 parent 1a81a7e commit fcd4c3c

8 files changed

Lines changed: 133 additions & 5 deletions

File tree

oauth2_http/java/com/google/auth/oauth2/AccessToken.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ public int hashCode() {
114114
return Objects.hash(tokenValue, expirationTimeMillis, scopes);
115115
}
116116

117+
/**
118+
* Returns a string representation of this access token, including the raw token value.
119+
*
120+
* <p><b>Security Warning:</b> The output of this method includes the raw, unmasked access token
121+
* value. Do not log this output in production environments as it may expose sensitive
122+
* credentials.
123+
*/
117124
@Override
118125
public String toString() {
119126
return MoreObjects.toStringHelper(this)

oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,13 @@ public int hashCode() {
284284
quotaProjectId);
285285
}
286286

287+
/**
288+
* Returns a string representation of this credential.
289+
*
290+
* <p><b>Security Warning:</b> The output of this method includes sensitive fields such as the
291+
* client secret, refresh token, and request metadata containing the raw Bearer access token. Do
292+
* not log this output in production environments as it may expose sensitive credentials.
293+
*/
287294
@Override
288295
public String toString() {
289296
return MoreObjects.toStringHelper(this)

oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,13 @@ public int hashCode() {
710710
iamEndpointOverride);
711711
}
712712

713+
/**
714+
* Returns a string representation of this credential.
715+
*
716+
* <p><b>Security Warning:</b> The output of this method includes the source credentials which may
717+
* recursively contain sensitive fields such as access tokens. Do not log this output in production
718+
* environments as it may expose sensitive credentials.
719+
*/
713720
@Override
714721
public String toString() {
715722
return MoreObjects.toStringHelper(this)

oauth2_http/java/com/google/auth/oauth2/LoggingUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ static void logResponsePayload(
7979
/**
8080
* Generic log method to use when not logging standard request, response and payload.
8181
*
82-
* <p>Note: This does not mask the data. Log carefully if the data contains sensitive tokens.
82+
* <p>Any key in the provided {@code contextMap} that matches the sensitive keys set (e.g.
83+
* access_token, refresh_token) will have its value masked via SHA-256 hash before being logged.
8384
*/
8485
static void log(
8586
LoggerProvider loggerProvider, Level level, Map<String, Object> contextMap, String message) {

oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,14 @@ protected Map<String, List<String>> getRequestMetadataInternal() {
446446
return null;
447447
}
448448

449+
/**
450+
* Returns a string representation of this credential, including request metadata and access
451+
* token.
452+
*
453+
* <p><b>Security Warning:</b> The output of this method includes the request metadata which
454+
* contains the raw Bearer access token, and the raw access token value. Do not log this output in
455+
* production environments as it may expose sensitive credentials.
456+
*/
449457
@Override
450458
public String toString() {
451459
OAuthValue localValue = value;

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
}

oauth2_http/java/com/google/auth/oauth2/UserCredentials.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,13 @@ public int hashCode() {
361361
quotaProjectId);
362362
}
363363

364+
/**
365+
* Returns a string representation of this credential.
366+
*
367+
* <p><b>Security Warning:</b> The output of this method includes sensitive fields such as the
368+
* refresh token and request metadata containing the raw Bearer access token. Do not log this
369+
* output in production environments as it may expose sensitive credentials.
370+
*/
364371
@Override
365372
public String toString() {
366373
return MoreObjects.toStringHelper(this)

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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,23 @@ void impersonatedCredentials_exchangeToken_masksSensitiveTokens()
650650

651651
assertEquals(3, testAppender.events.size());
652652

653-
// Verify response payload has tokens masked
653+
// 1. Verify request log contains properly formatted payload (JsonHttpContent masking)
654+
ILoggingEvent requestLog = testAppender.events.get(0);
655+
assertEquals(
656+
"Sending request to refresh access token", requestLog.getMessage());
657+
String requestPayload = null;
658+
for (KeyValuePair kvp : requestLog.getKeyValuePairs()) {
659+
if ("request.payload".equals(kvp.key)) {
660+
requestPayload = (String) kvp.value;
661+
}
662+
}
663+
// When logged at DEBUG level, the request payload should be present and valid JSON
664+
// (the JsonHttpContent payload goes through parseGenericData for masking)
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,67 @@ void impersonatedCredentials_exchangeToken_masksSensitiveTokens()
669685
assertTrue(foundAccessToken, "Expected accessToken in response payload logs");
670686
testAppender.stop();
671687
}
688+
689+
@Test
690+
void impersonatedCredentials_requestPayload_masksJsonHttpContentSensitiveKeys()
691+
throws IOException, IllegalStateException {
692+
// Set DEBUG level to ensure request payloads are logged
693+
Logger logger = LoggerFactory.getLogger(ImpersonatedCredentials.class);
694+
ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger;
695+
ch.qos.logback.classic.Level previousLevel = logbackLogger.getLevel();
696+
logbackLogger.setLevel(ch.qos.logback.classic.Level.DEBUG);
697+
698+
TestAppender testAppender = new TestAppender();
699+
testAppender.start();
700+
logbackLogger.addAppender(testAppender);
701+
702+
try {
703+
MockIAMCredentialsServiceTransportFactory mockTransportFactory =
704+
new MockIAMCredentialsServiceTransportFactory();
705+
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
706+
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
707+
mockTransportFactory.getTransport().setExpireTime(getDefaultExpireTime());
708+
mockTransportFactory
709+
.getTransport()
710+
.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
711+
ImpersonatedCredentials targetCredentials =
712+
ImpersonatedCredentials.create(
713+
ImpersonatedCredentialsTest.getSourceCredentials(),
714+
IMPERSONATED_CLIENT_EMAIL,
715+
null,
716+
IMMUTABLE_SCOPES_LIST,
717+
VALID_LIFETIME,
718+
mockTransportFactory);
719+
720+
targetCredentials.refreshAccessToken();
721+
722+
// Find the request log event
723+
ILoggingEvent requestLog = testAppender.events.get(0);
724+
assertEquals("Sending request to refresh access token", requestLog.getMessage());
725+
726+
// Extract request.payload
727+
String requestPayload = null;
728+
for (KeyValuePair kvp : requestLog.getKeyValuePairs()) {
729+
if ("request.payload".equals(kvp.key)) {
730+
requestPayload = (String) kvp.value;
731+
}
732+
}
733+
734+
// At DEBUG level, request payload must be present
735+
assertNotNull(requestPayload, "Request payload should be logged at DEBUG level");
736+
assertTrue(isValidJson(requestPayload), "Request payload should be valid JSON");
737+
738+
// The ImpersonatedCredentials request payload uses JsonHttpContent with fields:
739+
// delegates, scope, lifetime. None of these are in SENSITIVE_KEYS, so they should
740+
// appear as-is (not hashed). This validates that JsonHttpContent goes through
741+
// parseGenericData without breaking.
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
}
751+

0 commit comments

Comments
 (0)