How to Configure Proxy Authentication for Java HTTP Client

In our previous tutorial, we implemented a thin REST API client application to consume secured CRUD REST APIs. However, the client does not support proxy authentication. Therefore, we are going to configure the client with proxy authentication.

Introduction

Often, corporate networks provide internet access via proxy servers, and at times they require authentication as well. Hence, applications must do proxy authentication to connect to the internet.

What You’ll Build

A REST client that supports proxy authentication to consume REST APIs with JWT authentication.

What You’ll Need

  • Spring Tool Suite or any IDE of your choice
  • JDK 11
  • MySQL Server
  • Apache Maven

Configuring HTTP Client with Proxy Authentication

Let’s see how we can create an HTTP client v2 with proxy authentication in step-by-step.

  1. Firstly, create an HTTP Client v2 builder with 10 seconds of connection timeout.
  2. Secondly, If the proxy host and port are provided, then
    1. Create a ProxySelector using ProxySelector::of method. It provides a ProxySelector which uses a single proxy for all requests. The system-wide proxy selector can be retrieved by ProxySelector.getDefault().
    2. Invoke the builder.proxy() method to use this ProxySelector. Note: If this method is not invoked prior to building, then newly built clients will use the default proxy selector, which is usually adequate for client applications. The default proxy selector supports a set of system properties related to proxy settings. This default behavior can be disabled by supplying an explicit proxy selector, such as NO_PROXY, or one returned by ProxySelector::of, before building.
    3. Get the default Authenticator which will be used for connection without authentication. If the proxy username and password are provided, then it means the proxy server requires authentication. Hence, In this case, we have to create a new Authenticator and override the getPasswordAuthentication() method to create a new PasswordAuthentication object using these credentials.
    4. Invoke the builder.authenticator() method to use this authenticator.
  3. Finally, invoke the builder.build() method to create the instance of HTTP Client.
public ProductManagerImpl(ProductManagerConfig config) {
        this.productManagerConfig = config;
        HttpClient.Builder builder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
                .connectTimeout(Duration.ofSeconds(10));

        if (config.proxyHost() != null && config.proxyPort() != null) {
            builder.proxy(ProxySelector.of(new InetSocketAddress(config.proxyHost(), config.proxyPort())));
            Authenticator authenticator = Authenticator.getDefault();
            if (config.proxyUser() != null && config.proxyPass() != null) {
                authenticator = new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication(config.proxyUser(), config.proxyPass().toCharArray());
                    }
                };
            }
            builder.authenticator(authenticator);
        }
        this.requestHelper = new HttpRequestHelper(builder.build());
    }

Modifying REST Client API

Now, let’s see the complete changes required to support proxy authentication in our REST client that we developed earlier.

ProductManagerConfig.java

ProductManagerConfig class holds the REST API base URL and credentials required for authentication.

package com.javachinna.rest.client.config;

public record ProductManagerConfig(String baseUrl, String username, String password, String proxyHost,
                                   Integer proxyPort, String proxyUser, String proxyPass) {
}

Note: We have changed this class to a record class since it is intended to serve as a simple “data carrier”. A record class declares a sequence of fields, and then the appropriate accessors, constructors, equalshashCode, and toString methods are created automatically. All the fields are final.

ProductManagerImpl.java

We have modified this implementation to use the new HttpRequestHelper class with the dynamically created HTTP Client instance.

package com.javachinna.rest.client;

import com.javachinna.rest.client.config.ProductManagerConfig;
import com.javachinna.rest.client.helper.HttpRequestHelper;
import com.javachinna.rest.client.model.*;
import lombok.RequiredArgsConstructor;

import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RequiredArgsConstructor
public class ProductManagerImpl implements ProductManager {
    private static final String AUTH_API = "auth/signin";
    private static final String PRODUCT_API = "products";

    private final ProductManagerConfig productManagerConfig;
    private final HttpRequestHelper requestHelper;

    public ProductManagerImpl(ProductManagerConfig config) {
        this.productManagerConfig = config;
        HttpClient.Builder builder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
                .connectTimeout(Duration.ofSeconds(10));

        if (config.proxyHost() != null && config.proxyPort() != null) {
            builder.proxy(ProxySelector.of(new InetSocketAddress(config.proxyHost(), config.proxyPort())));
            Authenticator authenticator = Authenticator.getDefault();
            if (config.proxyUser() != null && config.proxyPass() != null) {
                authenticator = new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication(config.proxyUser(), config.proxyPass().toCharArray());
                    }
                };
            }
            builder.authenticator(authenticator);
        }
        this.requestHelper = new HttpRequestHelper(builder.build());
    }

    public Product getProduct(Integer productId) throws Exception {
        String url = productManagerConfig.baseUrl() + PRODUCT_API + "/" + productId;
        return requestHelper.get(url, getAccessToken(), Product.class);
    }

    @SuppressWarnings("unchecked")
    public List<HashMap<String, Object>> getAllProducts() throws Exception {
        String url = productManagerConfig.baseUrl() + PRODUCT_API;
        Map<String, Object> response = requestHelper.get(url, getAccessToken(), Map.class);
        return (List<HashMap<String, Object>>) response.get("content");
    }

    public ApiResponse createProduct(ProductRequest request) throws Exception {
        return requestHelper.post(productManagerConfig.baseUrl() + PRODUCT_API, request, ApiResponse.class,
                getAccessToken());
    }

    public ApiResponse updateProduct(Integer productId, ProductRequest request) throws Exception {
        return requestHelper.put(productManagerConfig.baseUrl() + PRODUCT_API + "/" + productId, request,
                ApiResponse.class, getAccessToken());
    }

    public ApiResponse deleteProduct(Integer productId) throws Exception {
        return requestHelper.delete(productManagerConfig.baseUrl() + PRODUCT_API + "/" + productId,
                ApiResponse.class, getAccessToken());
    }

    public String getAccessToken() throws Exception {
        LoginRequest loginRequest = new LoginRequest(productManagerConfig.username(),
                productManagerConfig.password());
        JwtAuthenticationResponse jwtResponse = requestHelper.post(productManagerConfig.baseUrl() + AUTH_API,
                loginRequest, JwtAuthenticationResponse.class);
        return jwtResponse.getAccessToken();
    }
}

HttpRequestHelper.java

HttpRequestHelper contains generic methods used to make HTTP GET, POST, PUT, and DELETE requests.

package com.javachinna.rest.client.helper;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.Builder;
import java.net.http.HttpResponse;
 
/**
 * 
 * @author Chinna [javachinna.com]
 *
 */
public class HttpRequestHelper {
 
    private final String APPLICATION_JSON = "application/json";
	private final String CONTENT_TYPE = "Content-Type";
    private final String AUTHORIZATION = "Authorization";
    private final String BEARER = "Bearer ";
    private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
    private final HttpClient httpClient;
 
    public HttpRequestHelper(final HttpClient httpClient) {
        this.httpClient = httpClient;
    }
 
    public <T> T get(String url, String token, Class<T> valueType)
            throws IOException, InterruptedException {
    	Builder builder = HttpRequest.newBuilder().GET().uri(URI.create(url));
        return send(valueType, token, builder);
    }
 
    public <T> T post(String uri, Object request, Class<T> valueType)
            throws IOException, InterruptedException {
        return post(uri, request, valueType, null);
    }
 
    public <T> T post(String uri, Object request, Class<T> valueType, String token)
            throws IOException, InterruptedException {
        Builder builder = HttpRequest.newBuilder().uri(URI.create(uri)).POST(getBodyPublisher(request)).header(CONTENT_TYPE, APPLICATION_JSON);
        return send(valueType, token, builder);
    }
 
    public <T> T put(String uri, Object request, Class<T> valueType, String token)
            throws IOException, InterruptedException {
        Builder builder = HttpRequest.newBuilder().uri(URI.create(uri)).PUT(getBodyPublisher(request)).header(CONTENT_TYPE, APPLICATION_JSON);
        return send(valueType, token, builder);
    }
    
    public <T> T patch(String uri, Class<T> valueType, String token)
    		throws IOException, InterruptedException {
    	Builder builder = HttpRequest.newBuilder().uri(URI.create(uri)).method("PATCH", HttpRequest.BodyPublishers.noBody()).header(CONTENT_TYPE, APPLICATION_JSON);
    	return send(valueType, token, builder);
    }
     
    public <T> T delete(String uri, Class<T> valueType, String token)
            throws IOException, InterruptedException {
        Builder builder = HttpRequest.newBuilder().uri(URI.create(uri)).DELETE();
        return send(valueType, token, builder);
    }
 
    private BodyPublisher getBodyPublisher(Object request) throws JsonProcessingException {
        return HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(request));
    }
 
    private <T> T send(Class<T> valueType, String token, Builder builder)
            throws IOException, InterruptedException {
        if (token != null) {
            builder.header(AUTHORIZATION, BEARER + token);
        }
        HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new RuntimeException(response.body());
        }
        return objectMapper.readValue(response.body(), valueType);
    }
}

Note: We have removed the HttpRequestUtils static class and created this helper class instead to create the HttpClient dynamically based on the configured proxy properties.

Source Code

https://github.com/JavaChinna/java-rest-api-client

Conclusion

That’s all folks. In this article, we have added support for proxy authentication to our lightweight Java HttpClient.

Thank you for reading.

Leave a Reply