Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatic custom authentication

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.

like image 844
humkins Avatar asked May 17 '17 20:05

humkins


1 Answers

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();
        }   

    }

}
like image 74
humkins Avatar answered Nov 13 '22 09:11

humkins