Saturday, June 17, 2017

Automatic custom authentication

Leave a Comment

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();         }         }  } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment