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 resent 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("user@example.com", "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.
1 Answers
Answers 1
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("user1@example.com", "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(); } } }
0 comments:
Post a Comment