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