Skip to content

Commit 6384677

Browse files
authored
Teach ApiKeyCredentialsProvider to respond to server auth challenge (#68)
1 parent 3569765 commit 6384677

File tree

8 files changed

+99
-27
lines changed

8 files changed

+99
-27
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
* Remove deprecated method `RowsResponse.getRequiredVersion()`.
1010
* Deprecate `GetUsersCommand` methods `getIncludeDeactivated()` and `setIncludeDeactivated()`; use `getIncludeInactive()`
1111
and `setIncludeInactive()` instead.
12-
* Update Commons Logging, Gradle, Gradle Plugins, HttpClient, and HttpCore versions
1312
* Remove Hamcrest references (unused)
13+
* Update `ApiKeyCredentialsProvider` to respond to server auth challenges with the `apikey` header and use the session
14+
on subsequent requests.
15+
* Update Commons Logging, Gradle, Gradle Plugins, HttpClient, and HttpCore versions
1416

1517
## version 6.0.0
1618
*Released*: 1 December 2023

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ artifactory_contextUrl=https://labkey.jfrog.io/artifactory
77
sourceCompatibility=17
88
targetCompatibility=17
99

10-
gradlePluginsVersion=2.0.0
10+
gradlePluginsVersion=2.2.1
1111

1212
commonsCodecVersion=1.16.0
1313
commonsLoggingVersion=1.3.0
1414

15-
httpclient5Version=5.3
15+
httpclient5Version=5.3.1
1616
httpcore5Version=5.2.4
1717

1818
jsonObjectVersion=20231013

src/org/labkey/remoteapi/ApiKeyCredentialsProvider.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
1919
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
2020
import org.apache.hc.client5.http.protocol.HttpClientContext;
21-
import org.labkey.remoteapi.security.WhoAmICommand;
21+
import org.labkey.remoteapi.security.EnsureLoginCommand;
2222

2323
import java.io.IOException;
2424
import java.net.URI;
@@ -40,13 +40,27 @@ public void configureClientBuilder(URI baseURI, HttpClientBuilder builder)
4040
@Override
4141
public void configureRequest(URI baseURI, HttpUriRequest request, HttpClientContext httpClientContext)
4242
{
43-
request.setHeader("apikey", _apiKey);
4443
}
4544

4645
@Override
4746
public void initializeConnection(Connection connection) throws IOException, CommandException
4847
{
49-
// No point in calling ensureLogin.api since the server doesn't "log in" the session when using an API key
50-
new WhoAmICommand().execute(connection, "/home");
48+
new EnsureLoginCommand().execute(connection, "/home");
49+
}
50+
51+
@Override
52+
public boolean shouldRetryRequest(CommandException exception, HttpUriRequest request)
53+
{
54+
// Determine if this is a Basic Auth challenge. If so, set the apikey header and retry the request. Subsequent
55+
// requests should use the session. This mimics the behavior of BasicAuthCredentialsProvider.
56+
57+
String authHeaderValue = exception.getAuthHeaderValue();
58+
59+
boolean authChallenge = (null != authHeaderValue && authHeaderValue.startsWith("Basic realm") && "You must log in to view this content.".equals(exception.getMessage()));
60+
61+
if (authChallenge)
62+
request.setHeader("apikey", _apiKey);
63+
64+
return authChallenge;
5165
}
5266
}

src/org/labkey/remoteapi/ApiVersionException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
public class ApiVersionException extends CommandException
2121
{
22-
ApiVersionException(String message, int statusCode, JSONObject jsonProperties, String responseText, String contentType)
22+
ApiVersionException(String message, int statusCode, JSONObject jsonProperties, String responseText, String contentType, String authHeaderValue)
2323
{
24-
super(message, statusCode, jsonProperties, responseText, contentType);
24+
super(message, statusCode, jsonProperties, responseText, contentType, authHeaderValue);
2525
}
2626
}

src/org/labkey/remoteapi/Command.java

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
2121
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
2222
import org.apache.hc.core5.http.Header;
23+
import org.apache.hc.core5.http.HttpHeaders;
24+
import org.apache.hc.core5.http.ProtocolException;
2325
import org.apache.hc.core5.net.URIBuilder;
2426
import org.json.JSONObject;
2527
import org.json.JSONTokener;
28+
import org.labkey.remoteapi.query.SelectRowsCommand;
2629

2730
import java.io.BufferedReader;
2831
import java.io.ByteArrayInputStream;
@@ -44,7 +47,7 @@
4447
/**
4548
* Abstract base class for all API commands. Developers interact with concrete classes that
4649
* extend this class. For example, to select data, create an instance of
47-
* {@link org.labkey.remoteapi.query.SelectRowsCommand}, which provides helpful methods for
50+
* {@link SelectRowsCommand}, which provides helpful methods for
4851
* setting options and obtaining specific result types.
4952
* <p>
5053
* If a developer wishes to invoke actions that are not yet supported with a specialized class
@@ -283,6 +286,19 @@ public String getText() throws IOException
283286
}
284287
}
285288

289+
public String getHeaderValue(String name)
290+
{
291+
Header header = null;
292+
try
293+
{
294+
header = _httpResponse.getHeader(name);
295+
}
296+
catch (ProtocolException ignored)
297+
{
298+
}
299+
return null == header ? null : header.getValue();
300+
}
301+
286302
// Caller is responsible for closing the response
287303
@Override
288304
public void close() throws IOException
@@ -291,27 +307,39 @@ public void close() throws IOException
291307
}
292308
}
293309

294-
/* NOTE: Internal experimental API for handling streaming commands. */
295310
protected Response _execute(Connection connection, String folderPath) throws CommandException, IOException
296311
{
297312
assert null != getControllerName() : "You must set the controller name before executing the command!";
298313
assert null != getActionName() : "You must set the action name before executing the command!";
299-
CloseableHttpResponse httpResponse;
300314

301-
final HttpUriRequest request;
302315
try
303316
{
304317
//construct and initialize the HttpUriRequest
305-
request = getHttpRequest(connection, folderPath);
306-
LogFactory.getLog(Command.class).info("Requesting URL: " + request.getRequestUri());
307-
308-
//execute the request
309-
httpResponse = connection.executeRequest(request, getTimeout());
318+
final HttpUriRequest request = getHttpRequest(connection, folderPath);
319+
try
320+
{
321+
return executeRequest(connection, request);
322+
}
323+
catch (CommandException e)
324+
{
325+
if (connection.getCredentialsProvider().shouldRetryRequest(e, request))
326+
return executeRequest(connection, request);
327+
else
328+
throw e;
329+
}
310330
}
311331
catch (URISyntaxException | AuthenticationException e)
312332
{
313333
throw new CommandException(e.getMessage());
314334
}
335+
}
336+
337+
private Response executeRequest(Connection connection, HttpUriRequest request) throws AuthenticationException, IOException, CommandException
338+
{
339+
LogFactory.getLog(Command.class).info("Requesting URL: " + request.getRequestUri());
340+
341+
//execute the request
342+
CloseableHttpResponse httpResponse = connection.executeRequest(request, getTimeout());
315343

316344
//get the content-type header
317345
Header contentTypeHeader = httpResponse.getFirstHeader("Content-Type");
@@ -322,10 +350,11 @@ protected Response _execute(Connection connection, String folderPath) throws Com
322350

323351
Response response = new Response(httpResponse, contentType, contentLength);
324352
checkThrowError(response);
353+
325354
return response;
326355
}
327356

328-
protected void checkThrowError(Response response) throws IOException, CommandException
357+
private void checkThrowError(Response response) throws IOException, CommandException
329358
{
330359
int status = response.getStatusCode();
331360

@@ -352,6 +381,8 @@ private void throwError(Response r, boolean throwByDefault) throws IOException,
352381
//use the status text as the message by default
353382
String message = null != r.getStatusText() ? r.getStatusText() : "(no status text)";
354383

384+
String authHeaderValue = r.getHeaderValue(HttpHeaders.WWW_AUTHENTICATE);
385+
355386
// This buffers the entire response in memory, which seems OK for API error responses.
356387
String responseText = r.getText();
357388
JSONObject json = null;
@@ -370,15 +401,15 @@ private void throwError(Response r, boolean throwByDefault) throws IOException,
370401
message = json.getString("exception");
371402

372403
if ("org.labkey.api.action.ApiVersionException".equals(json.opt("exceptionClass")))
373-
throw new ApiVersionException(message, r.getStatusCode(), json, responseText, contentType);
404+
throw new ApiVersionException(message, r.getStatusCode(), json, responseText, contentType, authHeaderValue);
374405

375-
throw new CommandException(message, r.getStatusCode(), json, responseText, contentType);
406+
throw new CommandException(message, r.getStatusCode(), json, responseText, contentType, authHeaderValue);
376407
}
377408
}
378409
}
379410

380411
if (throwByDefault)
381-
throw new CommandException(message, r.getStatusCode(), json, responseText, contentType);
412+
throw new CommandException(message, r.getStatusCode(), json, responseText, contentType, authHeaderValue);
382413

383414
// If we didn't encounter an exception property on the json object, save the fully consumed text and parsed json on the Response object
384415
r._json = json;

src/org/labkey/remoteapi/CommandException.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
public class CommandException extends Exception
3939
{
4040
private final String _contentType;
41+
private final String _authHeaderValue;
4142
private final int _statusCode;
4243
private final JSONObject _jsonProperties;
4344
private final String _responseText;
@@ -49,7 +50,7 @@ public class CommandException extends Exception
4950
*/
5051
public CommandException(String message)
5152
{
52-
this(message, 0, null, null, null);
53+
this(message, 0, null, null, null, null);
5354
}
5455

5556
/**
@@ -60,14 +61,16 @@ public CommandException(String message)
6061
* @param jsonProperties The exception property JSONObject (may be null)
6162
* @param responseText The full response text (may be null)
6263
* @param contentType The response content type (may be null)
64+
* @param authHeaderValue The value of the WWW-Authenticate header (may be null)
6365
*/
64-
public CommandException(String message, int statusCode, JSONObject jsonProperties, String responseText, String contentType)
66+
public CommandException(String message, int statusCode, JSONObject jsonProperties, String responseText, String contentType, String authHeaderValue)
6567
{
6668
super(buildMessage(message, statusCode));
6769
_statusCode = statusCode;
6870
_jsonProperties = jsonProperties;
6971
_responseText = responseText;
7072
_contentType = contentType;
73+
_authHeaderValue = authHeaderValue;
7174
}
7275

7376
private static String buildMessage(String message, int statusCode)
@@ -114,6 +117,15 @@ public String getResponseText()
114117
return _responseText;
115118
}
116119

120+
/**
121+
* Returns the value of the WWW-Authenticate header
122+
* @return The value or null
123+
*/
124+
public String getAuthHeaderValue()
125+
{
126+
return _authHeaderValue;
127+
}
128+
117129
/**
118130
* Returns the exception property map, or null if no map was set.
119131
* @return The exception property map or null.

src/org/labkey/remoteapi/Connection.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ CloseableHttpResponse executeRequest(HttpUriRequest request, Integer timeout) th
398398

399399
CloseableHttpClient client = getHttpClient();
400400

401-
// Set the timeout on the request if it is different the client's default
401+
// Set the timeout on the request if it's different from the client's default
402402
if (request instanceof HttpUriRequestBase r && timeout != null && timeout != getTimeout())
403403
{
404404
RequestConfig base = r.getConfig();
@@ -490,6 +490,11 @@ public void setUserAgent(String userAgent)
490490
_userAgent = userAgent;
491491
}
492492

493+
public CredentialsProvider getCredentialsProvider()
494+
{
495+
return _credentialsProvider;
496+
}
497+
493498
/**
494499
* @param name The cookie name
495500
* @param value The cookie value

src/org/labkey/remoteapi/CredentialsProvider.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ public interface CredentialsProvider
2929
void configureRequest(URI baseURI, HttpUriRequest request, HttpClientContext httpClientContext) throws AuthenticationException;
3030

3131
/**
32-
* Initialize the connection before its first request. If connection-based, authenticate the user. In all cases,
33-
* retrieve the CSRF token and session ID to use with subsequent requests on this connection.
32+
* Initialize the connection before its first request. For example, make a request to retrieve CSRF token &amp; session
33+
* ID and/or ensure the user is logged in.
3434
*/
3535
void initializeConnection(Connection connection) throws IOException, CommandException;
36+
37+
/**
38+
* Should the Command attempt to retry the request?
39+
*/
40+
default boolean shouldRetryRequest(CommandException exception, HttpUriRequest request)
41+
{
42+
return false;
43+
}
3644
}

0 commit comments

Comments
 (0)