Skip to content

Commit ae80759

Browse files
Merge pull request BetterCloud#90 from dshva/aws-login
Implement AWS login
2 parents 30a537a + 3455f60 commit ae80759

File tree

3 files changed

+394
-0
lines changed

3 files changed

+394
-0
lines changed

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

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,231 @@ public AuthResponse loginByLDAP(final String username, final String password, fi
535535
return loginByUserPass(username, password, mount);
536536
}
537537

538+
/**
539+
* <p>Basic login operation to authenticate to a AWS backend using EC2 authentication. Example usage:</p>
540+
*
541+
* <blockquote>
542+
* <pre>{@code
543+
* final AuthResponse response = vault.auth().loginByAwsEc2("my-role", "identity", "signature", "nonce", null);
544+
*
545+
* final String token = response.getAuthClientToken();
546+
* }</pre>
547+
* </blockquote>
548+
*
549+
* @param role Name of the role against which the login is being attempted. If role is not specified, then the login endpoint
550+
* looks for a role bearing the name of the AMI ID of the EC2 instance that is trying to login if using the ec2
551+
* auth method, or the "friendly name" (i.e., role name or username) of the IAM principal authenticated.
552+
* If a matching role is not found, login fails.
553+
* @param identity Base64 encoded EC2 instance identity document.
554+
* @param signature Base64 encoded SHA256 RSA signature of the instance identity document.
555+
* @param nonce Client nonce used for authentication. If null, a new nonce will be generated by Vault
556+
* @return The auth token, with additional response metadata
557+
* @throws VaultException If any error occurs, or unexpected response received from Vault
558+
*/
559+
public AuthResponse loginByAwsEc2(final String role, final String identity, final String signature, final String nonce, final String awsAuthMount) throws VaultException {
560+
int retryCount = 0;
561+
562+
final String mount = awsAuthMount != null ? awsAuthMount : "aws";
563+
while (true) {
564+
try {
565+
// HTTP request to Vault
566+
final JsonObject request = Json.object().add("identity", identity)
567+
.add("signature", signature);
568+
if(role != null) {
569+
request.add("role", role);
570+
}
571+
if(nonce != null) {
572+
request.add("nonce", nonce);
573+
}
574+
final String requestJson = request.toString();
575+
final RestResponse restResponse = new Rest()//NOPMD
576+
.url(config.getAddress() + "/v1/auth/" + mount + "/login")
577+
.body(requestJson.getBytes("UTF-8"))
578+
.connectTimeoutSeconds(config.getOpenTimeout())
579+
.readTimeoutSeconds(config.getReadTimeout())
580+
.sslVerification(config.getSslConfig().isVerify())
581+
.sslContext(config.getSslConfig().getSslContext())
582+
.post();
583+
584+
// Validate restResponse
585+
if (restResponse.getStatus() != 200) {
586+
throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus());
587+
}
588+
final String mimeType = restResponse.getMimeType() == null ? "null" : restResponse.getMimeType();
589+
if (!mimeType.equals("application/json")) {
590+
throw new VaultException("Vault responded with MIME type: " + mimeType, restResponse.getStatus());
591+
}
592+
return new AuthResponse(restResponse, retryCount);
593+
} catch (Exception e) {
594+
// If there are retries to perform, then pause for the configured interval and then execute the loop again...
595+
if (retryCount < config.getMaxRetries()) {
596+
retryCount++;
597+
try {
598+
final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
599+
Thread.sleep(retryIntervalMilliseconds);
600+
} catch (InterruptedException e1) {
601+
e1.printStackTrace();
602+
}
603+
} else if (e instanceof VaultException) {
604+
// ... otherwise, give up.
605+
throw (VaultException) e;
606+
} else {
607+
throw new VaultException(e);
608+
}
609+
}
610+
}
611+
}
612+
613+
/**
614+
* <p>Basic login operation to authenticate to a AWS backend using EC2 authentication. Example usage:</p>
615+
*
616+
* <blockquote>
617+
* <pre>{@code
618+
* final AuthResponse response = vault.auth().loginByAwsEc2("my-role", "pkcs7", "nonce", null);
619+
*
620+
* final String token = response.getAuthClientToken();
621+
* }</pre>
622+
* </blockquote>
623+
*
624+
* @param role Name of the role against which the login is being attempted. If role is not specified, then the login endpoint
625+
* looks for a role bearing the name of the AMI ID of the EC2 instance that is trying to login if using the ec2
626+
* auth method, or the "friendly name" (i.e., role name or username) of the IAM principal authenticated.
627+
* If a matching role is not found, login fails.
628+
* @param pkcs7 PKCS7 signature of the identity document with all \n characters removed.
629+
* @param nonce Client nonce used for authentication. If null, a new nonce will be generated by Vault
630+
* @return The auth token, with additional response metadata
631+
* @throws VaultException If any error occurs, or unexpected response received from Vault
632+
*/
633+
public AuthResponse loginByAwsEc2(final String role, final String pkcs7, final String nonce, final String awsAuthMount) throws VaultException {
634+
int retryCount = 0;
635+
636+
final String mount = awsAuthMount != null ? awsAuthMount : "aws";
637+
while (true) {
638+
try {
639+
// HTTP request to Vault
640+
final JsonObject request = Json.object().add("pkcs7", pkcs7);
641+
if(role != null) {
642+
request.add("role", role);
643+
}
644+
if(nonce != null) {
645+
request.add("nonce", nonce);
646+
}
647+
final String requestJson = request.toString();
648+
final RestResponse restResponse = new Rest()//NOPMD
649+
.url(config.getAddress() + "/v1/auth/" + mount + "/login")
650+
.body(requestJson.getBytes("UTF-8"))
651+
.connectTimeoutSeconds(config.getOpenTimeout())
652+
.readTimeoutSeconds(config.getReadTimeout())
653+
.sslVerification(config.getSslConfig().isVerify())
654+
.sslContext(config.getSslConfig().getSslContext())
655+
.post();
656+
657+
// Validate restResponse
658+
if (restResponse.getStatus() != 200) {
659+
throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus());
660+
}
661+
final String mimeType = restResponse.getMimeType() == null ? "null" : restResponse.getMimeType();
662+
if (!mimeType.equals("application/json")) {
663+
throw new VaultException("Vault responded with MIME type: " + mimeType, restResponse.getStatus());
664+
}
665+
return new AuthResponse(restResponse, retryCount);
666+
} catch (Exception e) {
667+
// If there are retries to perform, then pause for the configured interval and then execute the loop again...
668+
if (retryCount < config.getMaxRetries()) {
669+
retryCount++;
670+
try {
671+
final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
672+
Thread.sleep(retryIntervalMilliseconds);
673+
} catch (InterruptedException e1) {
674+
e1.printStackTrace();
675+
}
676+
} else if (e instanceof VaultException) {
677+
// ... otherwise, give up.
678+
throw (VaultException) e;
679+
} else {
680+
throw new VaultException(e);
681+
}
682+
}
683+
}
684+
}
685+
686+
/**
687+
* <p>Basic login operation to authenticate to a AWS backend using IAM authentication. Example usage:</p>
688+
*
689+
* <blockquote>
690+
* <pre>{@code
691+
* final AuthResponse response = vault.auth().loginByAwsIam("my-role", "pkcs7", "nonce", null);
692+
*
693+
* final String token = response.getAuthClientToken();
694+
* }</pre>
695+
* </blockquote>
696+
*
697+
* @param role Name of the role against which the login is being attempted. If role is not specified, then the login endpoint
698+
* looks for a role bearing the name of the AMI ID of the EC2 instance that is trying to login if using the ec2
699+
* auth method, or the "friendly name" (i.e., role name or username) of the IAM principal authenticated.
700+
* If a matching role is not found, login fails.
701+
* @param iamRequestUrl PKCS7 signature of the identity document with all \n characters removed.Base64-encoded HTTP URL used in the signed request.
702+
* Most likely just aHR0cHM6Ly9zdHMuYW1hem9uYXdzLmNvbS8= (base64-encoding of https://sts.amazonaws.com/) as most requests will
703+
* probably use POST with an empty URI.
704+
* @param iamRequestBody Base64-encoded body of the signed request. Most likely QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== which is
705+
* the base64 encoding of Action=GetCallerIdentity&Version=2011-06-15.
706+
* @param iamRequestHeaders
707+
* @return The auth token, with additional response metadata
708+
* @throws VaultException If any error occurs, or unexpected response received from Vault
709+
*/
710+
public AuthResponse loginByAwsIam(final String role, final String iamRequestUrl, final String iamRequestBody, final String iamRequestHeaders, final String awsAuthMount) throws VaultException {
711+
int retryCount = 0;
712+
713+
final String mount = awsAuthMount != null ? awsAuthMount : "aws";
714+
while (true) {
715+
try {
716+
// HTTP request to Vault
717+
final JsonObject request = Json.object().add("iam_request_url", iamRequestUrl)
718+
.add("iam_request_body", iamRequestBody)
719+
.add("iam_request_headers", iamRequestHeaders)
720+
.add("iam_request_method", "POST");
721+
if(role != null) {
722+
request.add("role", role);
723+
}
724+
final String requestJson = request.toString();
725+
final RestResponse restResponse = new Rest()//NOPMD
726+
.url(config.getAddress() + "/v1/auth/" + mount + "/login")
727+
.body(requestJson.getBytes("UTF-8"))
728+
.connectTimeoutSeconds(config.getOpenTimeout())
729+
.readTimeoutSeconds(config.getReadTimeout())
730+
.sslVerification(config.getSslConfig().isVerify())
731+
.sslContext(config.getSslConfig().getSslContext())
732+
.post();
733+
734+
// Validate restResponse
735+
if (restResponse.getStatus() != 200) {
736+
throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus());
737+
}
738+
final String mimeType = restResponse.getMimeType() == null ? "null" : restResponse.getMimeType();
739+
if (!mimeType.equals("application/json")) {
740+
throw new VaultException("Vault responded with MIME type: " + mimeType, restResponse.getStatus());
741+
}
742+
return new AuthResponse(restResponse, retryCount);
743+
} catch (Exception e) {
744+
// If there are retries to perform, then pause for the configured interval and then execute the loop again...
745+
if (retryCount < config.getMaxRetries()) {
746+
retryCount++;
747+
try {
748+
final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
749+
Thread.sleep(retryIntervalMilliseconds);
750+
} catch (InterruptedException e1) {
751+
e1.printStackTrace();
752+
}
753+
} else if (e instanceof VaultException) {
754+
// ... otherwise, give up.
755+
throw (VaultException) e;
756+
} else {
757+
throw new VaultException(e);
758+
}
759+
}
760+
}
761+
}
762+
538763
/**
539764
* <p>Basic login operation to authenticate to an github backend. Example usage:</p>
540765
*
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.bettercloud.vault.api;
2+
3+
import com.bettercloud.vault.Vault;
4+
import com.bettercloud.vault.VaultConfig;
5+
import com.bettercloud.vault.json.Json;
6+
import com.bettercloud.vault.json.JsonObject;
7+
import com.bettercloud.vault.vault.VaultTestUtils;
8+
import com.bettercloud.vault.vault.mock.AuthRequestValidatingMockVault;
9+
import org.apache.commons.io.IOUtils;
10+
import org.eclipse.jetty.server.Server;
11+
import org.junit.Test;
12+
13+
import javax.servlet.http.HttpServletRequest;
14+
import java.util.HashSet;
15+
import java.util.function.Predicate;
16+
17+
import static org.junit.Assert.assertEquals;
18+
import static org.junit.Assert.assertNotNull;
19+
20+
public class AuthBackendAwsTests {
21+
22+
private JsonObject readRequestBody(HttpServletRequest request) {
23+
try {
24+
StringBuilder requestBuffer = new StringBuilder();
25+
IOUtils.readLines(request.getReader()).forEach(requestBuffer::append);
26+
return Json.parse(requestBuffer.toString()).asObject();
27+
} catch (Exception e) {
28+
return null;
29+
}
30+
}
31+
32+
@Test
33+
public void testLoginByAwsEc2() throws Exception {
34+
final Predicate<HttpServletRequest> isValidEc2pkcs7Request = (request) -> {
35+
JsonObject requestBody = readRequestBody(request);
36+
return requestBody != null && request.getRequestURI().endsWith("/auth/aws/login") &&
37+
requestBody.getString("pkcs7", "") == "pkcs7";
38+
};
39+
40+
final Predicate<HttpServletRequest> isValidEc2IdRequest = (request) -> {
41+
JsonObject requestBody = readRequestBody(request);
42+
return requestBody != null && request.getRequestURI().endsWith("/auth/aws/login") &&
43+
requestBody.getString("identity", "") == "identity" &&
44+
requestBody.getString("signature", "") == "signature";
45+
};
46+
47+
final Predicate<HttpServletRequest> isValidEc2IamRequest = (request) -> {
48+
JsonObject requestBody = readRequestBody(request);
49+
return requestBody != null && request.getRequestURI().endsWith("/auth/aws/login") &&
50+
requestBody.getString("iam_http_request_method", "") == "POST" &&
51+
requestBody.getString("iam_http_request_url", "") == "url" &&
52+
requestBody.getString("iam_http_request_body", "") == "body" &&
53+
requestBody.getString("iam_http_request_headers", "") == "headers";
54+
};
55+
56+
final AuthRequestValidatingMockVault mockVault = new AuthRequestValidatingMockVault(new HashSet<Predicate<HttpServletRequest>>() {{
57+
add(isValidEc2pkcs7Request);
58+
add(isValidEc2IdRequest);
59+
}});
60+
61+
final Server server = VaultTestUtils.initHttpMockVault(mockVault);
62+
server.start();
63+
64+
final VaultConfig vaultConfig = new VaultConfig()
65+
.address("http://127.0.0.1:8999")
66+
.build();
67+
final Vault vault = new Vault(vaultConfig);
68+
69+
final String token1 = vault.auth()
70+
.loginByAwsEc2("role","pkcs7",null,null)
71+
.getAuthClientToken();
72+
73+
assertNotNull(token1);
74+
assertEquals("c9368254-3f21-aded-8a6f-7c818e81b17a", token1.trim());
75+
76+
final String token2 = vault.auth()
77+
.loginByAwsEc2("role","identity","signature", null, null)
78+
.getAuthClientToken();
79+
80+
assertNotNull(token2);
81+
assertEquals("c9368254-3f21-aded-8a6f-7c818e81b17a", token2.trim());
82+
}
83+
84+
@Test
85+
public void testLoginByAwsIam() throws Exception {
86+
final Predicate<HttpServletRequest> isValidEc2IamRequest = (request) -> {
87+
JsonObject requestBody = readRequestBody(request);
88+
return requestBody != null && request.getRequestURI().endsWith("/auth/aws/login") &&
89+
requestBody.getString("iam_http_request_method", "") == "POST" &&
90+
requestBody.getString("iam_http_request_url", "") == "url" &&
91+
requestBody.getString("iam_http_request_body", "") == "body" &&
92+
requestBody.getString("iam_http_request_headers", "") == "headers";
93+
};
94+
95+
final AuthRequestValidatingMockVault mockVault = new AuthRequestValidatingMockVault(new HashSet<Predicate<HttpServletRequest>>() {{
96+
add(isValidEc2IamRequest);
97+
}});
98+
99+
final Server server = VaultTestUtils.initHttpMockVault(mockVault);
100+
server.start();
101+
102+
final VaultConfig vaultConfig = new VaultConfig()
103+
.address("http://127.0.0.1:8999")
104+
.build();
105+
final Vault vault = new Vault(vaultConfig);
106+
107+
final String token = vault.auth()
108+
.loginByAwsIam("role","url","body","headers",null)
109+
.getAuthClientToken();
110+
111+
assertNotNull(token);
112+
assertEquals("c9368254-3f21-aded-8a6f-7c818e81b17a", token.trim());
113+
}
114+
}

0 commit comments

Comments
 (0)