lib/providers/assume_role_with_web_identity.ex

defmodule AwsSigner.Providers.AssumeRoleWithWebIdentity do
  alias AwsSigner.Credentials

  @client Application.get_env(:aws_signer, :aws_client, AwsSigner.Client)

  @spec get_credentials(
          arn: String.t(),
          region: String.t(),
          session_name: String.t(),
          web_identity_token_file: String.t()
        ) :: %Credentials{}

  def get_credentials(opts) do
    arn = Keyword.fetch!(opts, :arn)
    region = Keyword.fetch!(opts, :region)
    session_name = Keyword.get(opts, :session_name, "default")
    token = get_token(opts)
    url = "https://sts.#{region}.amazonaws.com?#{query(arn, session_name,  token)}"

    %{status: 200, body: body} = @client.get!(url)

    %Credentials{
      access_key_id: extract(body, "AccessKeyId"),
      expiration: extract(body, "Expiration"),
      secret_access_key: extract(body, "SecretAccessKey"),
      token: extract(body, "SessionToken")
    }
  end

  #
  # private
  #

  # No need for cache here
  # The web identity token file is read very rarely
  # (only when the session token expires, eg. hourly)
  defp get_token(opts) do
    Keyword.fetch!(opts, :web_identity_token_file)
    |> File.read!()
    |> String.trim()
  end

  defp encode(str),
    do: URI.encode_www_form(str)

  defp query(arn, session_name, token) do
    "Action=AssumeRoleWithWebIdentity" <>
      "&RoleArn=#{encode(arn)}" <>
      "&RoleSessionName=#{encode(session_name)}" <>
      "&Version=2011-06-15" <>
      "&WebIdentityToken=#{encode(token)}"
  end

  defp extract(xml, key) do
    ekey = Regex.escape(key)

    ~r{<#{ekey}>(.*)(?=</#{ekey}>)}s
    |> Regex.run(xml, capture: [1])
    |> hd()
  end
end