Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internalize authentication code for easier adaptation. #41

Merged
merged 4 commits into from
Jun 26, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions lib/ex_aws/auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
defmodule ExAws.Auth do
import ExAws.Auth.Utils
alias Timex.DateFormat

def headers(http_method, url, service, config, headers, body) do
now = %{Timex.Date.now | ms: 0}
headers = [
{"host", URI.parse(url).host},
{"x-amz-date", amz_date(now)} |
headers
]

auth_header = auth_header(
config[:access_key_id],
config[:secret_access_key],
http_method |> method_string,
url,
config[:region],
service |> service_name,
headers,
body,
now)

[{"Authorization", auth_header} | headers ]
end

def auth_header(access_key, secret_key, http_method, url, region, service, headers, body, now) do
date = DateFormat.format!(now, "%Y%m%d", :strftime)
scope = "#{date}/#{region}/#{service}/aws4_request"

signing_key = build_signing_key(secret_key, date, region, service)

signature = http_method
|> build_canonical_request(url, headers, body)
|> build_string_to_sign(now, scope)
|> sign_with(signing_key)

[
"AWS4-HMAC-SHA256 Credential=", access_key, "/", scope, ",",
"SignedHeaders=", signed_headers(headers), ",",
"Signature=", signature
] |> IO.iodata_to_binary
end

def build_signing_key(secret_key, date, region, service) do
["AWS4", secret_key]
|> hmac_sha256(date)
|> hmac_sha256(region)
|> hmac_sha256(service)
|> hmac_sha256("aws4_request")
end

def build_string_to_sign(canonical_request, now, scope) do
timestamp = now |> ExAws.Auth.Utils.amz_date
hashed_canonical_request = ExAws.Auth.Utils.hash_sha256(canonical_request)

[
"AWS4-HMAC-SHA256", "\n",
timestamp, "\n",
scope, "\n",
hashed_canonical_request
] |> IO.iodata_to_binary
end

def sign_with(string_to_sign, signing_key) do
signing_key
|> hmac_sha256(string_to_sign)
|> bytes_to_hex
end

def signed_headers(headers) do
headers
|> Enum.map(fn({k, _}) -> String.downcase(k) end)
|> Enum.sort(&(&1 < &2))
|> Enum.join(";")
end

def build_canonical_request(http_method, url, headers, body) do
uri = URI.parse(url)
http_method = http_method |> String.upcase

query_params = uri.query |> canonical_query_params

headers = headers |> canonical_headers
header_string = headers
|> Enum.map(fn {k, v} -> "#{k}:#{v}" end)
|> Enum.join("\n")

signed_headers_list = headers
|> Keyword.keys
|> Enum.join(";")

[
http_method, "\n",
uri.path, "\n",
query_params, "\n",
header_string, "\n",
"\n",
signed_headers_list, "\n",
ExAws.Auth.Utils.hash_sha256(body)
] |> IO.iodata_to_binary
end

def canonical_query_params(nil), do: ""
def canonical_query_params(params) do
params
|> URI.query_decoder
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end)
|> URI.encode_query
end

def canonical_headers(headers) do
headers
|> Enum.map(fn {k, v} -> {String.downcase(k), String.strip(v)} end)
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end)
end
end
32 changes: 32 additions & 0 deletions lib/ex_aws/auth/utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule ExAws.Auth.Utils do
def amz_date(now) do
now
|> Timex.DateFormat.format!("{ISOz}")
|> String.replace("-", "")
|> String.replace(":", "")
end

def hash_sha256(data) do
:sha256
|> :crypto.hash(data)
|> bytes_to_hex
end

def hmac_sha256(key, data) do
:crypto.hmac(:sha256, key, data)
end

def bytes_to_hex(bytes) do
bytes
|> Base.encode16
|> String.downcase
end

def service_name(service), do: service |> Atom.to_string

def method_string(method) do
method
|> Atom.to_string
|> String.upcase
end
end
40 changes: 1 addition & 39 deletions lib/ex_aws/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,10 @@ defmodule ExAws.Request do
_ -> config[:json_codec].encode!(data)
end

headers = headers(http_method, url, service, config, headers, body)
headers = ExAws.Auth.headers(http_method, url, service, config, headers, body)
request_and_retry(http_method, url, service, config, headers, body, {:attempt, 1})
end

def headers(http_method, url, service, config, headers, body) do
now = %{Timex.Date.now | ms: 0}
amz_date = Timex.DateFormat.format!(now, "{ISOz}")
|> String.replace("-", "")
|> String.replace(":", "")

headers = [
{"host", URI.parse(url).host},
{"x-amz-date", amz_date} |
headers
]

auth_header = AWSAuth.sign_authorization_header(
config[:access_key_id],
config[:secret_access_key],
http_method |> method_string,
url,
config[:region],
service |> service_name,
headers |> Enum.into(%{}),
body,
now)

[{"Authorization", auth_header} | headers ]
end

def service_name(service), do: service |> Atom.to_string

def binary_headers(headers) do
headers |> Enum.map(fn({k, v}) -> {List.to_string(k), List.to_string(v)} end)
end

@doc false
def request_and_retry(_method, _url, _service, _config, _headers, _req_body, {:error, reason}), do: {:error, reason}

Expand Down Expand Up @@ -122,10 +90,4 @@ defmodule ExAws.Request do
def backoff(attempt) do
:timer.sleep(attempt * 1000)
end

defp method_string(method) do
method
|> Atom.to_string
|> String.upcase
end
end
2 changes: 1 addition & 1 deletion lib/ex_aws/s3/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule ExAws.S3.Request do
|> url(bucket, path)
|> add_query(resource, query)

hashed_payload = AWSAuth.Utils.hash_sha256(body)
hashed_payload = ExAws.Auth.Utils.hash_sha256(body)

headers = [
{"x-amz-content-sha256", hashed_payload} |
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ defmodule ExAws.Mixfile do
end

def application do
[applications: [:logger],
[applications: [:logger, :timex, :crypto],
mod: {ExAws, []}]
end

defp deps do
[
{:aws_auth, "~> 0.2.3"} |
{:timex, "~> 0.13.4"} |
deps(:test_dev)
]
end
Expand Down
9 changes: 4 additions & 5 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
%{"aws_auth": {:hex, :aws_auth, "0.2.3"},
"earmark": {:hex, :earmark, "0.1.15"},
"ex_doc": {:hex, :ex_doc, "0.7.2"},
%{"earmark": {:hex, :earmark, "0.1.17"},
"ex_doc": {:hex, :ex_doc, "0.7.3"},
"hackney": {:hex, :hackney, "1.1.0"},
"httpoison": {:hex, :httpoison, "0.6.2"},
"httpotion": {:hex, :httpotion, "2.0.0"},
"ibrowse": {:git, "git://github.com/cmullaparthi/ibrowse.git", "d2e369ff42666c3574b8b7ec26f69027895c4d94", [tag: "v4.1.1"]},
"idna": {:hex, :idna, "1.0.2"},
"jsx": {:hex, :jsx, "2.5.3"},
"mixunit": {:hex, :mixunit, "0.9.1"},
"mixunit": {:hex, :mixunit, "0.9.2"},
"poison": {:hex, :poison, "1.2.1"},
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.4"},
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"},
"sweet_xml": {:hex, :sweet_xml, "0.2.1"},
"timex": {:hex, :timex, "0.13.4"}}