1package awsauth
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"net/http"
8	"net/http/httptest"
9	"net/url"
10	"strings"
11	"testing"
12
13	"github.com/aws/aws-sdk-go/aws/session"
14	"github.com/aws/aws-sdk-go/service/sts"
15	"github.com/hashicorp/vault/logical"
16)
17
18func TestBackend_pathLogin_getCallerIdentityResponse(t *testing.T) {
19	responseFromUser := `<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
20  <GetCallerIdentityResult>
21    <Arn>arn:aws:iam::123456789012:user/MyUserName</Arn>
22    <UserId>ASOMETHINGSOMETHINGSOMETHING</UserId>
23    <Account>123456789012</Account>
24  </GetCallerIdentityResult>
25  <ResponseMetadata>
26    <RequestId>7f4fc40c-853a-11e6-8848-8d035d01eb87</RequestId>
27  </ResponseMetadata>
28</GetCallerIdentityResponse>`
29	expectedUserArn := "arn:aws:iam::123456789012:user/MyUserName"
30
31	responseFromAssumedRole := `<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
32  <GetCallerIdentityResult>
33  <Arn>arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName</Arn>
34  <UserId>ASOMETHINGSOMETHINGELSE:RoleSessionName</UserId>
35    <Account>123456789012</Account>
36  </GetCallerIdentityResult>
37  <ResponseMetadata>
38    <RequestId>7f4fc40c-853a-11e6-8848-8d035d01eb87</RequestId>
39  </ResponseMetadata>
40</GetCallerIdentityResponse>`
41	expectedRoleArn := "arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName"
42
43	parsedUserResponse, err := parseGetCallerIdentityResponse(responseFromUser)
44	if err != nil {
45		t.Fatal(err)
46	}
47	if parsedArn := parsedUserResponse.GetCallerIdentityResult[0].Arn; parsedArn != expectedUserArn {
48		t.Errorf("expected to parse arn %#v, got %#v", expectedUserArn, parsedArn)
49	}
50
51	parsedRoleResponse, err := parseGetCallerIdentityResponse(responseFromAssumedRole)
52	if err != nil {
53		t.Fatal(err)
54	}
55	if parsedArn := parsedRoleResponse.GetCallerIdentityResult[0].Arn; parsedArn != expectedRoleArn {
56		t.Errorf("expected to parn arn %#v; got %#v", expectedRoleArn, parsedArn)
57	}
58
59	_, err = parseGetCallerIdentityResponse("SomeRandomGibberish")
60	if err == nil {
61		t.Errorf("expected to NOT parse random giberish, but didn't get an error")
62	}
63}
64
65func TestBackend_pathLogin_parseIamArn(t *testing.T) {
66	testParser := func(inputArn, expectedCanonicalArn string, expectedEntity iamEntity) {
67		entity, err := parseIamArn(inputArn)
68		if err != nil {
69			t.Fatal(err)
70		}
71		if expectedCanonicalArn != "" && entity.canonicalArn() != expectedCanonicalArn {
72			t.Fatalf("expected to canonicalize ARN %q into %q but got %q instead", inputArn, expectedCanonicalArn, entity.canonicalArn())
73		}
74		if *entity != expectedEntity {
75			t.Fatalf("expected to get iamEntity %#v from input ARN %q but instead got %#v", expectedEntity, inputArn, *entity)
76		}
77	}
78
79	testParser("arn:aws:iam::123456789012:user/UserPath/MyUserName",
80		"arn:aws:iam::123456789012:user/MyUserName",
81		iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "user", Path: "UserPath", FriendlyName: "MyUserName"},
82	)
83	canonicalRoleArn := "arn:aws:iam::123456789012:role/RoleName"
84	testParser("arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName",
85		canonicalRoleArn,
86		iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "assumed-role", FriendlyName: "RoleName", SessionInfo: "RoleSessionName"},
87	)
88	testParser("arn:aws:iam::123456789012:role/RolePath/RoleName",
89		canonicalRoleArn,
90		iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "role", Path: "RolePath", FriendlyName: "RoleName"},
91	)
92	testParser("arn:aws:iam::123456789012:instance-profile/profilePath/InstanceProfileName",
93		"",
94		iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "instance-profile", Path: "profilePath", FriendlyName: "InstanceProfileName"},
95	)
96
97	// Test that it properly handles pathological inputs...
98	_, err := parseIamArn("")
99	if err == nil {
100		t.Error("expected error from empty input string")
101	}
102
103	_, err = parseIamArn("arn:aws:iam::123456789012:role")
104	if err == nil {
105		t.Error("expected error from malformed ARN without a role name")
106	}
107
108	_, err = parseIamArn("arn:aws:iam")
109	if err == nil {
110		t.Error("expected error from incomplete ARN (arn:aws:iam)")
111	}
112
113	_, err = parseIamArn("arn:aws:iam::1234556789012:/")
114	if err == nil {
115		t.Error("expected error from empty principal type and no principal name (arn:aws:iam::1234556789012:/)")
116	}
117}
118
119func TestBackend_validateVaultHeaderValue(t *testing.T) {
120	const canaryHeaderValue = "Vault-Server"
121	requestURL, err := url.Parse("https://sts.amazonaws.com/")
122	if err != nil {
123		t.Fatalf("error parsing test URL: %v", err)
124	}
125	postHeadersMissing := http.Header{
126		"Host":          []string{"Foo"},
127		"Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"},
128	}
129	postHeadersInvalid := http.Header{
130		"Host":            []string{"Foo"},
131		iamServerIdHeader: []string{"InvalidValue"},
132		"Authorization":   []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"},
133	}
134	postHeadersUnsigned := http.Header{
135		"Host":            []string{"Foo"},
136		iamServerIdHeader: []string{canaryHeaderValue},
137		"Authorization":   []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"},
138	}
139	postHeadersValid := http.Header{
140		"Host":            []string{"Foo"},
141		iamServerIdHeader: []string{canaryHeaderValue},
142		"Authorization":   []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"},
143	}
144
145	postHeadersSplit := http.Header{
146		"Host":            []string{"Foo"},
147		iamServerIdHeader: []string{canaryHeaderValue},
148		"Authorization":   []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request", "SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"},
149	}
150
151	err = validateVaultHeaderValue(postHeadersMissing, requestURL, canaryHeaderValue)
152	if err == nil {
153		t.Error("validated POST request with missing Vault header")
154	}
155
156	err = validateVaultHeaderValue(postHeadersInvalid, requestURL, canaryHeaderValue)
157	if err == nil {
158		t.Error("validated POST request with invalid Vault header value")
159	}
160
161	err = validateVaultHeaderValue(postHeadersUnsigned, requestURL, canaryHeaderValue)
162	if err == nil {
163		t.Error("validated POST request with unsigned Vault header")
164	}
165
166	err = validateVaultHeaderValue(postHeadersValid, requestURL, canaryHeaderValue)
167	if err != nil {
168		t.Errorf("did NOT validate valid POST request: %v", err)
169	}
170
171	err = validateVaultHeaderValue(postHeadersSplit, requestURL, canaryHeaderValue)
172	if err != nil {
173		t.Errorf("did NOT validate valid POST request with split Authorization header: %v", err)
174	}
175}
176
177// TestBackend_pathLogin_IAMHeaders tests login with iam_request_headers,
178// supporting both base64 encoded string and JSON headers
179func TestBackend_pathLogin_IAMHeaders(t *testing.T) {
180	storage := &logical.InmemStorage{}
181	config := logical.TestBackendConfig()
182	config.StorageView = storage
183	b, err := Backend(config)
184	if err != nil {
185		t.Fatal(err)
186	}
187
188	err = b.Setup(context.Background(), config)
189	if err != nil {
190		t.Fatal(err)
191	}
192
193	// sets up a test server to stand in for STS service
194	ts := setupIAMTestServer()
195	defer ts.Close()
196
197	clientConfigData := map[string]interface{}{
198		"iam_server_id_header_value": testVaultHeaderValue,
199		"sts_endpoint":               ts.URL,
200	}
201	clientRequest := &logical.Request{
202		Operation: logical.UpdateOperation,
203		Path:      "config/client",
204		Storage:   storage,
205		Data:      clientConfigData,
206	}
207	_, err = b.HandleRequest(context.Background(), clientRequest)
208	if err != nil {
209		t.Fatal(err)
210	}
211
212	// create a role entry
213	roleEntry := &awsRoleEntry{
214		Version:  currentRoleStorageVersion,
215		AuthType: iamAuthType,
216	}
217
218	if err := b.nonLockedSetAWSRole(context.Background(), storage, testValidRoleName, roleEntry); err != nil {
219		t.Fatalf("failed to set entry: %s", err)
220	}
221
222	// create a baseline loginData map structure, including iam_request_headers
223	// already base64encoded. This is the "Default" loginData used for all tests.
224	// Each sub test can override the map's iam_request_headers entry
225	loginData, err := defaultLoginData()
226	if err != nil {
227		t.Fatal(err)
228	}
229
230	// expected errors for certain tests
231	missingHeaderErr := errors.New("error validating X-Vault-AWS-IAM-Server-ID header: missing header \"X-Vault-AWS-IAM-Server-ID\"")
232	parsingErr := errors.New("error making upstream request: error parsing STS response")
233
234	testCases := []struct {
235		Name      string
236		Header    interface{}
237		ExpectErr error
238	}{
239		{
240			Name: "Default",
241		},
242		{
243			Name: "Map-complete",
244			Header: map[string]interface{}{
245				"Content-Length":            "43",
246				"Content-Type":              "application/x-www-form-urlencoded; charset=utf-8",
247				"User-Agent":                "aws-sdk-go/1.14.24 (go1.11; darwin; amd64)",
248				"X-Amz-Date":                "20180910T203328Z",
249				"X-Vault-Aws-Iam-Server-Id": "VaultAcceptanceTesting",
250				"Authorization":             "AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180910/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=cdef5819b2e97f1ff0f3e898fd2621aa03af00a4ec3e019122c20e5482534bf4",
251			},
252		},
253		{
254			Name: "Map-incomplete",
255			Header: map[string]interface{}{
256				"Content-Length": "43",
257				"Content-Type":   "application/x-www-form-urlencoded; charset=utf-8",
258				"User-Agent":     "aws-sdk-go/1.14.24 (go1.11; darwin; amd64)",
259				"X-Amz-Date":     "20180910T203328Z",
260				"Authorization":  "AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180910/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=cdef5819b2e97f1ff0f3e898fd2621aa03af00a4ec3e019122c20e5482534bf4",
261			},
262			ExpectErr: missingHeaderErr,
263		},
264		{
265			Name: "JSON-complete",
266			Header: `{
267				"Content-Length":"43",
268				"Content-Type":"application/x-www-form-urlencoded; charset=utf-8",
269				"User-Agent":"aws-sdk-go/1.14.24 (go1.11; darwin; amd64)",
270				"X-Amz-Date":"20180910T203328Z",
271				"X-Vault-Aws-Iam-Server-Id": "VaultAcceptanceTesting",
272				"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180910/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=cdef5819b2e97f1ff0f3e898fd2621aa03af00a4ec3e019122c20e5482534bf4"
273			}`,
274		},
275		{
276			Name: "JSON-incomplete",
277			Header: `{
278				"Content-Length":"43",
279				"Content-Type":"application/x-www-form-urlencoded; charset=utf-8",
280				"User-Agent":"aws-sdk-go/1.14.24 (go1.11; darwin; amd64)",
281				"X-Amz-Date":"20180910T203328Z",
282				"X-Vault-Aws-Iam-Server-Id": "VaultAcceptanceTesting",
283				"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180910/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id"
284			}`,
285			ExpectErr: parsingErr,
286		},
287		{
288			Name:   "Base64-complete",
289			Header: base64Complete(),
290		},
291		{
292			Name:      "Base64-incomplete-missing-header",
293			Header:    base64MissingVaultID(),
294			ExpectErr: missingHeaderErr,
295		},
296		{
297			Name:      "Base64-incomplete-missing-auth-sig",
298			Header:    base64MissingAuthField(),
299			ExpectErr: parsingErr,
300		},
301	}
302
303	for _, tc := range testCases {
304		t.Run(tc.Name, func(t *testing.T) {
305			if tc.Header != nil {
306				loginData["iam_request_headers"] = tc.Header
307			}
308
309			loginRequest := &logical.Request{
310				Operation: logical.UpdateOperation,
311				Path:      "login",
312				Storage:   storage,
313				Data:      loginData,
314			}
315
316			resp, err := b.HandleRequest(context.Background(), loginRequest)
317			if err != nil || resp == nil || resp.IsError() {
318				if tc.ExpectErr != nil && tc.ExpectErr.Error() == resp.Error().Error() {
319					return
320				}
321				t.Errorf("un expected failed login:\nresp: %#v\n\nerr: %v", resp, err)
322			}
323		})
324	}
325}
326
327func defaultLoginData() (map[string]interface{}, error) {
328	awsSession, err := session.NewSession()
329	if err != nil {
330		return nil, fmt.Errorf("failed to create session: %s", err)
331	}
332
333	stsService := sts.New(awsSession)
334	stsInputParams := &sts.GetCallerIdentityInput{}
335	stsRequestValid, _ := stsService.GetCallerIdentityRequest(stsInputParams)
336	stsRequestValid.HTTPRequest.Header.Add(iamServerIdHeader, testVaultHeaderValue)
337	stsRequestValid.HTTPRequest.Header.Add("Authorization", fmt.Sprintf("%s,%s,%s",
338		"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request",
339		"SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id",
340		"Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"))
341	stsRequestValid.Sign()
342
343	return buildCallerIdentityLoginData(stsRequestValid.HTTPRequest, testValidRoleName)
344}
345
346// setupIAMTestServer configures httptest server to intercept and respond to the
347// IAM login path's invocation of submitCallerIdentityRequest (which does not
348// use the AWS SDK), which receieves the mocked response responseFromUser
349// containing user information matching the role.
350func setupIAMTestServer() *httptest.Server {
351	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
352		responseString := `<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
353  <GetCallerIdentityResult>
354    <Arn>arn:aws:iam::123456789012:user/valid-role</Arn>
355    <UserId>ASOMETHINGSOMETHINGSOMETHING</UserId>
356    <Account>123456789012</Account>
357  </GetCallerIdentityResult>
358  <ResponseMetadata>
359    <RequestId>7f4fc40c-853a-11e6-8848-8d035d01eb87</RequestId>
360  </ResponseMetadata>
361</GetCallerIdentityResponse>`
362
363		auth := r.Header.Get("Authorization")
364		parts := strings.Split(auth, ",")
365		for i, s := range parts {
366			s = strings.TrimSpace(s)
367			key := strings.Split(s, "=")
368			parts[i] = key[0]
369		}
370
371		// verify the "Authorization" header contains all the expected parts
372		expectedAuthParts := []string{"AWS4-HMAC-SHA256 Credential", "SignedHeaders", "Signature"}
373		var matchingCount int
374		for _, v := range parts {
375			for _, z := range expectedAuthParts {
376				if z == v {
377					matchingCount++
378				}
379			}
380		}
381		if matchingCount != len(expectedAuthParts) {
382			responseString = "missing auth parts"
383		}
384		fmt.Fprintln(w, responseString)
385	}))
386}
387
388// base64Complete returns a base64 encoded auth header as expected
389func base64Complete() string {
390	min := `{"Authorization":["AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180907/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=97086b0531854844099fc52733fa2c88a2bfb54b2689600c6e249358a8353b52"],"Content-Length":["43"],"Content-Type":["application/x-www-form-urlencoded; charset=utf-8"],"User-Agent":["aws-sdk-go/1.14.24 (go1.11; darwin; amd64)"],"X-Amz-Date":["20180907T222145Z"],"X-Vault-Aws-Iam-Server-Id":["VaultAcceptanceTesting"]}`
391	return min
392}
393
394// base64MissingVaultID returns a base64 encoded auth header, that omits the
395// Vault ID header
396func base64MissingVaultID() string {
397	min := `{"Authorization":["AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180907/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=97086b0531854844099fc52733fa2c88a2bfb54b2689600c6e249358a8353b52"],"Content-Length":["43"],"Content-Type":["application/x-www-form-urlencoded; charset=utf-8"],"User-Agent":["aws-sdk-go/1.14.24 (go1.11; darwin; amd64)"],"X-Amz-Date":["20180907T222145Z"]}`
398	return min
399}
400
401// base64MissingAuthField returns a base64 encoded Auth header, that omits the
402// "Signature" part
403func base64MissingAuthField() string {
404	min := `{"Authorization":["AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180907/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id"],"Content-Length":["43"],"Content-Type":["application/x-www-form-urlencoded; charset=utf-8"],"User-Agent":["aws-sdk-go/1.14.24 (go1.11; darwin; amd64)"],"X-Amz-Date":["20180907T222145Z"],"X-Vault-Aws-Iam-Server-Id":["VaultAcceptanceTesting"]}`
405	return min
406}
407