Skip to content

Commit fed2171

Browse files
authored
Merge pull request #93 from mechanical-orchard/aws-web-identity-parse-errors
Web identity provider better handles error API responses
2 parents 9de5bd7 + a6ccc0f commit fed2171

File tree

3 files changed

+109
-19
lines changed

3 files changed

+109
-19
lines changed

src/aws_credentials_web_identity.erl

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,28 @@ fetch_assume_role_token(RoleArn, {ok, AuthToken}, SessionName) ->
4646
aws_credentials_httpc:request(get, Url).
4747

4848
-spec make_map({error, _}
49-
| {ok, aws_credentials_httpc:status_code(),
49+
| {ok, aws_credentials_httpc:status_code(),
5050
aws_credentials_httpc:body(),
5151
aws_credentials_httpc:headers()}) ->
5252
{error, _}
5353
| {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
5454
make_map({error, _Error} = Error) -> Error;
55-
make_map({ok, _Status, Body, _Headers}) ->
55+
make_map({ok, Status, Body, Headers}) ->
56+
handle_response(Status, Body, Headers).
57+
58+
-spec handle_response(aws_credentials_httpc:status_code(),
59+
aws_credentials_httpc:body(),
60+
aws_credentials_httpc:headers()) ->
61+
{error, _}
62+
| {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
63+
handle_response(200, Body, _Headers) ->
64+
parse_credentials(Body);
65+
handle_response(Status, Body, Headers) ->
66+
{error, build_error_payload(Status, Body, Headers)}.
67+
68+
-spec parse_credentials(aws_credentials_httpc:body()) ->
69+
{ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
70+
parse_credentials(Body) ->
5671
{Doc, []} = xmerl_scan:string(binary_to_list(Body)),
5772
[#xmlText{value = AccessKeyId}] = xmerl_xpath:string("//Credentials/AccessKeyId/text()", Doc),
5873
[#xmlText{value = SecretAccessKey}] =
@@ -64,3 +79,32 @@ make_map({ok, _Status, Body, _Headers}) ->
6479
list_to_binary(SecretAccessKey),
6580
list_to_binary(Token)),
6681
{ok, Creds, list_to_binary(Expiration)}.
82+
83+
-spec build_error_payload(aws_credentials_httpc:status_code(),
84+
aws_credentials_httpc:body(),
85+
aws_credentials_httpc:headers()) -> map().
86+
build_error_payload(Status, Body, _Headers) ->
87+
try xmerl_scan:string(binary_to_list(Body)) of
88+
{Doc, []} ->
89+
#{ status => Status,
90+
code => extract_code(Doc),
91+
message => extract_error_message(Doc)
92+
}
93+
catch
94+
_ ->
95+
<<"unable to parse response as XML">>
96+
end.
97+
98+
-spec extract_code(XmlElement::any()) -> binary().
99+
extract_code(Doc) ->
100+
case xmerl_xpath:string("//*[local-name()='Code']/text()", Doc) of
101+
[#xmlText{value = C}] -> list_to_binary(C);
102+
_ -> <<"Unable to find error code in AssumeRoleWithWebIdentity response">>
103+
end.
104+
105+
-spec extract_error_message(XmlElement::any()) -> binary().
106+
extract_error_message(Doc) ->
107+
case xmerl_xpath:string("//*[local-name()='Message']/text()", Doc) of
108+
[#xmlText{value = Msg}] -> list_to_binary(Msg);
109+
_ -> <<"Unable to find error message in AssumeRoleWithWebIdentity response">>
110+
end.

test/aws_credentials_SUITE.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ init_per_group(mecked_metadata, Config) ->
1919
Role = <<"aws-metadata-user">>,
2020
AccessKeyID = <<"AccessKeyID">>,
2121
SecretAccessKey = <<"SecretAccessKey">>,
22-
Expiry = <<"2025-09-25T23:43:56Z">>,
22+
Expiry = <<"2035-09-25T23:43:56Z">>,
2323
Region = <<"ap-southeast-1">>,
2424
Token = <<"token">>,
2525
Credentials = <<"{
@@ -29,7 +29,7 @@ init_per_group(mecked_metadata, Config) ->
2929
\"AccessKeyId\" : \"AccessKeyID\",
3030
\"SecretAccessKey\" : \"SecretAccessKey\",
3131
\"Token\" : \"token\",
32-
\"Expiration\" : \"2025-09-25T23:43:56Z\"
32+
\"Expiration\" : \"2035-09-25T23:43:56Z\"
3333
}">>,
3434
Document = <<"{
3535
\"instanceType\" : \"t2.micro\",

test/aws_credentials_providers_SUITE.erl

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
-define(DUMMY_SESSION_TOKEN, "dummy-session-token").
2525
-define(DUMMY_REGION, <<"us-east-1">>).
2626
-define(DUMMY_REGION2, <<"us-east-2">>).
27+
-define(DUMMY_EXPIRATION, <<"2035-09-25T23:43:56Z">>).
2728

2829
all() ->
2930
[ {group, file}
@@ -38,6 +39,7 @@ all() ->
3839
, {group, eks}
3940
, {group, web_identity}
4041
, {group, web_identity_default_session_name}
42+
, {group, web_identity_error}
4143
, {group, credential_process}
4244
].
4345

@@ -54,6 +56,7 @@ groups() ->
5456
, {eks, [], all_testcases()}
5557
, {web_identity, [], all_testcases()}
5658
, {web_identity_default_session_name, [], all_testcases()}
59+
, {web_identity_error, [], all_testcases()}
5760
, {credential_process, [], all_testcases()}
5861
].
5962

@@ -81,6 +84,8 @@ init_per_group(GroupName, Config) ->
8184
init_group(credential_process, provider(file), credential_process, Config);
8285
web_identity_default_session_name = GroupName ->
8386
init_group(GroupName, provider(web_identity), GroupName, Config);
87+
web_identity_error ->
88+
init_group(web_identity_error, provider(web_identity), web_identity, Config);
8489
GroupName -> init_group(GroupName, Config)
8590
end.
8691

@@ -135,6 +140,14 @@ assert_test(WebIdentity) when WebIdentity =:= web_identity;
135140
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider),
136141
#{token := Token} = aws_credentials:get_credentials(),
137142
?assertEqual(<<"unused">>, Token);
143+
assert_test(web_identity_error) ->
144+
?assertEqual(undefined, aws_credentials:get_credentials()),
145+
{error, [{_, {error, ErrorInfo}}]} =
146+
aws_credentials_provider:fetch(),
147+
#{status := 400, message := Message, code := Code} = ErrorInfo,
148+
ExpectedMsg = <<"The web identity token that was passed is expired">>,
149+
?assertEqual(<<"InvalidIdentityToken">>, Code),
150+
?assertEqual(ExpectedMsg, Message);
138151
assert_test(GroupName) ->
139152
Provider = provider(GroupName),
140153
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider).
@@ -249,6 +262,19 @@ setup_provider(web_identity, Config) ->
249262
, env => [ {"AWS_ROLE_ARN", OldRoleArn}
250263
, {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
251264
]};
265+
setup_provider(web_identity_error, Config) ->
266+
OldRoleArn = os:getenv("AWS_ROLE_ARN"),
267+
OldWebIdentityTokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
268+
application:set_env(aws_credentials, fail_if_unavailable, false),
269+
os:putenv("AWS_ROLE_ARN", "arg:aws:iam::123123123"),
270+
os:putenv("AWS_WEB_IDENTITY_TOKEN_FILE", ?config(data_dir, Config) ++ "web_identity/token"),
271+
meck:new(httpc, [no_link, passthrough]),
272+
meck:expect(httpc, request, fun mock_httpc_request_web_identity_error/5),
273+
#{ mocks => [httpc]
274+
, env => [ {"AWS_ROLE_ARN", OldRoleArn}
275+
, {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
276+
]
277+
};
252278
setup_provider(config_env, Config) ->
253279
Old = os:getenv("AWS_CONFIG_FILE"),
254280
os:putenv("AWS_CONFIG_FILE", ?config(data_dir, Config) ++ "env/config"),
@@ -283,6 +309,7 @@ setup_provider(_GroupName, _Config) ->
283309
}.
284310

285311
teardown_provider(Context) ->
312+
application:unset_env(aws_credentials, fail_if_unavailable),
286313
#{mocks := Mocks, env := Env} = Context,
287314
[meck:unload(Mock) || Mock <- Mocks],
288315
[maybe_put_env(Key, Value) || {Key, Value} <- Env],
@@ -301,7 +328,7 @@ mock_httpc_request_ec2(Method, Request, HTTPOptions, Options, Profile) ->
301328
{ok, response('document')};
302329
_ ->
303330
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
304-
end.
331+
end.
305332

306333
mock_httpc_request_ecs(Method, Request, HTTPOptions, Options, Profile) ->
307334
case Request of
@@ -343,9 +370,19 @@ mock_httpc_request_web_identity(Method, Request, HTTPOptions, Options, Profile)
343370
_ ->
344371
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
345372
end.
373+
mock_httpc_request_web_identity_error(Method, {Url, Headers}, HTTPOptions, Options, Profile) ->
374+
case string:find(Url, "sts.amazonaws.com") of
375+
nomatch ->
376+
meck:passthrough([Method, {Url, Headers}, HTTPOptions, Options, Profile]);
377+
_ ->
378+
{ok, response(400, 'web-identity-error')}
379+
end.
346380

347381
response(BodyTag) ->
348-
StatusLine = {unused, 200, unused},
382+
response(200, BodyTag).
383+
384+
response(Status, BodyTag) ->
385+
StatusLine = {"HTTP/1.1", Status, ""},
349386
Headers = [],
350387
Body = body(BodyTag),
351388
{StatusLine, Headers, Body}.
@@ -357,34 +394,43 @@ body('security-credentials') ->
357394
body('dummy-role') ->
358395
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
359396
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
360-
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
397+
, 'Expiration' => ?DUMMY_EXPIRATION
361398
, 'Token' => unused
362399
});
363400
body('document') ->
364401
jsx:encode(#{ 'region' => unused });
365402
body('dummy-uri') ->
366403
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
367404
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
368-
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
405+
, 'Expiration' => ?DUMMY_EXPIRATION
369406
, 'Token' => unused
370407
});
371408
body('eks-credentials') ->
372409
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
373410
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
374-
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
411+
, 'Expiration' => ?DUMMY_EXPIRATION
375412
, 'Token' => unused
376413
});
377414
body('web-identity-credentials') ->
378-
<<"<AssumeRoleWithWebIdentityResponse>
379-
<AssumeRoleWithWebIdentityResult>
380-
<Credentials>
381-
<AccessKeyId>", ?DUMMY_ACCESS_KEY/binary, "</AccessKeyId>
382-
<SecretAccessKey>", ?DUMMY_SECRET_ACCESS_KEY/binary, "</SecretAccessKey>
383-
<SessionToken>unused</SessionToken>
384-
<Expiration>2026-09-25T23:43:56Z</Expiration>
385-
</Credentials>
386-
</AssumeRoleWithWebIdentityResult>
387-
</AssumeRoleWithWebIdentityResponse>">>.
415+
<<"<AssumeRoleWithWebIdentityResponse>\n"
416+
" <AssumeRoleWithWebIdentityResult>\n"
417+
" <Credentials>\n"
418+
" <AccessKeyId>", ?DUMMY_ACCESS_KEY/binary, "</AccessKeyId>\n"
419+
" <SecretAccessKey>", ?DUMMY_SECRET_ACCESS_KEY/binary, "</SecretAccessKey>\n"
420+
" <SessionToken>unused</SessionToken>\n"
421+
" <Expiration>", ?DUMMY_EXPIRATION/binary, "</Expiration>\n"
422+
" </Credentials>\n"
423+
" </AssumeRoleWithWebIdentityResult>\n"
424+
"</AssumeRoleWithWebIdentityResponse>">>;
425+
body('web-identity-error') ->
426+
<<"<ErrorResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\">\n"
427+
" <Error>\n"
428+
" <Type>User</Type>\n"
429+
" <Code>InvalidIdentityToken</Code>\n"
430+
" <Message>The web identity token that was passed is expired</Message>\n"
431+
" </Error>\n"
432+
" <RequestId>dummy-request-id</RequestId>\n"
433+
"</ErrorResponse>">>.
388434

389435
maybe_put_env(Key, false) ->
390436
os:unsetenv(Key);

0 commit comments

Comments
 (0)