Link Search Menu Expand Document

Setup HMAC Authentication in Java 8

We will be using Java 8 and use maven to manage dependency. We assume you are already familiar with Java and have it installed on your system. Let’s get started.

Install Dependencies

Because we’ll be using maven, make sure you have the pom.xml file in the root of your working directory. The content of the file should look like this:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>HMAC Signature</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        ...
    </dependencies>

</project>

There will be only one dependency: OkHttp to make HTTP requests to the Mekari API. There are many Http Client Library out there and you can also choose any other clients.

To install the dependency simply add it under dependencies attribute on the pom.xml. The file will look something like this.

<dependencies>
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.9.1</version>
    </dependency>
<dependencies>

Making API Request

Using OkHttp Client that we have installed earlier, we are going to setup a script that will perform API request to one of Klikpajak API endpoint which is create sales invoice (https://api.mekari.com/v2/klikpajak/v1/efaktur/out).

We create the script inside the main function:

public static void main(String[] args) {

    OkHttpClient client = new OkHttpClient();

    //      Request Base URL
    String base = "https://api.mekari.com";

    //      HMAC User client id
    String clientId = System.getenv("MEKARI_API_CLIENT_ID");

    //      HMAC User client secret
    String clientSecret = System.getenv("MEKARI_API_CLIENT_SECRET");

    //      Request method GET/POST/PUT/DELETE
    String method = "POST";

    //      Request endpoint
    String path = "/v2/klikpajak/v1/efaktur/out";

    String queryParam = "?auto_approval=false";

    //      Request Body
    String json = "requestBody";

    RequestBody body = RequestBody.create(json, JSON); 

    Request request = new Request.Builder()
        .url(base + path + queryParam)
        .post(body)
        .build();

    Call call = client.newCall(request);
    try {
        Response response = call.execute();
        System.out.println(response.body().string());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

We can try run the script now but will immediately get Unauthorized response due to no authentication attached in the request.

{"message":"Unauthorized"}

Creating HMAC Signature

The signature is one of the requirements for forming an API request with HMAC Authentication. The signature is an HMAC256 representation of the request line (a combination of the request method, the request path, the query param and HTTP/1.1) and the Date header in RFC 7231 format. The signature must then be converted into a Base64 string so that it can be attached to the Authorization header.

The full request will look like this:

// ... the rest of the code

//      Request Date
String dateFormatted = getDateTimeNowUtcString();
        
Request request = new Request.Builder()
    .url(base + path + queryParam)
    .post(body)
    .addHeader("Date", dateFormatted)
    .addHeader("Authorization",
        generateAuthSignature(clientId, clientSecret, method, path + queryParam, dateFormatted)
    )
    .build();

Call call = client.newCall(request);

To get Date following RFC 7231 format we use this function

private static String getDateTimeNowUtcString() {
    Instant instant = Instant.now();
    return DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O")
        .withZone(ZoneOffset.UTC)
        .withLocale(Locale.US)
        .format(instant);
}

To generate the Authorization Header we use these functions

import java.nio.charset.StandardCharsets;

private static String generateAuthSignature(
    String clientId, String clientSecret, String method,
    String pathWithQueryParam, String dateString
) {
    String payload = generatePayload(pathWithQueryParam, method, dateString);
    String signature = hmacSha256(clientSecret, payload);

    return "hmac username=\"" + clientId
        + "\", algorithm=\"hmac-sha256\", headers=\"date request-line\", signature=\""
        + signature + "\"";
}

private static String generatePayload(String pathWithQueryParam, String method, String dateString) {
    String requestLine = method + ' ' + pathWithQueryParam + " HTTP/1.1";
    return String.join("\n", Arrays.asList("date: " + dateString, requestLine));
}

private static String hmacSha256(String clientSecret, String payload) {
    try {
        SecretKeySpec signingKey = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);

        return Base64.getEncoder().encodeToString(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
    } catch (NoSuchAlgorithmException | UnsupportedEncodingException | InvalidKeyException exception) {
        exception.printStackTrace();
        return null;
    }
}

It is important to note that we should not include any credentials in our codebase. This means that we must save the Mekari API client id and client secret that you obtained from the Mekari Developer dashboard to an environment variable. Modern full-stack frameworks, such as Spring, usually include an application properties file to make managing environment variables easier.

MEKARI_API_CLIENT_ID=YOUR_MEKARI_API_CLIENT_ID
MEKARI_API_CLIENT_SECRET=YOUR_MEKARI_CLIENT_SECRET

Wrap It Out

It’s now time to combine everything we’ve learned.

import java.nio.charset.StandardCharsets;

public class HmacGeneratorApplication {
    public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    public static void main(String[] args) {

        OkHttpClient client = new OkHttpClient();

        //      Request Base URL
        String base = "https://api.mekari.com";

        //      HMAC User client id
        String clientId = System.getenv("MEKARI_API_CLIENT_ID");

        //      HMAC User client secret
        String clientSecret = System.getenv("MEKARI_API_CLIENT_SECRET");

        //      Request method GET/POST/PUT/DELETE
        String method = "POST";

        //      Request endpoint
        String path = "/v2/klikpajak/v1/efaktur/out";
        String queryParam = "?auto_approval=false";

        //      Request Body
        String json = "requestBody";

        RequestBody body = RequestBody.create(json, JSON);

        //      Request Date
        String dateFormatted = getDateTimeNowUtcString();

        Request request = new Request.Builder()
            .url(base + path + queryParam)
            .post(body)
            .addHeader("Date", dateFormatted)
            .addHeader("Authorization",
                generateAuthSignature(clientId, clientSecret, method, path + queryParam, dateFormatted)
            )
            .addHeader("x-idempotency-key", UUID.randomUUID().toString())
            .build();

        Call call = client.newCall(request);
        try {
            Response response = call.execute();
            System.out.println(response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String generateAuthSignature(
        String clientId, String clientSecret, String method,
        String pathWithQueryParam, String dateString
    ) {
        String payload = generatePayload(pathWithQueryParam, method, dateString);
        String signature = hmacSha256(clientSecret, payload);

        return "hmac username=\"" + clientId
            + "\", algorithm=\"hmac-sha256\", headers=\"date request-line\", signature=\""
            + signature + "\"";
    }

    private static String generatePayload(String pathWithQueryParam, String method, String dateString) {
        String requestLine = method + ' ' + pathWithQueryParam + " HTTP/1.1";
        return String.join("\n", Arrays.asList("date: " + dateString, requestLine));
    }

    private static String hmacSha256(String clientSecret, String payload) {
        try {
            SecretKeySpec signingKey = new SecretKeySpec(
                clientSecret.getBytes(StandardCharsets.UTF_8), 
                "HmacSHA256"
            );
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(signingKey);

            return Base64.getEncoder()
                .encodeToString(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException 
            | UnsupportedEncodingException 
            | InvalidKeyException exception
        ) {
            exception.printStackTrace();
            return null;
        }
    }

    private static String getDateTimeNowUtcString() {
        Instant instant = Instant.now();
        return DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O")
            .withZone(ZoneOffset.UTC)
            .withLocale(Locale.US)
            .format(instant);
    }
}

If you want to integrate this script with your existing code, you may need to modify it. Additionally, each Mekari API has its own requirements regarding request headers, query, or body, and you may need to change the script based on your needs. We hope that this guide makes integrating Mekari API into your code easier for you.

You can also look at the final code on this repository.