I want to use Apache HttpClient 4+ to send authenticated requests to an HTTP server (actually, I need this for different server implementations) AND to authenticate (or re-authenticate) automatically ONLY when it is required, when auth token is not present or it is dead.
In order to authenticate I need to send a POST request with JSON containing user credentials.
In case authentication token is not provided in the cookie, one server returns status code 401, another one 500 with AUTH_REQUIRED text in the response body.
I played a lot with different HttpClient versions by setting CredentialsProvider with proper Credentials, trying to implement own AuthScheme and registering it and unregistering the rest of standard ones.
I also tried to set own AuthenticationHandler. When isAuthenticationRequested is called I'm analyzing HttpResponse which is passed as the method argument and decided what to return by analyzing status code and response body. I expected that this (isAuthenticationRequested() == true) is what force the client to authenticate by calling AuthScheme.authenticate (my AuthScheme implementation which is returned by AuthenticationHandler.selectScheme), but instead of AuthScheme.authenticate invocation I can see AuthenticationHandler.getChallenges. I really don't know what I should return by this method, thus I'm just returning new HashMap<>().
Here is debug output I have in result
DEBUG org.apache.http.impl.client.DefaultHttpClient - Authentication required
DEBUG org.apache.http.impl.client.DefaultHttpClient - example.com requested authentication
DEBUG com.test.httpclient.MyAuthenticationHandler - MyAuthenticationHandler.getChallenges()
DEBUG org.apache.http.impl.client.DefaultHttpClient - Response contains no authentication challenges
What should I do next? Am I moving in the right direction?
UPDATE
I've almost achieved what I needed. Unfortunately, I can't provide fully working project sources, because I can't provide public access to my server. Here is my simplified code example:
MyAuthScheme.java
public class MyAuthScheme implements ContextAwareAuthScheme {
public static final String NAME = "myscheme";
@Override
public Header authenticate(Credentials credentials,
HttpRequest request,
HttpContext context) throws AuthenticationException {
HttpClientContext clientContext = ((HttpClientContext) context);
String name = clientContext.getTargetAuthState().getState().name();
// Hack #1:
// I've come to this check. I don't like it, but it allows to authenticate
// first request and don't repeat authentication procedure for further
// requests
if(name.equals("CHALLENGED") && clientContext.getResponse() == null) {
//
// auth procedure must be here but is omitted in current example
//
// Hack #2: first request won't be present with auth token cookie set via cookie store
request.setHeader(new BasicHeader("Cookie", "MYAUTHTOKEN=bru99rshi7r5ucstkj1wei4fshsd"));
// this works for second and subsequent requests
BasicClientCookie authTokenCookie = new BasicClientCookie("MYAUTHTOKEN", "bru99rshi7r5ucstkj1wei4fshsd");
authTokenCookie.setDomain("example.com");
authTokenCookie.setPath("/");
BasicCookieStore cookieStore = (BasicCookieStore) clientContext.getCookieStore();
cookieStore.addCookie(authTokenCookie);
}
// I can't return cookie header here, otherwise it will clear
// other cookies, right?
return null;
}
@Override
public void processChallenge(Header header) throws MalformedChallengeException {
}
@Override
public String getSchemeName() {
return NAME;
}
@Override
public String getParameter(String name) {
return null;
}
@Override
public String getRealm() {
return null;
}
@Override
public boolean isConnectionBased() {
return false;
}
@Override
public boolean isComplete() {
return true;
}
@Override
public Header authenticate(Credentials credentials,
HttpRequest request) throws AuthenticationException {
return null;
}
}
MyAuthStrategy.java
public class MyAuthStrategy implements AuthenticationStrategy {
@Override
public boolean isAuthenticationRequested(HttpHost authhost,
HttpResponse response,
HttpContext context) {
return response.getStatusLine().getStatusCode() == 401;
}
@Override
public Map<String, Header> getChallenges(HttpHost authhost,
HttpResponse response,
HttpContext context) throws MalformedChallengeException {
Map<String, Header> challenges = new HashMap<>();
challenges.put(MyAuthScheme.NAME, new BasicHeader(
"WWW-Authentication",
"Myscheme realm=\"My SOAP authentication\""));
return challenges;
}
@Override
public Queue<AuthOption> select(Map<String, Header> challenges,
HttpHost authhost,
HttpResponse response,
HttpContext context) throws MalformedChallengeException {
Credentials credentials = ((HttpClientContext) context)
.getCredentialsProvider()
.getCredentials(new AuthScope(authhost));
Queue<AuthOption> authOptions = new LinkedList<>();
authOptions.add(new AuthOption(new MyAuthScheme(), credentials));
return authOptions;
}
@Override
public void authSucceeded(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}
@Override
public void authFailed(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}
}
MyApp.java
public class MyApp {
public static void main(String[] args) throws IOException {
CredentialsProvider credsProvider = new BasicCredentialsProvider();
Credentials credentials = new UsernamePasswordCredentials("[email protected]", "secret");
credsProvider.setCredentials(AuthScope.ANY, credentials);
HttpClientContext context = HttpClientContext.create();
context.setCookieStore(new BasicCookieStore());
context.setCredentialsProvider(credsProvider);
CloseableHttpClient client = HttpClientBuilder.create()
// my server requires this header otherwise it returns response with code 500
.setDefaultHeaders(Collections.singleton(new BasicHeader("x-requested-with", "XMLHttpRequest")))
.setTargetAuthenticationStrategy(new MyAuthStrategy())
.build();
String url = "https://example.com/some/resource";
String url2 = "https://example.com/another/resource";
// ======= REQUEST 1 =======
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request, context);
String responseText = EntityUtils.toString(response.getEntity());
request.reset();
// ======= REQUEST 2 =======
HttpGet request2 = new HttpGet(url);
HttpResponse response2 = client.execute(request2, context);
String responseText2 = EntityUtils.toString(response2.getEntity());
request2.reset();
// ======= REQUEST 3 =======
HttpGet request3 = new HttpGet(url2);
HttpResponse response3 = client.execute(request3, context);
String responseText3 = EntityUtils.toString(response3.getEntity());
request3.reset();
client.close();
}
}
Versions
httpcore: 4.4.6
httpclient: 4.5.3
Probably this is not the best code but at least it works.
Please look at my comments in MyAuthScheme.authenticate() method.
500to request authentication??? Anyways. In case one server returns a500, you should check for that (and the response message) in yourMyAuthStrategy.isAuthenticationRequested, am I wrong?