diff --git a/README.md b/README.md
index 604bcef..c771e18 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,8 @@ sources in the following default order:
3. An AWS [credentials file][1]
4. ECS task credentials
5. EC2 metadata
+6. EKS Pod Identity
+7. Web Identity
Usage
-----
diff --git a/rebar.config b/rebar.config
index 522db88..d0ee324 100644
--- a/rebar.config
+++ b/rebar.config
@@ -40,4 +40,6 @@
, deprecated_functions
]}.
+{dialyzer, [{plt_extra_apps, [xmerl]}]}.
+
{plugins, [ {rebar3_lint, "3.0.1"} ]}.
diff --git a/src/aws_credentials_provider.erl b/src/aws_credentials_provider.erl
index cf6f062..91ab327 100644
--- a/src/aws_credentials_provider.erl
+++ b/src/aws_credentials_provider.erl
@@ -37,6 +37,7 @@
| aws_credentials_ecs
| aws_credentials_ec2
| aws_credentials_eks
+ | aws_credentials_web_identity
| module().
-type error_log() :: [{provider(), term()}].
-export_type([ options/0, expiration/0, provider/0, error_log/0 ]).
@@ -50,7 +51,8 @@
aws_credentials_file,
aws_credentials_ecs,
aws_credentials_ec2,
- aws_credentials_eks]).
+ aws_credentials_eks,
+ aws_credentials_web_identity]).
-spec fetch() ->
{ok, aws_credentials:credentials(), expiration()} |
diff --git a/src/aws_credentials_web_identity.erl b/src/aws_credentials_web_identity.erl
new file mode 100644
index 0000000..bc5fdde
--- /dev/null
+++ b/src/aws_credentials_web_identity.erl
@@ -0,0 +1,66 @@
+%% @doc This provider looks up credential information from web identity token
+%% Environment parameters:
+%%
+%% - <<"role_session_name">> - this is provided to the credential fetch endpoint,
+%% and will label the provided session with that name, see:
+%% https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_RequestParameters
+%% By default this is `erlang_aws_credentials'
+%%
+%% @end
+-module(aws_credentials_web_identity).
+-behaviour(aws_credentials_provider).
+
+-include_lib("xmerl/include/xmerl.hrl").
+
+-define(ASSUME_ROLE_URL,
+ "https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
+ "&RoleArn=~s&WebIdentityToken=~s&RoleSessionName=~s").
+-define(DEFAULT_SESSION_NAME, "erlang_aws_credentials").
+
+-export([fetch/1]).
+
+-spec fetch(aws_credentials_provider:options()) ->
+ {error, _}
+ | {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
+fetch(Options) ->
+ RoleArn = os:getenv("AWS_ROLE_ARN"),
+ TokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
+ AuthToken = read_token(TokenFile),
+ SessionName = maps:get(role_session_name, Options, ?DEFAULT_SESSION_NAME),
+ Response = fetch_assume_role_token(RoleArn, AuthToken, SessionName),
+ make_map(Response).
+
+-spec read_token(false | string()) -> {error, _} | {ok, binary()}.
+read_token(false) -> {error, no_credentials};
+read_token(Path) -> file:read_file(Path).
+
+-spec fetch_assume_role_token(false | string(), {error, _} | {ok, binary()}, binary()) ->
+ {error, _}
+ | {ok, aws_credentials_httpc:status_code(),
+ aws_credentials_httpc:body(),
+ aws_credentials_httpc:headers()}.
+fetch_assume_role_token(false, _AuthToken, _SessionName) -> {error, no_credentials};
+fetch_assume_role_token(_RoleArn, {error, _Error} = Error, _SessionName) -> Error;
+fetch_assume_role_token(RoleArn, {ok, AuthToken}, SessionName) ->
+ Url = lists:flatten(io_lib:format(?ASSUME_ROLE_URL, [RoleArn, AuthToken, SessionName])),
+ aws_credentials_httpc:request(get, Url).
+
+-spec make_map({error, _}
+| {ok, aws_credentials_httpc:status_code(),
+ aws_credentials_httpc:body(),
+ aws_credentials_httpc:headers()}) ->
+ {error, _}
+ | {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
+make_map({error, _Error} = Error) -> Error;
+make_map({ok, _Status, Body, _Headers}) ->
+ {Doc, []} = xmerl_scan:string(binary_to_list(Body)),
+ [#xmlText{value = AccessKeyId}] = xmerl_xpath:string("//Credentials/AccessKeyId/text()", Doc),
+ [#xmlText{value = SecretAccessKey}] =
+ xmerl_xpath:string("//Credentials/SecretAccessKey/text()", Doc),
+ [#xmlText{value = Token}] = xmerl_xpath:string("//Credentials/SessionToken/text()", Doc),
+ [#xmlText{value = Expiration}] = xmerl_xpath:string("//Credentials/Expiration/text()", Doc),
+ Creds = aws_credentials:make_map(?MODULE,
+ list_to_binary(AccessKeyId),
+ list_to_binary(SecretAccessKey),
+ list_to_binary(Token)),
+ {ok, Creds, list_to_binary(Expiration)}.
diff --git a/test/aws_credentials_providers_SUITE.erl b/test/aws_credentials_providers_SUITE.erl
index 25715c7..fbd1d04 100644
--- a/test/aws_credentials_providers_SUITE.erl
+++ b/test/aws_credentials_providers_SUITE.erl
@@ -36,6 +36,8 @@ all() ->
, {group, application_env}
, {group, ecs}
, {group, eks}
+ , {group, web_identity}
+ , {group, web_identity_default_session_name}
, {group, credential_process}
].
@@ -50,6 +52,8 @@ groups() ->
, {application_env, [], all_testcases()}
, {ecs, [], all_testcases()}
, {eks, [], all_testcases()}
+ , {web_identity, [], all_testcases()}
+ , {web_identity_default_session_name, [], all_testcases()}
, {credential_process, [], all_testcases()}
].
@@ -75,6 +79,8 @@ init_per_group(GroupName, Config) ->
application_env -> init_group(application_env, provider(env), application_env, Config);
credential_process ->
init_group(credential_process, provider(file), credential_process, Config);
+ web_identity_default_session_name = GroupName ->
+ init_group(GroupName, provider(web_identity), GroupName, Config);
GroupName -> init_group(GroupName, Config)
end.
@@ -123,6 +129,12 @@ assert_test(credential_process) ->
assert_test(eks) ->
Provider = provider(eks),
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider);
+assert_test(WebIdentity) when WebIdentity =:= web_identity;
+ WebIdentity =:= web_identity_default_session_name ->
+ Provider = provider(web_identity),
+ assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider),
+ #{token := Token} = aws_credentials:get_credentials(),
+ ?assertEqual(<<"unused">>, Token);
assert_test(GroupName) ->
Provider = provider(GroupName),
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider).
@@ -159,6 +171,8 @@ provider_opts(credential_env, _Config) ->
#{credential_path => os:getenv("HOME")};
provider_opts(credential_process, Config) ->
#{credential_path => ?config(data_dir, Config) ++ "credential_process/"};
+provider_opts(web_identity, _Config) ->
+ #{role_session_name => "overridden"};
provider_opts(_GroupName, _Config) ->
#{}.
@@ -213,6 +227,28 @@ setup_provider(eks, Config) ->
, {"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", OldTokenFile}
]
};
+setup_provider(web_identity_default_session_name, Config) ->
+ OldRoleArn = os:getenv("AWS_ROLE_ARN"),
+ OldWebIdentityTokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
+ os:putenv("AWS_ROLE_ARN", "arg:aws:iam::123123123"),
+ os:putenv("AWS_WEB_IDENTITY_TOKEN_FILE", ?config(data_dir, Config) ++ "web_identity/token"),
+ meck:new(httpc, [no_link, passthrough]),
+ meck:expect(httpc, request, fun mock_httpc_request_web_identity_default_session_name/5),
+ #{ mocks => [httpc]
+ , env => [ {"AWS_ROLE_ARN", OldRoleArn}
+ , {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
+ ]};
+setup_provider(web_identity, Config) ->
+ OldRoleArn = os:getenv("AWS_ROLE_ARN"),
+ OldWebIdentityTokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
+ os:putenv("AWS_ROLE_ARN", "arg:aws:iam::123123123"),
+ os:putenv("AWS_WEB_IDENTITY_TOKEN_FILE", ?config(data_dir, Config) ++ "web_identity/token"),
+ meck:new(httpc, [no_link, passthrough]),
+ meck:expect(httpc, request, fun mock_httpc_request_web_identity/5),
+ #{ mocks => [httpc]
+ , env => [ {"AWS_ROLE_ARN", OldRoleArn}
+ , {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
+ ]};
setup_provider(config_env, Config) ->
Old = os:getenv("AWS_CONFIG_FILE"),
os:putenv("AWS_CONFIG_FILE", ?config(data_dir, Config) ++ "env/config"),
@@ -283,6 +319,31 @@ mock_httpc_request_eks(Method, Request, HTTPOptions, Options, Profile) ->
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
end.
+mock_httpc_request_web_identity_default_session_name(
+ Method, Request, HTTPOptions, Options, Profile) ->
+ case Request of
+ {"https://sts.amazonaws.com/" ++
+ "?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
+ "&RoleArn=arg:aws:iam::123123123" ++
+ "&WebIdentityToken=dummy-web-identity-token" ++
+ "&RoleSessionName=erlang_aws_credentials", []} ->
+ {ok, response('web-identity-credentials')};
+ _ ->
+ meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
+ end.
+
+mock_httpc_request_web_identity(Method, Request, HTTPOptions, Options, Profile) ->
+ case Request of
+ {"https://sts.amazonaws.com/" ++
+ "?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
+ "&RoleArn=arg:aws:iam::123123123" ++
+ "&WebIdentityToken=dummy-web-identity-token" ++
+ "&RoleSessionName=overridden", []} ->
+ {ok, response('web-identity-credentials')};
+ _ ->
+ meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
+ end.
+
response(BodyTag) ->
StatusLine = {unused, 200, unused},
Headers = [],
@@ -296,7 +357,7 @@ body('security-credentials') ->
body('dummy-role') ->
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
- , 'Expiration' => <<"2025-09-25T23:43:56Z">>
+ , 'Expiration' => <<"2026-09-25T23:43:56Z">>
, 'Token' => unused
});
body('document') ->
@@ -304,15 +365,26 @@ body('document') ->
body('dummy-uri') ->
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
- , 'Expiration' => <<"2025-09-25T23:43:56Z">>
+ , 'Expiration' => <<"2026-09-25T23:43:56Z">>
, 'Token' => unused
});
body('eks-credentials') ->
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
- , 'Expiration' => <<"2025-09-25T23:43:56Z">>
+ , 'Expiration' => <<"2026-09-25T23:43:56Z">>
, 'Token' => unused
- }).
+ });
+body('web-identity-credentials') ->
+ <<"
+
+
+ ", ?DUMMY_ACCESS_KEY/binary, "
+ ", ?DUMMY_SECRET_ACCESS_KEY/binary, "
+ unused
+ 2026-09-25T23:43:56Z
+
+
+ ">>.
maybe_put_env(Key, false) ->
os:unsetenv(Key);
diff --git a/test/aws_credentials_providers_SUITE_data/web_identity/token b/test/aws_credentials_providers_SUITE_data/web_identity/token
new file mode 100644
index 0000000..4d83eee
--- /dev/null
+++ b/test/aws_credentials_providers_SUITE_data/web_identity/token
@@ -0,0 +1 @@
+dummy-web-identity-token
\ No newline at end of file