Current section
Files
Jump to
Current section
Files
lib/plug_hmac.ex
defmodule PlugHmac do
@moduledoc """
A Plug to validate and generate `Authorization` header.
Authorization = hmac id=URI_encode(client_id),signature=URI_encode(Signature),nonce=URI_encode(Nonce)
Signature = HMAC_SHA256(client_secret, HTTP verb + Path + Query string + Body + Nonce )
Nonce = Random string
"""
@behaviour Plug
import Plug.Conn
require Logger
@base_hamc_algo [
:md5,
:md4,
:sha,
:sha224,
:sha256,
:sha384,
:sha512,
:sha3_224,
:sha3_256,
:sha3_384,
:sha3_512
]
def init(opts) do
with {:check_error_handler, error_handler}
when is_atom(error_handler) or is_function(error_handler, 1) <-
{:check_error_handler, opts[:error_handler]},
{:check_secret_handler, secret_handler}
when is_atom(secret_handler) or is_function(secret_handler, 1) <-
{:check_secret_handler, opts[:secret_handler]},
{:check_hmac_algo, true} <- {:check_hmac_algo, opts[:hmac_algo] in @base_hamc_algo},
{:check_client_signature_name, true} <-
{:check_client_signature_name,
is_bitstring(opts[:client_signature_name]) or is_nil(opts[:client_signature_name])} do
error_handler =
if is_atom(error_handler) do
&error_handler.handle/1
else
error_handler
end
secret_handler =
if is_atom(secret_handler) do
&secret_handler.get_secret/1
else
secret_handler
end
Keyword.merge(opts, error_handler: error_handler, secret_handler: secret_handler)
else
error ->
raise "check_opts_error with #{inspect(error)} with opts: #{inspect(opts)}"
end
end
def call(conn, opts) do
error_handler = opts[:error_handler]
secret_handler = opts[:secret_handler]
hmac_algo = opts[:hmac_algo]
client_signature_name = Keyword.get(opts, :client_signature_name, "authorization")
case get_req_header(conn, client_signature_name) do
["hmac " <> credential] ->
credential = split_params_from_string(credential)
if check_sign?(hmac_algo, secret_handler, credential, conn) do
assign(conn, :client_id, credential["id"])
else
conn
|> error_handler.handle(:permission_denied)
|> halt()
end
_ ->
conn
|> error_handler.handle(:invalid_auth_header)
|> halt()
end
end
@compile {:inline, split_params_from_string: 1}
def split_params_from_string(string) do
string
|> String.split(",", trim: true)
|> Map.new(fn part ->
String.split(part, "=", trim: true)
|> Enum.map(fn v ->
String.trim(v)
|> URI.decode_www_form()
end)
|> case do
[k, v] -> {k, v}
[k] -> {k, ""}
end
end)
end
def check_sign?(hmac_algo, secret_handler, credential, conn) do
with {:get_signature, signature} when signature != nil and signature != "" <-
{:get_signature, credential["signature"]},
{:get_client_id, id} when id != nil and id != "" <- {:get_client_id, credential["id"]},
{:ok, secret} when secret != nil <- secret_handler.(id) do
sign(
hmac_algo,
secret,
[
conn.method,
conn.request_path,
conn.query_string,
conn.assigns[:raw_body],
credential["nonce"]
]
) == signature
else
error ->
Logger.info(fn -> "check_sign with error: #{inspect(error)}" end)
false
end
end
@compile {:inline, sign: 3}
def sign(hmac_algo, secret, content_to_sign) do
:crypto.hmac(hmac_algo, secret, Enum.join(content_to_sign))
|> Base.encode64()
end
def make_header(hmac_algo, get_secret_fun, method, path, query_string, body, nonce)
when is_function(get_secret_fun, 0) do
{:ok, client_id, secret} = get_secret_fun.()
make_header(hmac_algo, client_id, secret, method, path, query_string, body, nonce)
end
def make_header(
hmac_algo,
secret_handler,
client_id,
method,
path,
query_string,
body,
nonce \\ nil
)
def make_header(hmac_algo, secret_handler, client_id, method, path, query_string, body, nonce)
when is_atom(secret_handler) and is_binary(client_id) do
{:ok, secret} = secret_handler.get_secret(client_id)
make_header(hmac_algo, client_id, secret, method, path, query_string, body, nonce)
end
def make_header(hmac_algo, client_id, secret, method, path, query_string, body, nonce)
when is_binary(client_id) and is_binary(secret) do
nonce =
if is_nil(nonce) do
make_nonce()
else
nonce
end
signature =
sign(hmac_algo, secret, [method, path, query_string, body, nonce])
|> URI.encode_www_form()
client_id = URI.encode_www_form(client_id)
uriencode_nonce = URI.encode_www_form(nonce)
"hmac id=#{client_id},signature=#{signature},nonce=#{uriencode_nonce}"
end
def make_nonce() do
:crypto.strong_rand_bytes(20)
|> Base.encode64()
|> binary_part(0, 20)
end
end