Skip to content

Commit 7fded27

Browse files
committed
Add support for response wrapping #78
See issue #78
1 parent c97d805 commit 7fded27

5 files changed

Lines changed: 236 additions & 23 deletions

File tree

src/main/java/com/bettercloud/vault/api/Auth.java

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import com.bettercloud.vault.json.JsonObject;
88
import com.bettercloud.vault.response.AuthResponse;
99
import com.bettercloud.vault.response.LookupResponse;
10-
import com.bettercloud.vault.rest.RestResponse;
1110
import com.bettercloud.vault.rest.Rest;
11+
import com.bettercloud.vault.rest.RestResponse;
1212
import lombok.Getter;
1313

1414
import java.io.Serializable;
@@ -1196,4 +1196,111 @@ public void revokeSelf(final String tokenAuthMount) throws VaultException {
11961196
}
11971197
}
11981198

1199+
/**
1200+
* <p>Returns the original response inside the wrapped auth token. This method is useful if you need to unwrap a
1201+
* token without being authenticated. See {@link #unwrap(String)} if you need to do that authenticated.</p>
1202+
*
1203+
* <p>In the example below, you cannot use twice the {@code VaultConfig}, since
1204+
* after the first usage of the {@code wrappingToken}, it is not usable anymore. You need to use the
1205+
* {@code unwrappedToken} in a new vault configuration to continue. Example usage:</p>
1206+
*
1207+
* <blockquote>
1208+
* <pre>{@code
1209+
* final String wrappingToken = "...";
1210+
* final VaultConfig config = new VaultConfig().address(...).token(wrappingToken).build();
1211+
* final Vault vault = new Vault(config);
1212+
* final AuthResponse response = vault.auth().unwrap();
1213+
* final String unwrappedToken = response.getAuthClientToken();
1214+
* }</pre>
1215+
* </blockquote>
1216+
*
1217+
* @return The response information returned from Vault
1218+
* @throws VaultException If any error occurs, or unexpected response received from Vault
1219+
* @see #unwrap(String)
1220+
*/
1221+
public AuthResponse unwrap() throws VaultException {
1222+
return unwrap(null);
1223+
}
1224+
1225+
/**
1226+
* <p>Returns the original response inside the given wrapped auth token. This method is useful if you need to unwrap
1227+
* a token, while being already authenticated. Do NOT authenticate in vault with your wrapping token, since it will
1228+
* both fail authentication and invalidate the wrapping token at the same time. See {@link #unwrap()} if you need to
1229+
* do that without being authenticated.</p>
1230+
*
1231+
* <p>In the example below, {@code authToken} is NOT your wrapped token, and should have unwrapping permissions.
1232+
* The unwrapped token in {@code unwrappedToken}. Example usage:</p>
1233+
*
1234+
* <blockquote>
1235+
* <pre>{@code
1236+
* final String authToken = "...";
1237+
* final String wrappingToken = "...";
1238+
* final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
1239+
* final Vault vault = new Vault(config);
1240+
* final AuthResponse response = vault.auth().unwrap(wrappingToken);
1241+
* final String unwrappedToken = response.getAuthClientToken();
1242+
* }</pre>
1243+
* </blockquote>
1244+
*
1245+
* @param wrappedToken Specifies the wrapping token ID, do NOT also put this in your {@link VaultConfig#token},
1246+
* if token is {@code null}, this method will unwrap the auth token in {@link VaultConfig#token}
1247+
* @return The response information returned from Vault
1248+
* @throws VaultException If any error occurs, or unexpected response received from Vault
1249+
* @see #unwrap()
1250+
*/
1251+
public AuthResponse unwrap(final String wrappedToken) throws VaultException {
1252+
int retryCount = 0;
1253+
while (true) {
1254+
try {
1255+
// Parse parameters to JSON
1256+
final JsonObject jsonObject = Json.object();
1257+
if (wrappedToken != null) {
1258+
jsonObject.add("token", wrappedToken);
1259+
}
1260+
1261+
final String requestJson = jsonObject.toString();
1262+
final String url = config.getAddress() + "/v1/sys/wrapping/unwrap";
1263+
1264+
// HTTP request to Vault
1265+
final RestResponse restResponse = new Rest()
1266+
.url(url)
1267+
.header("X-Vault-Token", config.getToken())
1268+
.body(requestJson.getBytes("UTF-8"))
1269+
.connectTimeoutSeconds(config.getOpenTimeout())
1270+
.readTimeoutSeconds(config.getReadTimeout())
1271+
.sslVerification(config.getSslConfig().isVerify())
1272+
.sslContext(config.getSslConfig().getSslContext())
1273+
.post();
1274+
1275+
// Validate restResponse
1276+
if (restResponse.getStatus() != 200) {
1277+
throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(),
1278+
restResponse.getStatus());
1279+
}
1280+
final String mimeType = restResponse.getMimeType() == null ? "null" : restResponse.getMimeType();
1281+
if (!mimeType.equals("application/json")) {
1282+
throw new VaultException("Vault responded with MIME type: " + mimeType, restResponse.getStatus());
1283+
}
1284+
return new AuthResponse(restResponse, retryCount);
1285+
} catch (final Exception e) {
1286+
// If there are retries to perform, then pause for the configured interval and then execute the
1287+
// loop again...
1288+
if (retryCount < config.getMaxRetries()) {
1289+
retryCount++;
1290+
try {
1291+
final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
1292+
Thread.sleep(retryIntervalMilliseconds);
1293+
} catch (InterruptedException e1) {
1294+
e1.printStackTrace();
1295+
}
1296+
} else if (e instanceof VaultException) {
1297+
// ... otherwise, give up.
1298+
throw (VaultException) e;
1299+
} else {
1300+
throw new VaultException(e);
1301+
}
1302+
}
1303+
}
1304+
}
1305+
11991306
}

src/test/java/com/bettercloud/vault/vault/VaultTestUtils.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.bettercloud.vault.vault;
22

3+
import com.bettercloud.vault.json.Json;
4+
import com.bettercloud.vault.json.JsonObject;
35
import com.bettercloud.vault.vault.mock.MockVault;
6+
import org.apache.commons.io.IOUtils;
47
import org.eclipse.jetty.server.Connector;
58
import org.eclipse.jetty.server.HttpConfiguration;
69
import org.eclipse.jetty.server.HttpConnectionFactory;
@@ -10,6 +13,15 @@
1013
import org.eclipse.jetty.server.SslConnectionFactory;
1114
import org.eclipse.jetty.util.ssl.SslContextFactory;
1215

16+
import javax.servlet.http.HttpServletRequest;
17+
import java.io.IOException;
18+
import java.util.Collections;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
import static java.util.function.Function.identity;
23+
import static java.util.stream.Collectors.toMap;
24+
1325
/**
1426
* <p>Utilities used by all of the Vault-related unit test classes under
1527
* <code>src/test/java/com/bettercloud/vault</code>, to setup and shutdown mock Vault server implementations.</p>
@@ -54,5 +66,20 @@ public static void shutdownMockVault(final Server server) throws Exception {
5466
}
5567
}
5668

69+
public static Optional<JsonObject> readRequestBody(HttpServletRequest request) {
70+
try {
71+
StringBuilder requestBuffer = new StringBuilder();
72+
IOUtils.readLines(request.getReader()).forEach(requestBuffer::append);
73+
return Optional.of(Json.parse(requestBuffer.toString()).asObject());
74+
} catch (IOException e) {
75+
return Optional.empty();
76+
}
77+
}
78+
79+
public static Map<String, String> readRequestHeaders(HttpServletRequest request) {
80+
return Collections.list(request.getHeaderNames()).stream()
81+
.collect(toMap(identity(), request::getHeader));
82+
}
83+
5784
}
5885

src/test/java/com/bettercloud/vault/vault/api/AuthBackendAwsTests.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@
33
import com.bettercloud.vault.Vault;
44
import com.bettercloud.vault.VaultConfig;
55
import com.bettercloud.vault.VaultException;
6-
import com.bettercloud.vault.json.Json;
76
import com.bettercloud.vault.json.JsonObject;
87
import com.bettercloud.vault.vault.VaultTestUtils;
98
import com.bettercloud.vault.vault.mock.AuthRequestValidatingMockVault;
10-
import org.apache.commons.io.IOUtils;
119
import org.eclipse.jetty.server.Server;
1210
import org.junit.Test;
1311

1412
import javax.servlet.http.HttpServletRequest;
1513
import java.util.function.Predicate;
1614

15+
import static com.bettercloud.vault.vault.VaultTestUtils.readRequestBody;
1716
import static org.junit.Assert.assertEquals;
1817
import static org.junit.Assert.assertNotNull;
1918

@@ -23,7 +22,7 @@ public class AuthBackendAwsTests {
2322
public void testLoginByAwsEc2Id() throws Exception {
2423
final Predicate<HttpServletRequest> isValidEc2IdRequest = (request) -> {
2524
try {
26-
JsonObject requestBody = readRequestBody(request);
25+
JsonObject requestBody = readRequestBody(request).orElse(null);
2726
return requestBody != null && request.getRequestURI().endsWith("/auth/aws/login") &&
2827
requestBody.getString("identity", "").equals("identity") &&
2928
requestBody.getString("signature", "").equals("signature");
@@ -59,7 +58,7 @@ public void testLoginByAwsEc2Id() throws Exception {
5958
public void testLoginByAwsEc2Pkcs7() throws Exception {
6059
final Predicate<HttpServletRequest> isValidEc2pkcs7Request = (request) -> {
6160
try {
62-
JsonObject requestBody = readRequestBody(request);
61+
JsonObject requestBody = readRequestBody(request).orElse(null);
6362
return requestBody != null && request.getRequestURI().endsWith("/auth/aws/login") &&
6463
requestBody.getString("pkcs7", "").equals("pkcs7");
6564
} catch (Exception e) {
@@ -95,7 +94,7 @@ public void testLoginByAwsEc2Pkcs7() throws Exception {
9594
@Test
9695
public void testLoginByAwsIam() throws Exception {
9796
final Predicate<HttpServletRequest> isValidEc2IamRequest = (request) -> {
98-
JsonObject requestBody = readRequestBody(request);
97+
JsonObject requestBody = readRequestBody(request).orElse(null);
9998
return requestBody != null && request.getRequestURI().endsWith("/auth/aws/login") &&
10099
requestBody.getString("iam_http_request_method", "").equals("POST") &&
101100
requestBody.getString("iam_request_url", "").equals("url") &&
@@ -123,14 +122,4 @@ public void testLoginByAwsIam() throws Exception {
123122
assertEquals("c9368254-3f21-aded-8a6f-7c818e81b17a", token.trim());
124123
}
125124

126-
private JsonObject readRequestBody(HttpServletRequest request) {
127-
try {
128-
StringBuilder requestBuffer = new StringBuilder();
129-
IOUtils.readLines(request.getReader()).forEach(requestBuffer::append);
130-
return Json.parse(requestBuffer.toString()).asObject();
131-
} catch (Exception e) {
132-
return null;
133-
}
134-
}
135-
136125
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.bettercloud.vault.vault.api;
2+
3+
import com.bettercloud.vault.Vault;
4+
import com.bettercloud.vault.VaultConfig;
5+
import com.bettercloud.vault.json.JsonArray;
6+
import com.bettercloud.vault.json.JsonObject;
7+
import com.bettercloud.vault.response.AuthResponse;
8+
import com.bettercloud.vault.vault.VaultTestUtils;
9+
import com.bettercloud.vault.vault.mock.MockVault;
10+
import org.eclipse.jetty.server.Server;
11+
import org.junit.After;
12+
import org.junit.Before;
13+
import org.junit.Test;
14+
15+
import static org.junit.Assert.assertEquals;
16+
17+
public class AuthUnwrapTest {
18+
19+
private static final JsonObject RESPONSE_AUTH_UNWRAP = new JsonObject()
20+
.add("renewable", false)
21+
.add("auth", new JsonObject()
22+
.add("policies", new JsonArray())
23+
.add("client_token", "unwrappedToken"));
24+
25+
private Server server;
26+
private MockVault vaultServer;
27+
28+
@Before
29+
public void before() throws Exception {
30+
vaultServer = new MockVault(200, RESPONSE_AUTH_UNWRAP.toString());
31+
server = VaultTestUtils.initHttpMockVault(vaultServer);
32+
server.start();
33+
}
34+
35+
@After
36+
public void after() throws Exception {
37+
VaultTestUtils.shutdownMockVault(server);
38+
}
39+
40+
@Test
41+
public void should_unwrap_without_param_sends_no_token_and_return_unwrapped_token() throws Exception {
42+
VaultConfig vaultConfig = new VaultConfig().address("http://127.0.0.1:8999").token("wrappedToken").build();
43+
Vault vault = new Vault(vaultConfig);
44+
AuthResponse response = vault.auth().unwrap();
45+
46+
assertEquals(200, response.getRestResponse().getStatus());
47+
48+
// Assert request body should NOT have token body (wrapped is in header)
49+
assertEquals(null, vaultServer.getRequestBody().get("token"));
50+
assertEquals("wrappedToken", vaultServer.getRequestHeaders().get("X-Vault-Token"));
51+
52+
// Assert response should have the unwrapped token in the client_token key
53+
assertEquals("unwrappedToken", response.getAuthClientToken());
54+
}
55+
56+
@Test
57+
public void should_unwrap_param_sends_token_and_return_unwrapped_token() throws Exception {
58+
VaultConfig vaultConfig = new VaultConfig().address("http://127.0.0.1:8999").token("authToken").build();
59+
Vault vault = new Vault(vaultConfig);
60+
AuthResponse response = vault.auth().unwrap("wrappedToken");
61+
62+
assertEquals(200, response.getRestResponse().getStatus());
63+
64+
// Assert request body SHOULD have token body
65+
assertEquals("wrappedToken", vaultServer.getRequestBody().getString("token", null));
66+
assertEquals("authToken", vaultServer.getRequestHeaders().get("X-Vault-Token"));
67+
68+
// Assert response should have the unwrapped token in the client_token key
69+
assertEquals("unwrappedToken", response.getAuthClientToken());
70+
}
71+
72+
}

src/test/java/com/bettercloud/vault/vault/mock/MockVault.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package com.bettercloud.vault.vault.mock;
22

3-
import org.eclipse.jetty.server.Request;
4-
import org.eclipse.jetty.server.handler.AbstractHandler;
3+
import static com.bettercloud.vault.vault.VaultTestUtils.readRequestBody;
4+
import static com.bettercloud.vault.vault.VaultTestUtils.readRequestHeaders;
55

6+
import java.io.IOException;
7+
import java.util.Map;
68
import javax.servlet.ServletException;
79
import javax.servlet.http.HttpServletRequest;
810
import javax.servlet.http.HttpServletResponse;
9-
import java.io.IOException;
11+
12+
import org.eclipse.jetty.server.Request;
13+
import org.eclipse.jetty.server.handler.AbstractHandler;
14+
15+
import com.bettercloud.vault.json.JsonObject;
1016

1117
/**
1218
* <p>This class is used to mock out a Vault server in unit tests involving retry logic. As it extends Jetty's
@@ -22,7 +28,7 @@
2228
* server.setHandler( new MockVault(200, "{\"data\":{\"value\":\"mock\"}}") );
2329
* server.start();
2430
*
25-
* final VaultConfig vaultConfig = new VaultConfig("http://127.0.0.1:8999", "mock_token");
31+
* final VaultConfig vaultConfig = new VaultConfig().address("http://127.0.0.1:8999").token("mock_token").build();
2632
* final Vault vault = new Vault(vaultConfig);
2733
* final LogicalResponse response = vault.logical().read("secret/hello");
2834
*
@@ -37,22 +43,26 @@ public class MockVault extends AbstractHandler {
3743

3844
private int mockStatus;
3945
private String mockResponse;
46+
private JsonObject requestBody;
47+
private Map<String, String> requestHeaders;
48+
49+
MockVault() {
50+
}
4051

4152
public MockVault(final int mockStatus, final String mockResponse) {
4253
this.mockStatus = mockStatus;
4354
this.mockResponse = mockResponse;
4455
}
4556

46-
protected MockVault() {
47-
}
48-
4957
@Override
5058
public void handle(
5159
final String target,
5260
final Request baseRequest,
5361
final HttpServletRequest request,
5462
final HttpServletResponse response
5563
) throws IOException, ServletException {
64+
requestBody = readRequestBody(request).orElse(null);
65+
requestHeaders = readRequestHeaders(request);
5666
response.setContentType("application/json");
5767
baseRequest.setHandled(true);
5868
System.out.println("MockVault is sending an HTTP " + mockStatus + " code, with expected success payload...");
@@ -62,4 +72,12 @@ public void handle(
6272
}
6373
}
6474

75+
public JsonObject getRequestBody() {
76+
return requestBody;
77+
}
78+
79+
public Map<String, String> getRequestHeaders() {
80+
return requestHeaders;
81+
}
82+
6583
}

0 commit comments

Comments
 (0)