Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,12 +454,36 @@ Feign.builder()

## HMAC (Hash-based Message Authentication Code) Example

#### WIP
HMAC authentication is based on a shared secret key between the client and the server. The client computes a hash (signature) of the request data (such as HTTP method, URL, headers, or payload) using the secret key and sends it along with the request, typically in a custom header (e.g., `X-HMAC-SIGNATURE`). The server recomputes the hash to verify the integrity and authenticity of the request.

### Feign Client Configuration (HMAC)

To implement HMAC authentication with Feign, you can use a `RequestInterceptor` that generates the HMAC signature and adds it to the request headers.

Example configuration:

```java
public class HmacClientConfig {

@Value("${spring.application.rest.client.hmac.secret}")
private String secret;

@Bean
public RequestInterceptor hmacRequestInterceptor() {
return requestTemplate -> {
String dataToSign = requestTemplate.method() + requestTemplate.url();
String signature = HmacUtils.hmacSha256Hex(secret, dataToSign);
requestTemplate.header("X-HMAC-SIGNATURE", signature);
};
}

@Bean
public Logger.Level hmacLoggerLevel() {return Logger.Level.FULL;}
}
```

In the test, the Feign client sends a request with the `X-HMAC-SIGNATURE` header, and the server verifies the signature using the shared secret.

## SAML (Security Assertion Markup Language) Example

#### WIP


Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package raff.stein.feignclient.client.hmac;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import raff.stein.feignclient.client.hmac.config.HashBasedMessageAuthClientConfig;

@FeignClient(
name = "hashBasedMessageAuthClient",
url = "${spring.application.rest.client.hmac.host}",
configuration = HashBasedMessageAuthClientConfig.class)
public interface HashBasedMessageAuthClient {

@GetMapping("/get-data")
String getData();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package raff.stein.feignclient.client.hmac.config;

import feign.Logger;
import feign.RequestInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class HashBasedMessageAuthClientConfig {

@Value("${spring.application.rest.client.hmac.secret}")
private String secret;

@Bean
public RequestInterceptor hmacRequestInterceptor() {
return requestTemplate -> {
try {
String data = requestTemplate.method() + requestTemplate.url();
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String signature = Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes()));
requestTemplate.header("X-HMAC-SIGNATURE", signature);
} catch (Exception e) {
throw new RuntimeException("Error during HMAC sign generation", e);
}
};
}

@Bean
public Logger.Level hashBasedMessageAuthLoggerLevel() {return Logger.Level.FULL;}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@ public ResponseEntity<String> getDataWithMutualTls() {
return ResponseEntity.ok(responseString);
}

@GetMapping("/hmac")
public ResponseEntity<String> getDataWithHmac() {
final String responseString = feignClientSimpleService.simpleHashBasedMessageAuthClientCall();
return ResponseEntity.ok(responseString);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import raff.stein.feignclient.client.apikey.ApiKeyClient;
import raff.stein.feignclient.client.basicauth.BasicAuthClient;
import raff.stein.feignclient.client.digest.DigestApacheClient;
import raff.stein.feignclient.client.hmac.HashBasedMessageAuthClient;
import raff.stein.feignclient.client.jwt.JwtClient;
import raff.stein.feignclient.client.mutualtls.MutualTlsClient;
import raff.stein.feignclient.client.ntlm.NTLMClient;
Expand All @@ -23,6 +24,7 @@ public class FeignClientSimpleService {
private final JwtClient jwtClient;
private final DigestApacheClient digestApacheClient;
private final MutualTlsClient mutualTlsClient;
private final HashBasedMessageAuthClient hashBasedMessageAuthClient;

public String simpleBasicAuthClientCall() {
return basicAuthClient.getData();
Expand Down Expand Up @@ -54,5 +56,9 @@ public String simpleMutualTlsClientCall() {
return mutualTlsClient.getData();
}

public String simpleHashBasedMessageAuthClientCall() {
return hashBasedMessageAuthClient.getData();
}


}
5 changes: 4 additions & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ spring:
password: keystorePassword
truststore:
path: truststorePath
password: truststorePassword
password: truststorePassword
hmac:
host: http://localhost:8090
secret: hmacKey
59 changes: 59 additions & 0 deletions src/test/java/raff/stein/feignclient/FeignClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class FeignClientTest {
// MUTUAL TLS AUTH
static WireMockServer mutualTlsMockServer;

// HMAC AUTH
static WireMockServer hmacMockServer;


private static final String BASIC_AUTH_200_RESPONSE_STRING = "Basic auth response content";
private static final String OAUTH_200_RESPONSE_STRING = "OAuth2 response content";
Expand All @@ -62,6 +65,7 @@ class FeignClientTest {
private static final String DIGEST_200_FIRST_RESPONSE_STRING = "Digest first response content";
private static final String DIGEST_200_SECOND_RESPONSE_STRING = "Digest second response content";
private static final String MUTUAL_TLS_200_SECOND_RESPONSE_STRING = "Mutual TLS response content";
private static final String HMAC_200_RESPONSE_STRING = "HMAC response content";



Expand All @@ -75,6 +79,7 @@ static void beforeAll() {
setupJWTServer();
setupDigestServer();
setupMutualTlsServer();
setupHmacServer();
}


Expand Down Expand Up @@ -267,6 +272,28 @@ private static void setupMutualTlsServer() {
.withBody(MUTUAL_TLS_200_SECOND_RESPONSE_STRING)));
}

private static void setupHmacServer() {
hmacMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().port(8090));
hmacMockServer.start();
hmacMockServer.stubFor(
WireMock.get(WireMock.urlEqualTo("/get-data"))
.withHeader("X-HMAC-SIGNATURE", WireMock.matching(".+"))
.willReturn(
WireMock.aResponse()
.withStatus(200)
.withBody(HMAC_200_RESPONSE_STRING)
));
// Stub for missing HMAC signature
hmacMockServer.stubFor(
WireMock.get(WireMock.urlEqualTo("/get-data"))
.atPriority(1)
.withHeader("X-HMAC-SIGNATURE", WireMock.absent())
.willReturn(WireMock.aResponse()
.withStatus(401)
.withBody("Missing HMAC signature"))
);
}



@AfterAll
Expand All @@ -291,6 +318,8 @@ private static void stopWireMockServers() {
digestMockServer.stop();
if(mutualTlsMockServer.isRunning())
mutualTlsMockServer.stop();
if(hmacMockServer.isRunning())
hmacMockServer.stop();
}


Expand Down Expand Up @@ -532,4 +561,34 @@ void testMutualTlsAuthClient() throws Exception {
});
}

@Test
void testHmacAuthClient() throws Exception {
mockMvc.perform(MockMvcRequestBuilders
.get("/hmac"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
.andExpect(MockMvcResultMatchers.content().string(HMAC_200_RESPONSE_STRING));

Awaitility.await()
.atMost(Duration.ofSeconds(30))
.pollInterval(Duration.ofSeconds(2))
.untilAsserted(() -> {
List<ServeEvent> events = hmacMockServer.getAllServeEvents();
int numberOfRequestReceived = events.size();
Assertions.assertEquals(1, numberOfRequestReceived);

boolean requestReceived = events
.stream()
.anyMatch(event -> event.getRequest().getUrl().equals("/get-data") &&
event.getRequest().getAbsoluteUrl().contains(":" + hmacMockServer.getOptions().portNumber()));

Assertions.assertTrue(requestReceived, "No request found on hmacMockServer");

// check HMAC header presence and value
String hmacHeader = events.getFirst().getRequest().getHeader("X-HMAC-SIGNATURE");
Assertions.assertNotNull(hmacHeader, "X-HMAC-SIGNATURE header is missing");
Assertions.assertFalse(hmacHeader.isBlank(), "X-HMAC-SIGNATURE header is blank");
});
}

}
3 changes: 3 additions & 0 deletions src/test/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ spring:
truststore:
path: src/test/resources/mutualtls/server-truststore.jks
password: changeit
hmac:
host: http://localhost:8090
secret: hmacKey
logging:
level:
raff:
Expand Down