Skip to content

Commit b895999

Browse files
committed
Added encoder and decoder classes for the feign OpenAPI Generator library template
1 parent 5fac999 commit b895999

File tree

8 files changed

+424
-12
lines changed

8 files changed

+424
-12
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ The `com.mastercard.developer.interceptors` package will provide you with some i
297297

298298
Library options currently supported for the `java` generator:
299299
+ [okhttp-gson](#okhttp-gson)
300+
+ [feign](#feign)
300301
+ [retrofit](#retrofit)
301302
+ [retrofit2](#retrofit2)
302303
+ [google-api-client](#google-api-client)
@@ -328,6 +329,32 @@ ServiceApi serviceApi = new ServiceApi(client);
328329
// ...
329330
```
330331

332+
#### feign <a name="feign"></a>
333+
##### OpenAPI Generator Plugin Configuration
334+
```xml
335+
<configuration>
336+
<inputSpec>${project.basedir}/src/main/resources/openapi-spec.yaml</inputSpec>
337+
<generatorName>java</generatorName>
338+
<library>feign</library>
339+
<!-- ... -->
340+
</configuration>
341+
```
342+
343+
##### Usage of `FeignFieldLevelEncryptionEncoder` and `FeignFieldLevelEncryptionDecoder`
344+
```java
345+
ApiClient client = new ApiClient();
346+
ObjectMapper objectMapper = client.getObjectMapper();
347+
client.setBasePath("https://sandbox.api.mastercard.com");
348+
Feign.Builder feignBuilder = client.getFeignBuilder();
349+
ArrayList<RequestInterceptor> interceptors = new ArrayList<>();
350+
interceptors.add(new OpenFeignOAuth1Interceptor(consumerKey, signingKey, client.getBasePath()));
351+
feignBuilder.requestInterceptors(interceptors);
352+
feignBuilder.encoder(new FeignFieldLevelEncryptionEncoder(config, new FormEncoder(new JacksonEncoder(objectMapper))));
353+
feignBuilder.decoder(new FeignFieldLevelEncryptionDecoder(config, new JacksonDecoder(objectMapper)));
354+
ServiceApi serviceApi = client.buildClient(ServiceApi.class);
355+
// ...
356+
```
357+
331358
#### retrofit <a name="retrofit"></a>
332359
##### OpenAPI Generator Plugin Configuration
333360
```xml
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.mastercard.developer.interceptors;
2+
3+
import com.mastercard.developer.encryption.EncryptionException;
4+
import com.mastercard.developer.encryption.FieldLevelEncryption;
5+
import com.mastercard.developer.encryption.FieldLevelEncryptionConfig;
6+
import feign.Response;
7+
import feign.Util;
8+
import feign.codec.DecodeException;
9+
import feign.codec.Decoder;
10+
11+
import java.io.IOException;
12+
import java.lang.reflect.Type;
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.*;
15+
16+
/**
17+
* A Feign decoder for decrypting parts of HTTP payloads.
18+
*/
19+
public class FeignFieldLevelEncryptionDecoder implements Decoder {
20+
21+
private final FieldLevelEncryptionConfig config;
22+
private final Decoder delegate;
23+
24+
public FeignFieldLevelEncryptionDecoder(FieldLevelEncryptionConfig config, Decoder delegate) {
25+
this.config = config;
26+
this.delegate = delegate;
27+
}
28+
29+
@Override
30+
public Object decode(Response response, Type type) throws IOException {
31+
try {
32+
// Check response actually has a payload
33+
Response.Body body = response.body();
34+
if (body == null || body.length() <= 0) {
35+
// Nothing to decrypt
36+
return this.delegate.decode(response, type);
37+
}
38+
39+
// Read response payload
40+
String responsePayload = Util.toString(body.asReader());
41+
42+
// Decrypt fields
43+
String decryptedPayload = FieldLevelEncryption.decryptPayload(responsePayload, config);
44+
Map<String, Collection<String>> headers = new HashMap<>(response.headers());
45+
updateContentLength(headers, String.valueOf(decryptedPayload.length()));
46+
response = response.toBuilder()
47+
.body(decryptedPayload, StandardCharsets.UTF_8)
48+
.headers(headers)
49+
.build();
50+
} catch (EncryptionException e) {
51+
throw new DecodeException("Failed to decrypt response!", e);
52+
}
53+
54+
// Call the regular decoder
55+
return this.delegate.decode(response, type);
56+
}
57+
58+
private static void updateContentLength(Map<String, Collection<String>> headers, String length) {
59+
Set<String> headerNames = new HashSet<>(headers.keySet());
60+
for (String headerName : headerNames) {
61+
if (headerName.equalsIgnoreCase("content-length")) {
62+
headers.remove(headerName);
63+
}
64+
}
65+
headers.put("Content-Length", Collections.singleton(length));
66+
}
67+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.mastercard.developer.interceptors;
2+
3+
import com.mastercard.developer.encryption.EncryptionException;
4+
import com.mastercard.developer.encryption.FieldLevelEncryption;
5+
import com.mastercard.developer.encryption.FieldLevelEncryptionConfig;
6+
import feign.RequestTemplate;
7+
import feign.codec.EncodeException;
8+
import feign.codec.Encoder;
9+
10+
import java.lang.reflect.Type;
11+
import java.nio.charset.StandardCharsets;
12+
13+
/**
14+
* A Feign encoder for encrypting parts of HTTP payloads.
15+
*/
16+
public class FeignFieldLevelEncryptionEncoder implements Encoder {
17+
18+
private final FieldLevelEncryptionConfig config;
19+
private final Encoder delegate;
20+
21+
public FeignFieldLevelEncryptionEncoder(FieldLevelEncryptionConfig config, Encoder delegate) {
22+
this.config = config;
23+
this.delegate = delegate;
24+
}
25+
26+
@Override
27+
public void encode(Object object, Type type, RequestTemplate requestTemplate) {
28+
// Call the regular encoder
29+
delegate.encode(object, type, requestTemplate);
30+
31+
try {
32+
// Check request actually has a payload
33+
byte[] bodyBytes = requestTemplate.body();
34+
if (null == bodyBytes || bodyBytes.length <= 0) {
35+
// Nothing to encrypt
36+
return ;
37+
}
38+
39+
// Read request payload
40+
String payload = new String(bodyBytes, StandardCharsets.UTF_8);
41+
42+
// Encrypt fields
43+
String encryptedPayload = FieldLevelEncryption.encryptPayload(payload, config);
44+
requestTemplate.body(encryptedPayload);
45+
requestTemplate.header("Content-Length", String.valueOf(encryptedPayload.length()));
46+
} catch (EncryptionException e) {
47+
throw new EncodeException("Failed to encrypt request!", e);
48+
}
49+
}
50+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package com.mastercard.developer.interceptor;
2+
3+
import com.mastercard.developer.encryption.EncryptionException;
4+
import com.mastercard.developer.encryption.FieldLevelEncryptionConfig;
5+
import com.mastercard.developer.interceptors.FeignFieldLevelEncryptionDecoder;
6+
import feign.Response;
7+
import feign.Util;
8+
import feign.codec.DecodeException;
9+
import feign.codec.Decoder;
10+
import org.junit.Assert;
11+
import org.junit.Rule;
12+
import org.junit.Test;
13+
import org.junit.rules.ExpectedException;
14+
import org.mockito.ArgumentCaptor;
15+
16+
import java.lang.reflect.Type;
17+
import java.nio.charset.StandardCharsets;
18+
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
22+
import static com.mastercard.developer.test.TestUtils.assertPayloadEquals;
23+
import static com.mastercard.developer.test.TestUtils.getTestFieldLevelEncryptionConfigBuilder;
24+
import static org.hamcrest.core.Is.isA;
25+
import static org.mockito.ArgumentMatchers.any;
26+
import static org.mockito.Mockito.*;
27+
28+
public class FeignFieldLevelEncryptionDecoderTest {
29+
30+
@Rule
31+
public ExpectedException expectedException = ExpectedException.none();
32+
33+
@Test
34+
public void testDecode_ShouldDecryptResponsePayloadAndUpdateContentLengthHeader() throws Exception {
35+
36+
// GIVEN
37+
String encryptedPayload = "{" +
38+
" \"encryptedData\": {" +
39+
" \"iv\": \"a32059c51607d0d02e823faecda5fb15\"," +
40+
" \"encryptedKey\": \"a31cfe7a7981b72428c013270619554c1d645c04b9d51c7eaf996f55749ef62fd7c7f8d334f95913be41ae38c46d192670fd1acb84ebb85a00cd997f1a9a3f782229c7bf5f0fdf49fe404452d7ed4fd41fbb95b787d25893fbf3d2c75673cecc8799bbe3dd7eb4fe6d3f744b377572cdf8aba1617194e10475b6cd6a8dd4fb8264f8f51534d8f7ac7c10b4ce9c44d15066724b03a0ab0edd512f9e6521fdb5841cd6964e457d6b4a0e45ba4aac4e77d6bbe383d6147e751fa88bc26278bb9690f9ee84b17123b887be2dcef0873f4f9f2c895d90e23456fafb01b99885e31f01a3188f0ad47edf22999cc1d0ddaf49e1407375117b5d66f1f185f2b57078d255\"," +
41+
" \"encryptedValue\": \"21d754bdb4567d35d58720c9f8364075\"," +
42+
" \"oaepHashingAlgorithm\": \"SHA256\"" +
43+
" }" +
44+
"}";
45+
FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder()
46+
.withDecryptionPath("$.encryptedData", "$.data")
47+
.build();
48+
49+
Type type = mock(Type.class);
50+
HashMap<String, Collection<String>> headers = new HashMap<>();
51+
headers.put("content-length", Collections.singleton("100"));
52+
Response response = Response.builder()
53+
.status(200)
54+
.headers(headers)
55+
.body(encryptedPayload, StandardCharsets.UTF_8)
56+
.build();
57+
Decoder delegate = mock(Decoder.class);
58+
59+
// WHEN
60+
FeignFieldLevelEncryptionDecoder instanceUnderTest = new FeignFieldLevelEncryptionDecoder(config, delegate);
61+
instanceUnderTest.decode(response, type);
62+
63+
// THEN
64+
ArgumentCaptor<Response> responseCaptor = ArgumentCaptor.forClass(Response.class);
65+
verify(delegate).decode(responseCaptor.capture(), any(Type.class));
66+
Response responseValue = responseCaptor.getValue();
67+
String payload = Util.toString(responseValue.body().asReader());
68+
assertPayloadEquals("{\"data\":\"string\"}", payload);
69+
Assert.assertEquals(String.valueOf(payload.length()), responseValue.headers().get("Content-Length").toArray()[0]);
70+
}
71+
72+
@Test
73+
public void testDecode_ShouldDoNothing_WhenNoPayload() throws Exception {
74+
75+
// GIVEN
76+
FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder().build();
77+
Type type = mock(Type.class);
78+
Response response = mock(Response.class);
79+
Decoder delegate = mock(Decoder.class);
80+
when(response.body()).thenReturn(null);
81+
82+
// WHEN
83+
FeignFieldLevelEncryptionDecoder instanceUnderTest = new FeignFieldLevelEncryptionDecoder(config, delegate);
84+
instanceUnderTest.decode(response, type);
85+
86+
// THEN
87+
verify(delegate).decode(any(Response.class), any(Type.class));
88+
verify(response).body();
89+
verifyNoMoreInteractions(response);
90+
}
91+
92+
@Test
93+
public void testDecode_ShouldDoNothing_WhenEmptyPayload() throws Exception {
94+
95+
// GIVEN
96+
FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder().build();
97+
Type type = mock(Type.class);
98+
Response response = mock(Response.class);
99+
when(response.body()).thenReturn(buildResponseBody(""));
100+
Decoder delegate = mock(Decoder.class);
101+
102+
// WHEN
103+
FeignFieldLevelEncryptionDecoder instanceUnderTest = new FeignFieldLevelEncryptionDecoder(config, delegate);
104+
instanceUnderTest.decode(response, type);
105+
106+
// THEN
107+
verify(delegate).decode(any(Response.class), any(Type.class));
108+
verify(response).body();
109+
verifyNoMoreInteractions(response);
110+
}
111+
112+
@Test
113+
public void testDecode_ShouldThrowDecodeException_WhenDecryptionFails() throws Exception {
114+
115+
// GIVEN
116+
String encryptedPayload = "{" +
117+
" \"encryptedData\": {" +
118+
" \"iv\": \"a2c494ca28dec4f3d6ce7d68b1044cfe\"," +
119+
" \"encryptedKey\": \"NOT A VALID KEY!\"," +
120+
" \"encryptedValue\": \"0672589113046bf692265b6ea6088184\"" +
121+
" }" +
122+
"}";
123+
FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder()
124+
.withDecryptionPath("$.encryptedData", "$.data")
125+
.build();
126+
Type type = mock(Type.class);
127+
Response response = mock(Response.class);
128+
when(response.body()).thenReturn(buildResponseBody(encryptedPayload));
129+
Decoder delegate = mock(Decoder.class);
130+
131+
// THEN
132+
expectedException.expect(DecodeException.class);
133+
expectedException.expectMessage("Failed to decrypt response!");
134+
expectedException.expectCause(isA(EncryptionException.class));
135+
136+
// WHEN
137+
FeignFieldLevelEncryptionDecoder instanceUnderTest = new FeignFieldLevelEncryptionDecoder(config, delegate);
138+
instanceUnderTest.decode(response, type);
139+
}
140+
141+
private static Response.Body buildResponseBody(String payload) {
142+
Response response = Response.builder()
143+
.status(200)
144+
.headers(new HashMap<String, Collection<String>>())
145+
.body(payload, StandardCharsets.UTF_8)
146+
.build();
147+
return response.body();
148+
}
149+
}

0 commit comments

Comments
 (0)