DEV Community

Cover image for Mastering API Integrations: A Step-by-Step Guide to Secure API Authentication in Java
Joao Marques
Joao Marques

Posted on

Mastering API Integrations: A Step-by-Step Guide to Secure API Authentication in Java

Introduction 🛤️

Integrating with external APIs is a common practice in software development, allowing your applications to consume third-party services and expand their functionalities. Among these APIs, some stand out as robust solutions for managing subscriptions, products, and other variety of operations.

In this article, I will explain how to configure a service in Java to authenticate and interact with a REST API, specifically focusing on the authentication process. The goal is to provide a detailed guide that facilitates the creation of a secure and efficient integration with the external API, ensuring that your applications can fully leverage the resources offered by the platform without making unnecessary refresh token requests.

About ♟️

An external API allows you to manage the lifecycle of items related to the data that you need, and it will charge you for the amount of requests you make. To interact with the API, you need to authenticate your requests using a token-based authentication method. This method ensures that only authorized users can access the API resources.

This is the structure that we will talk about in this article:

The auth service structure in the project

Environment 🧩

To test and develop integrations with an external API, you can use the development environment provided by the service, accessible at a specific URL. This environment allows you to perform test operations without affecting production data, providing a safe space for developing and validating your integrations.

Creating a Sandbox Account: First, you need to create a sandbox account with the external service. This account allows you to access the development environment and test all API functionalities without risks.

Obtaining API Credentials: After creating your sandbox account, obtain your API credentials (client ID and client secret). These credentials will be used to authenticate your requests.

API Endpoints: Use the development environment's endpoints to make your requests. For example, the endpoint for authentication might be something like https://rest.test.external-service.com/oauth/token

Note that it is possible to define requests that require the presence of the authentication token and those that do not.

The basis 🥾

First, I'll create the base of the HTTP service, where the base HTTP methods will be present.

public class BaseHttpService {

    private static final String DEFAULT_CHARSET = "UTF-8";

    protected String post(String url, String body, Map<String, String> headers) throws IOException {

        HttpPost httpPost = new HttpPost(url);

        addHeadersToRequest(httpPost, headers);

        StringEntity requestBody = new StringEntity(body, DEFAULT_CHARSET);
        httpPost.setEntity(requestBody);

        return executeRequest(httpPost);
    }

    protected String get(String url, Map<String, String> headers) throws IOException {
        HttpGet httpGet = new HttpGet(url);
        addHeadersToRequest(httpGet, headers);
        return executeRequest(httpGet);
    }

    private String executeRequest(HttpUriRequest request) throws IOException {

        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpResponse response = httpClient.execute(request);
        HttpEntity entity = response.getEntity();

        if (entity == null) {
            return null;
        }
        String jsonResponse = EntityUtils.toString(entity);
        httpClient.close();
        return jsonResponse;
    }

    protected void addHeadersToRequest(HttpRequestBase httpRequest, Map<String, String> headers) {

        if (headers == null) {
            return;
        }

        for (var header : headers.entrySet()) {
            httpRequest.addHeader(header.getKey(), header.getValue());
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

These methods will be used to authenticate with the external API and also to complement the methods that require authentication. Therefore, they form the base of the service.

The Auth class 🛂

The next class we'll discuss is ExternalApiAuthenticatedRequestService. Since it is quite large, I'll divide it into smaller parts to explain each one individually.

The first part, and the most important, is to know which properties we will use in this class to maintain maximum encapsulation. These properties should be related to authentication, such as:

SecretValues: An object created to store the client ID and client secret, depending on the environment your application is running in.

baseUrl: The URL of your external service environment.
EXTERNAL_API_AUTH_PATH: The path to obtain the authentication token.

isAuthenticated: A boolean used to facilitate authentication control.

bearerToken: The authentication token, defined as private to ensure complete control and encapsulation over this property, being acesses only through this class.

externalApiTokenExpirationTimeInMillis: The expiration time of the token in milliseconds. We check this value when calling the getBearerToken method. If it has expired, we need to authenticate again.

private final SecretValues secretValues;
protected final String baseUrl;
private static final String EXTERNAL_API_AUTH_PATH = "/oauth/token";
private boolean isAuthenticated;
private String bearerToken;
private long externalApiTokenExpirationTimeInMillis;
Enter fullscreen mode Exit fullscreen mode

Constructor Method 🏗️

The constructor method should set the values for secretValues and baseUrl. This depends on each application.

public ExternalApiAuthenticatedRequestService(SecretValues secretValues, String baseUrl) {
    this.secretValues = secretValues;
    this.baseUrl = baseUrl;

    // We authenticate with external Api on start, so it's faster when the first request comes
    authenticateWithExternalApi();
    logger.info("Started ExternalApiAuthenticatedRequestService");
}
Enter fullscreen mode Exit fullscreen mode

The Authentication Method 🔑

Here is the most crucial method, which we use to authenticate with the external service by calling the post method defined above.

private void authenticateWithExternalApi() {
    logger.info("Authenticating to {}", getAuthUrl());

    String authRequestBody = buildAuthRequestBody(secretValues);

    Map<String, String> authHeaders = new HashMap<>();
    authHeaders.put("Content-Type", "application/x-www-form-urlencoded");
    try {
        String response = super.post(getAuthUrl(), authRequestBody, authHeaders);
        ExternalApiAuthenticationResponseDTO externalApiTokenExpirationTimeInMillis =
                Utility.convertStringToObject(response, ExternalApiAuthenticationResponseDTO.class);
        externalApiTokenExpirationTimeInMillis =
                Long.parseLong(externalApiAuthenticationResponseDTO.getExpiresIn()) * 1000 + System.currentTimeMillis();
        bearerToken = externalApiAuthenticationResponseDTO.getAccessToken();
        isAuthenticated = true;
        logger.info("Auth token retrieved from {}", getAuthUrl());
    } catch (IOException e) {
        isAuthenticated = false;
        logger.error("Could not authenticate with external Api, error: {}", e.getMessage());
    }
}
Enter fullscreen mode Exit fullscreen mode

Important Points about this Method

  1. It sends a POST request using the post method from the base class.
  2. It obtains the token from the external API and stores it in the bearerToken variable.
  3. It updates the token expiration time. We will use this value later.
  4. Calls buildAuthRequestBody and getAuthUrl

The methods that hold hands 🧑‍🤝‍🧑

private String buildAuthRequestBody(SecretValues secretValues) {
    return "grant_type=client_credentials&client_id=" + secretValues.getClientId()
            + "&client_secret="
            + secretValues.getClientSecret();
}
Enter fullscreen mode Exit fullscreen mode

This is to good way to not leave too much responsibility to authenticateWithExternalApi method. Its easy to create tests with this separation of concerns.

private String getAuthUrl() {
    return baseUrl + EXTERNAL_API_AUTH_PATH;
}
Enter fullscreen mode Exit fullscreen mode

Get the precious bearer token 💍

private String getBearerToken() throws ExternalApiAuthenticationException {
    // Validate token existence
    if (!isAuthenticated) {
        logger.warn("External Api is not authenticated, authenticating");
        reAuthenticateWithExternalApi();
    }
    // Validate token expiration
    if (externalApiTokenExpirationTimeInMillis < System.currentTimeMillis()) {
        logger.info("External Api token expired, authenticating again");
        reAuthenticateWithExternalApi();
    }
    return bearerToken;
}
Enter fullscreen mode Exit fullscreen mode

Notice that here we cannot just call authenticateWithExternalApi, because in case it fails we wanna do something about it.
That's why I added this method reAuthenticateWithExternalApi, in some application you might wanna try authenting again one or two times before throwing your exception.
You might want to define a variable called numberOfTrialsForAuthentication in your application-{environment}.xml to call reAuthenticateWithExternalApi recursively.

private void reAuthenticateWithExternalApi() throws ExternalApiAuthenticationException {
    authenticateWithExternalApi();
    if (!isAuthenticated) {
        throw new ExternalApiAuthenticationException("Could not authenticate with external Api");
    }
}
Enter fullscreen mode Exit fullscreen mode

The magic methods 🖌️

And finally your service can make authenticated get and post requests:

@Override
protected String post(String url, String body, Map<String, String> headers) throws IOException {
    if (headers == null) {
        headers = new HashMap<>();
    }
    headers.put("Authorization", "bearer " + getBearerToken());
    return super.post(url, body, headers);
}

@Override
protected String get(String url, Map<String, String> headers) throws IOException {
    if (headers == null) {
        headers = new HashMap<>();
    }
    headers.put("Authorization", "bearer " + getBearerToken());
    return super.get(url, headers);
}
Enter fullscreen mode Exit fullscreen mode

Note that we re-use the get and post methods from the base http service.

Please, leave in the comments if you could understand the thought process.

The action 👨‍💻

public class ExternalApiHttpService extends ExternalApiAuthenticatedRequestService{

    private static final Logger logger = LoggerFactory.getLogger(ExternalApiHttpService.class);

    private static final String API_VERSION = "/v1";
    private static final String GET_PRODUCTS = "/products/accounts/%s";

    public ExternalApiHttpService(SecretValues secretValues, String baseUrl) {
        super(secretValues, baseUrl);
    }

    public ProductsResponseDTO retrieveProductsFromAccount(String accountId)
            throws IOException, ExternalApiAuthenticationException {
        logger.info("Retrieving products from account: {}", accountId);
        String requestPath = buildBaseUrl() + String.format(GET_PRODUCTS, accountId);
        String jsonResponse = get(requestPath, new HashMap<>());
        return Utility.convertStringToObject(jsonResponse, ProductsResponseDTO.class);
    }

    private String buildBaseUrl(){
        return baseUrl + API_VERSION;
    }
}
Enter fullscreen mode Exit fullscreen mode

And finally, you can add some http api requests in this class.

notice that I only added /products/accounts/%s

but you can find a good way to organize your project using this structure.

If you feel that this class will grow too much, maybe would be a good idea to create a separated auth service, and call this auth service with your requests.
In my case I added it as a parent class because at the moment only few requests are necessary. So I don't need to hold the authentication service reference and it saves me some minutes of coding 😌.

Please share with me your thoughts, there is no right and wrong in this dev community, there are alternatives and developers should identify which one fits better for their outcome.

Top comments (0)