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();
}
}
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.
This works as expected for me with Apache HttpClient 4.2
NOTE. Though it is compiled and executed with httpclient 4.5, its execution falls into forever loop.
MyAuthScheme.java
public class MyAuthScheme implements ContextAwareAuthScheme {
public static final String NAME = "myscheme";
private static final String REQUEST_BODY = "{\"login\":\"%s\",\"password\":\"%s\"}";
private final URI loginUri;
public MyAuthScheme(URI uri) {
loginUri = uri;
}
@Override
public Header authenticate(Credentials credentials,
HttpRequest request,
HttpContext context) throws AuthenticationException {
BasicCookieStore cookieStore = (BasicCookieStore) context.getAttribute(ClientContext.COOKIE_STORE);
DefaultHttpClient client = new DefaultHttpClient();
// authentication cookie is set automatically when
// login response arrived
client.setCookieStore(cookieStore);
HttpPost loginRequest = new HttpPost(loginUri);
String requestBody = String.format(
REQUEST_BODY,
credentials.getUserPrincipal().getName(),
credentials.getPassword());
loginRequest.setHeader("Content-Type", "application/json");
try {
loginRequest.setEntity(new StringEntity(requestBody));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
try {
HttpResponse response = client.execute(loginRequest);
int code = response.getStatusLine().getStatusCode();
EntityUtils.consume(response.getEntity());
if(code != 200) {
throw new IllegalStateException("Authentication problem");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
loginRequest.reset();
}
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 false;
}
@Override
public Header authenticate(Credentials credentials,
HttpRequest request) throws AuthenticationException {
// not implemented
return null;
}
}
MyAuthSchemeFactory.java
public class MyAuthSchemeFactory implements AuthSchemeFactory {
private final URI loginUri;
public MyAuthSchemeFactory(URI uri) {
this.loginUri = uri;
}
@Override
public AuthScheme newInstance(HttpParams params) {
return new MyAuthScheme(loginUri);
}
}
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("myscheme", new BasicHeader("WWW-Authenticate", "myscheme"));
return challenges;
}
@Override
public Queue<AuthOption> select(Map<String, Header> challenges,
HttpHost authhost,
HttpResponse response,
HttpContext context) throws MalformedChallengeException {
AuthSchemeRegistry registry = (AuthSchemeRegistry) context.getAttribute(ClientContext.AUTHSCHEME_REGISTRY);
AuthScheme authScheme = registry.getAuthScheme(MyAuthScheme.NAME, new BasicHttpParams());
CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER);
Credentials credentials = credsProvider.getCredentials(new AuthScope(authhost));
Queue<AuthOption> options = new LinkedList<>();
options.add(new AuthOption(authScheme, credentials));
return options;
}
@Override
public void authSucceeded(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}
@Override
public void authFailed(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}
}
App.java
public class App {
public static void main(String[] args) throws IOException, URISyntaxException {
URI loginUri = new URI("https://example.com/api/v3/users/login");
AuthSchemeRegistry schemeRegistry = new AuthSchemeRegistry();
schemeRegistry.register(MyAuthScheme.NAME, new MyAuthSchemeFactory(loginUri));
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
new AuthScope("example.com", 8065),
new UsernamePasswordCredentials("[email protected]", "secret"));
DefaultHttpClient client = new DefaultHttpClient();
client.setCredentialsProvider(credentialsProvider);
client.setTargetAuthenticationStrategy(new MyAuthStrategy());
client.setAuthSchemes(schemeRegistry);
client.setCookieStore(new BasicCookieStore());
String getResourcesUrl = "https://example.com:8065/api/v3/myresources/";
HttpGet getResourcesRequest = new HttpGet(getResourcesUrl);
getResourcesRequest.setHeader("x-requested-with", "XMLHttpRequest");
try {
HttpResponse response = client.execute(getResourcesRequest);
// consume response
} finally {
getResourcesRequest.reset();
}
// further requests won't call MyAuthScheme.authenticate()
HttpGet getResourcesRequest2 = new HttpGet(getResourcesUrl);
getResourcesRequest2.setHeader("x-requested-with", "XMLHttpRequest");
try {
HttpResponse response2 = client.execute(getResourcesRequest);
// consume response
} finally {
getResourcesRequest2.reset();
}
HttpGet getResourcesRequest3 = new HttpGet(getResourcesUrl);
getResourcesRequest3.setHeader("x-requested-with", "XMLHttpRequest");
try {
HttpResponse response3 = client.execute(getResourcesRequest);
// consume response
} finally {
getResourcesRequest3.reset();
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With