Skip to content

Commit dee0fb8

Browse files
committed
Mask access token in logs
1 parent 039c640 commit dee0fb8

File tree

3 files changed

+140
-1
lines changed

3 files changed

+140
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright © 2012 The Feign Authors ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign;
17+
18+
import java.util.Collection;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.regex.Pattern;
22+
import java.util.stream.Collectors;
23+
24+
public final class ConfidentialLogger extends Logger {
25+
private static final String AUTHORIZATION_HEADER = "Authorization";
26+
private static final Pattern ACCESS_TOKEN_PATTERN =
27+
Pattern.compile("^Bearer [A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$");
28+
29+
private final Logger delegate;
30+
31+
public ConfidentialLogger(final Logger delegate) {
32+
this.delegate = delegate;
33+
}
34+
35+
@Override
36+
protected void logRequest(final String configKey, final Level logLevel, final Request request) {
37+
final Map<String, Collection<String>> filteredHeaders = filterHeaders(request.headers());
38+
39+
final Request copyRequest =
40+
Request.create(
41+
request.httpMethod(),
42+
request.url(),
43+
filteredHeaders,
44+
request.body(),
45+
request.charset(),
46+
request.requestTemplate());
47+
48+
super.logRequest(configKey, logLevel, copyRequest);
49+
}
50+
51+
@Override
52+
protected void log(final String configKey, final String format, final Object... args) {
53+
delegate.log(configKey, format, args);
54+
}
55+
56+
static Map<String, Collection<String>> filterHeaders(
57+
final Map<String, Collection<String>> headers) {
58+
final Map<String, Collection<String>> filteredHeaders = new HashMap<>(headers);
59+
filteredHeaders.computeIfPresent(
60+
AUTHORIZATION_HEADER,
61+
(ignored, values) ->
62+
values.stream()
63+
.map(
64+
value ->
65+
ACCESS_TOKEN_PATTERN.matcher(value).matches()
66+
? "Bearer <access token>"
67+
: value)
68+
.collect(Collectors.toList()));
69+
return filteredHeaders;
70+
}
71+
}

oauth2/src/main/java/feign/auth/oauth2/OAuth2Authentication.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,19 @@
2525
import java.time.temporal.ChronoUnit;
2626
import java.util.concurrent.ExecutionException;
2727
import java.util.concurrent.TimeoutException;
28+
import java.util.logging.Level;
2829

2930
public class OAuth2Authentication implements Capability {
31+
private static final java.util.logging.Logger JAVA_LOGGER =
32+
java.util.logging.Logger.getLogger(OAuth2Authentication.class.getName());
33+
3034
protected ClientRegistration clientRegistration;
3135
protected OAuth2IDPClient idpClient;
3236

3337
protected AsyncClient<Object> httpClient;
3438
protected Request.Options httpOptions;
3539
protected Decoder jsonDecoder;
40+
protected Logger logger;
3641

3742
private OAuth2TokenResponse oAuth2TokenResponse = null;
3843
private Instant expiresAt = null;
@@ -65,6 +70,12 @@ public Decoder enrich(final Decoder decoder) {
6570
return decoder;
6671
}
6772

73+
@Override
74+
public Logger enrich(final Logger logger) {
75+
this.logger = new ConfidentialLogger(logger);
76+
return this.logger;
77+
}
78+
6879
@Override
6980
public <B extends BaseBuilder<B, T>, T> B beforeBuild(final B baseBuilder) {
7081
if (httpClient == null) {
@@ -84,12 +95,14 @@ public <B extends BaseBuilder<B, T>, T> B beforeBuild(final B baseBuilder) {
8495
return baseBuilder
8596
.requestInterceptor(new AuthenticationInterceptor())
8697
.retryer(new UnauthorizedRetryer())
87-
.errorDecoder(UnauthorizedErrorDecoder.INSTANCE);
98+
.errorDecoder(UnauthorizedErrorDecoder.INSTANCE)
99+
.logger(this.logger);
88100
}
89101

90102
private synchronized String getAccessToken() {
91103
if (expiresAt != null && expiresAt.minus(10, ChronoUnit.SECONDS).isBefore(Instant.now())) {
92104
// Access token is expired or about to expire
105+
JAVA_LOGGER.log(Level.INFO, "Access token is about to be expired. Refreshing token.");
93106
expiresAt = null;
94107
oAuth2TokenResponse = null;
95108
}
@@ -102,6 +115,8 @@ private synchronized String getAccessToken() {
102115
}
103116

104117
private synchronized String forceAuthentication() {
118+
JAVA_LOGGER.log(Level.INFO, "Perform authentication against IDP.");
119+
105120
try {
106121
oAuth2TokenResponse =
107122
idpClient
@@ -136,9 +151,14 @@ public void continueOrPropagate(final RetryableException unauthorizedException)
136151
}
137152

138153
if (reauthenticated) {
154+
JAVA_LOGGER.log(
155+
Level.WARNING,
156+
"Client still unauthorized event after access token was updated. Fail request.");
139157
throw unauthorizedException;
140158
}
141159

160+
JAVA_LOGGER.log(
161+
Level.INFO, "Request was unauthorized by Resource Server. Refresh access token.");
142162
final String accessToken = forceAuthentication();
143163

144164
final RequestTemplate requestTemplate = unauthorizedException.request().requestTemplate();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright © 2012 The Feign Authors ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.Map;
23+
import org.junit.jupiter.api.Test;
24+
25+
public class ConfidentialLoggerTest {
26+
27+
@Test
28+
void testFilterHeaders() {
29+
30+
/* Given */
31+
Map<String, Collection<String>> headers =
32+
Collections.singletonMap(
33+
"Authorization",
34+
Collections.singleton(
35+
"Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ0dlhpald3X0tWSnZHY1B5N25mUlg4SjBpRDcxSTZEUWVFR0VjczJwbWxRIn0.eyJleHAiOjE3NDA1MDA0MzgsImlhdCI6MTc0MDUwMDQyOCwianRpIjoiMzljOTVhNzAtNDk4Yi00MWFjLTlmMTctOTA3Yjk4MmIzNDNkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozMjc5MC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImMzZjU5ZjA3LWE4NTItNGY1Ny1hZjEwLTI0OGJjNjg4YWMxYSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImZlaWduLWNsaWVudC1zZWNyZXQiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtZmVpZ24tY2xpZW50LXNlY3JldCIsImNsaWVudEFkZHJlc3MiOiIxNzIuMTcuMC4xIiwiY2xpZW50X2lkIjoiZmVpZ24tY2xpZW50LXNlY3JldCJ9.mcRl0ex7p4bPd-Kk1KwJoFOWYfkxwtEmAO9X9kdgGq4iCY6UUWGINqYXwI_D0QObclJ9J2ka9qCxo225MfV-zmza60IC3w6tfgsm7mnEZgec47GSoQjUqTLna4pDGdLq4c9QIedzkrhLqI9_qJi1V6iGYd6CNb6Y1u0G0QBoLejzHGVf5avxrlrRHTkGMUvphe7N0WAq5N9JjFrB6pqFsL1a9gMBkyThM6SpOwe1O2rXA07J7IgcL50AHU-4MxXRroz779GYObhm7o9RY7iPgs0BlBjVKxj75R8R57YNJo0LEPqBuCn5tAD7VJRPgCrM91Jfdv4X7mrg39JIndsyAw"));
36+
37+
/* When */
38+
Map<String, Collection<String>> filteredHeaders = ConfidentialLogger.filterHeaders(headers);
39+
40+
/* Then */
41+
assertThat(filteredHeaders).isNotNull().isNotEmpty().hasSize(1).containsKey("Authorization");
42+
assertThat(filteredHeaders.get("Authorization"))
43+
.isNotNull()
44+
.isNotEmpty()
45+
.hasSize(1)
46+
.contains("Bearer <access token>");
47+
}
48+
}

0 commit comments

Comments
 (0)