Skip to content

Commit 55dec2b

Browse files
committed
Surface Set-Cookie and remembered_device; mask secret headers
Support the Shibboleth plugin's API-client "remember this device" feature: - PIResponse: add rememberedDevice (parsed from detail.remembered_device) and setCookieHeaders (the raw Set-Cookie response headers, which the JSON body does not carry). - Endpoint/AsyncRequestCallable/PrivacyIDEA: capture the response Set-Cookie headers and thread them out via a new submitRequest() so validateCheck can attach them to the returned PIResponse. - Endpoint: never log the X-API-Key or Cookie header values (masked like pass); the full API key was previously written to the header dump. - PrivacyIDEA: log the "no service account configured" message at info level instead of error (it is a normal configuration). - Bump version to 1.5.2.
1 parent a0c4de7 commit 55dec2b

7 files changed

Lines changed: 56 additions & 10 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>org.privacyidea</groupId>
77
<artifactId>privacyidea-java-client</artifactId>
8-
<version>1.5.1</version>
8+
<version>1.5.2</version>
99
<packaging>jar</packaging>
1010
<properties>
1111
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

src/main/java/org/privacyidea/AsyncRequestCallable.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ public class AsyncRequestCallable implements Callable<String>, Callback
4141
private final Endpoint endpoint;
4242
private final PrivacyIDEA privacyIDEA;
4343
final String[] callbackResult = {null};
44+
// Body returned by call() (mirrors callbackResult[0], or null on timeout/failure) and the raw
45+
// Set-Cookie response header(s). Populated on the request thread, read after the Future completes.
46+
String body = null;
47+
final java.util.List<String> setCookies = new java.util.ArrayList<>();
4448
private CountDownLatch latch;
4549

4650
public AsyncRequestCallable(PrivacyIDEA privacyIDEA, Endpoint endpoint, String path, Map<String, String> params,
@@ -82,6 +86,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO
8286
// Using try-with-resources guarantees the body is properly closed after reading.
8387
try (ResponseBody responseBody = response.body())
8488
{
89+
setCookies.addAll(response.headers("Set-Cookie"));
8590
if (responseBody != null)
8691
{
8792
String s = responseBody.string();

src/main/java/org/privacyidea/Endpoint.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@
3131
import javax.net.ssl.X509TrustManager;
3232
import okhttp3.Callback;
3333
import okhttp3.FormBody;
34+
import okhttp3.Headers;
3435
import okhttp3.HttpUrl;
3536
import okhttp3.OkHttpClient;
3637
import okhttp3.Request;
3738

3839
import static org.privacyidea.PIConstants.GET;
40+
import static org.privacyidea.PIConstants.HEADER_COOKIE;
3941
import static org.privacyidea.PIConstants.HEADER_USER_AGENT;
42+
import static org.privacyidea.PIConstants.HEADER_X_API_KEY;
4043
import static org.privacyidea.PIConstants.POST;
4144
import static org.privacyidea.PIConstants.WEBAUTHN_PARAMETERS;
4245

@@ -193,7 +196,20 @@ void sendRequestAsync(String endpoint, Map<String, String> params, Map<String, S
193196
}
194197

195198
Request request = requestBuilder.build();
196-
privacyIDEA.log("Header: " + request.headers().toString().replace("\n", " | "));
199+
// Log headers, but never the secret values (API key, session cookie).
200+
Headers reqHeaders = request.headers();
201+
StringBuilder headerLog = new StringBuilder("Header: ");
202+
for (int i = 0; i < reqHeaders.size(); i++)
203+
{
204+
String name = reqHeaders.name(i);
205+
String value = reqHeaders.value(i);
206+
if (HEADER_X_API_KEY.equalsIgnoreCase(name) || HEADER_COOKIE.equalsIgnoreCase(name))
207+
{
208+
value = "<hidden>";
209+
}
210+
headerLog.append(name).append(": ").append(value).append(" | ");
211+
}
212+
privacyIDEA.log(headerLog.toString());
197213
client.newCall(request).enqueue(callback);
198214
}
199215
}

src/main/java/org/privacyidea/JSONParser.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ else if ("interactive".equals(modeFromResponse))
244244
response.otpLength = getInt(detail, OTPLEN);
245245
response.isEnrollViaMultichallenge = getBoolean(detail, "enroll_via_multichallenge");
246246
response.isEnrollViaMultichallengeOptional = getBoolean(detail, "enroll_via_multichallenge_optional");
247+
response.rememberedDevice = getBoolean(detail, "remembered_device");
247248
// The enrollment link can be in the detail or in one of the
248249
JsonObject passkeyChallenge = detail.getAsJsonObject(PASSKEY);
249250
if (passkeyChallenge != null && !passkeyChallenge.isJsonNull())

src/main/java/org/privacyidea/PIConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public class PIConstants
3636
public static final String HEADER_ORIGIN = "Origin";
3737
public static final String HEADER_AUTHORIZATION = "Authorization";
3838
public static final String HEADER_USER_AGENT = "User-Agent";
39+
// Secret headers whose values must never be written to the log.
40+
public static final String HEADER_X_API_KEY = "X-API-Key";
41+
public static final String HEADER_COOKIE = "Cookie";
3942

4043
// TOKEN TYPES / CONTAINER
4144
public static final String TOKEN_TYPE_PUSH = "push";

src/main/java/org/privacyidea/PIResponse.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ public class PIResponse
7070
public String webAuthnSignRequest = "";
7171
public String webAuthnTransactionId = "";
7272

73+
// Remember-this-device: detail.remembered_device reports whether a presented persistent-session
74+
// cookie was recognised. setCookieHeaders carries the raw Set-Cookie response header(s) (e.g. the
75+
// rotated pi_remember_device cookie) which the JSON body does not contain; it is populated by
76+
// PrivacyIDEA#validateCheck after parsing, not by fromJSON.
77+
public boolean rememberedDevice = false;
78+
public transient List<String> setCookieHeaders = new ArrayList<>();
79+
7380
public boolean authenticationSuccessful()
7481
{
7582
if (authentication == AuthenticationStatus.ACCEPT && (multiChallenge == null || multiChallenge.isEmpty()))

src/main/java/org/privacyidea/PrivacyIDEA.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.Objects;
2727
import java.util.concurrent.ArrayBlockingQueue;
2828
import java.util.concurrent.BlockingQueue;
29-
import java.util.concurrent.Callable;
3029
import java.util.concurrent.CountDownLatch;
3130
import java.util.concurrent.ExecutionException;
3231
import java.util.concurrent.Executors;
@@ -95,7 +94,7 @@ private PrivacyIDEA(PIConfig configuration, IPILogger logger, IPISimpleLogger si
9594
}
9695
else
9796
{
98-
error("No service account configured. No JWT will be retrieved.");
97+
log("No service account configured. No JWT will be retrieved.");
9998
}
10099
}
101100

@@ -209,8 +208,14 @@ private PIResponse getPIResponse(String type, String input, String pass, Map<Str
209208
{
210209
params.put(TRANSACTION_ID, transactionID);
211210
}
212-
String response = runRequestAsync(ENDPOINT_VALIDATE_CHECK, params, headers, false, POST);
213-
return this.parser.parsePIResponse(response);
211+
AsyncRequestCallable callable = submitRequest(ENDPOINT_VALIDATE_CHECK, params, headers, false, POST);
212+
PIResponse piResponse = this.parser.parsePIResponse(callable.body);
213+
if (piResponse != null)
214+
{
215+
// The rotated pi_remember_device cookie lives in the Set-Cookie response header, not the body.
216+
piResponse.setCookieHeaders = callable.setCookies;
217+
}
218+
return piResponse;
214219
}
215220

216221
/**
@@ -588,24 +593,33 @@ public boolean serviceAccountAvailable()
588593
*/
589594
private String runRequestAsync(String path, Map<String, String> params, Map<String, String> headers, boolean authorizationRequired,
590595
String method)
596+
{
597+
return submitRequest(path, params, headers, authorizationRequired, method).body;
598+
}
599+
600+
/**
601+
* Like {@link #runRequestAsync} but returns the completed callable so callers can also read
602+
* response metadata (e.g. Set-Cookie headers) that the plain body string does not carry.
603+
*/
604+
private AsyncRequestCallable submitRequest(String path, Map<String, String> params, Map<String, String> headers,
605+
boolean authorizationRequired, String method)
591606
{
592607
if (authorizationRequired)
593608
{
594609
// Wait for the JWT to be retrieved and add it to the header
595610
headers.put(PIConstants.HEADER_AUTHORIZATION, getJWT());
596611
}
597-
Callable<String> callable = new AsyncRequestCallable(this, this.endpoint, path, params, headers, method);
612+
AsyncRequestCallable callable = new AsyncRequestCallable(this, this.endpoint, path, params, headers, method);
598613
Future<String> future = this.threadPool.submit(callable);
599-
String response = null;
600614
try
601615
{
602-
response = future.get();
616+
callable.body = future.get();
603617
}
604618
catch (InterruptedException | ExecutionException e)
605619
{
606620
log("runRequestAsync: " + e.getLocalizedMessage());
607621
}
608-
return response;
622+
return callable;
609623
}
610624

611625
/**

0 commit comments

Comments
 (0)