Current section
Files
Jump to
Current section
Files
lib/oauther.ex
defmodule OAuther do
defmodule Credentials do
defstruct [
:consumer_key,
:consumer_secret,
:token,
:token_secret,
method: :hmac_sha1
]
@type t :: %__MODULE__{
consumer_key: String.t(),
consumer_secret: String.t(),
token: nil | String.t(),
token_secret: nil | String.t(),
method: :hmac_sha1 | :hmac_sha256 | :rsa_sha1 | :plaintext
}
end
@type params :: [{String.t(), String.Chars.t()}]
@type header :: {String.t(), String.t()}
@spec credentials(Enumerable.t()) :: Credentials.t() | no_return
def credentials(args) do
Enum.reduce(args, %Credentials{}, fn {key, val}, acc ->
:maps.update(key, val, acc)
end)
end
@spec sign(String.t(), URI.t() | String.t(), params, Credentials.t()) :: params
def sign(verb, url, params, %Credentials{} = creds) do
params = protocol_params(params, creds)
signature = signature(verb, url, params, creds)
[{"oauth_signature", signature} | params]
end
@spec header(params) :: {header, params}
def header(params) do
{oauth_params, req_params} = split_with(params, &protocol_param?/1)
{{"Authorization", "OAuth " <> compose_header(oauth_params)}, req_params}
end
@spec protocol_params(params, Credentials.t()) :: params
def protocol_params(params, %Credentials{} = creds) do
[
{"oauth_consumer_key", creds.consumer_key},
{"oauth_nonce", nonce()},
{"oauth_signature_method", signature_method(creds.method)},
{"oauth_timestamp", timestamp()},
{"oauth_version", "1.0"}
| maybe_put_token(params, creds.token)
]
end
@spec signature(String.t(), URI.t() | String.t(), params, Credentials.t()) :: binary
def signature(_, _, _, %Credentials{method: :plaintext} = creds) do
compose_key(creds)
end
def signature(verb, url, params, %Credentials{method: :hmac_sha1} = creds) do
creds
|> compose_key()
|> hmac_sha(base_string(verb, url, params), :sha)
|> Base.encode64()
end
def signature(verb, url, params, %Credentials{method: :hmac_sha256} = creds) do
creds
|> compose_key()
|> hmac_sha(base_string(verb, url, params), :sha256)
|> Base.encode64()
end
def signature(verb, url, params, %Credentials{method: :rsa_sha1} = creds) do
base_string(verb, url, params)
|> :public_key.sign(:sha, decode_private_key(creds.consumer_secret))
|> Base.encode64()
end
# TODO: Remove this once we require at minimum OTP 22.
if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do
defp hmac_sha(key, data, hash_function) do
:crypto.mac(:hmac, hash_function, key, data)
end
else
defp hmac_sha(key, data, hash_function) do
:crypto.hmac(hash_function, key, data)
end
end
defp protocol_param?({key, _value}) do
String.starts_with?(key, "oauth_")
end
defp compose_header([_ | _] = params) do
params
|> Stream.map(&percent_encode/1)
|> Enum.map_join(", ", &compose_header/1)
end
defp compose_header({key, value}) do
key <> "=\"" <> value <> "\""
end
defp compose_key(creds) do
[creds.consumer_secret, creds.token_secret]
|> Enum.map_join("&", &percent_encode/1)
end
defp read_private_key("-----BEGIN RSA PRIVATE KEY-----" <> _ = private_key) do
private_key
end
defp read_private_key(path) do
File.read!(path)
end
defp decode_private_key(private_key_or_path) do
[entry] =
private_key_or_path
|> read_private_key()
|> :public_key.pem_decode()
:public_key.pem_entry_decode(entry)
end
defp base_string(verb, url, params) do
{uri, query_params} = parse_url(url)
[verb, uri, params ++ query_params]
|> Stream.map(&normalize/1)
|> Enum.map_join("&", &percent_encode/1)
end
defp normalize(verb) when is_binary(verb) do
String.upcase(verb)
end
defp normalize(%URI{host: host} = uri) do
%{uri | host: String.downcase(host)}
end
defp normalize([_ | _] = params) do
Enum.map(params, &percent_encode/1)
|> Enum.sort()
|> Enum.map_join("&", &normalize_pair/1)
end
defp normalize_pair({key, value}) do
key <> "=" <> value
end
defp parse_url(url) do
uri = URI.parse(url)
{%{uri | query: nil}, parse_query_params(uri.query)}
end
defp parse_query_params(params) do
if is_nil(params) do
[]
else
URI.query_decoder(params)
|> Enum.to_list()
end
end
defp nonce() do
:crypto.strong_rand_bytes(24)
|> Base.encode64()
end
defp timestamp() do
{megasec, sec, _microsec} = :os.timestamp()
megasec * 1_000_000 + sec
end
defp maybe_put_token(params, value) do
if is_nil(value) do
params
else
[{"oauth_token", value} | params]
end
end
defp signature_method(:plaintext), do: "PLAINTEXT"
defp signature_method(:hmac_sha1), do: "HMAC-SHA1"
defp signature_method(:hmac_sha256), do: "HMAC-SHA256"
defp signature_method(:rsa_sha1), do: "RSA-SHA1"
defp percent_encode({key, value}) do
{percent_encode(key), percent_encode(value)}
end
defp percent_encode(other) do
other
|> to_string()
|> URI.encode(&URI.char_unreserved?/1)
end
# TODO: Remove once we depend on Elixir 1.4 and higher.
Code.ensure_loaded(Enum)
split_with = if function_exported?(Enum, :split_with, 2), do: :split_with, else: :partition
defp split_with(enum, fun), do: Enum.unquote(split_with)(enum, fun)
end