Skip to content

Commit

Permalink
Configurable header name (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
voltone authored Sep 9, 2022
1 parent a77a4c9 commit 2a3d82b
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 4,253 deletions.
24 changes: 12 additions & 12 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ jobs:
matrix:
include:
- pair:
elixir: 1.7.4
otp: 21.0.9
elixir: '1.10.4'
otp: '22.3.4.26'
- pair:
elixir: 1.8.2
otp: 21.3.8.21
elixir: '1.10.4'
otp: '23.0.4'
- pair:
elixir: 1.9.4
otp: 22.3.4.20
elixir: '1.11.4'
otp: '23.3.4.17'
- pair:
elixir: 1.10.4
otp: 23.0.4
elixir: '1.12.3'
otp: '24.0.6'
- pair:
elixir: 1.11.3
otp: 23.3.4.3
elixir: '1.13.4'
otp: '24.3.4.4'
- pair:
elixir: 1.12.1
otp: 24.0.2
elixir: '1.14.0'
otp: '25.0.4'
lint: lint
steps:
- uses: actions/checkout@v2
Expand Down
46 changes: 41 additions & 5 deletions lib/plug_signature.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ defmodule PlugSignature do
* `:callback_module` (mandatory) - the name of a callback module implementing
the `PlugSignature.Callback` behaviour; this module must implement the
`c:PlugSignature.Callback.client_lookup/3` callback
* `:header_name` - name of the request HTTP header from which to take the
signature; when set to "authorization" (the default) all Authorization
header values are scanned for the first entry with a scheme of "Signature";
for any other value, the contents of the first HTTP header with that name
is used
* `:algorithms` - the signature algorithms, as defined in the IETF
specification; a list containing one or more of:
Expand Down Expand Up @@ -154,10 +159,11 @@ defmodule PlugSignature do
def call(conn, opts) do
algorithms = Keyword.fetch!(opts, :algorithms)
default_algorithm = Keyword.fetch!(opts, :default_algorithm)
header_name = Keyword.fetch!(opts, :header_name)

# Parse Authorization header and select algorithm
with {:ok, authorization} <- get_authorization_header(conn),
{:ok, signature_opts} <- PlugSignature.Parser.authorization(authorization),
with {:ok, signature_string} <- get_signature(conn, header_name),
{:ok, signature_opts} <- PlugSignature.Parser.signature(signature_string),
{:ok, algorithm, algorithm_opts} <-
select_algorithm(signature_opts, default_algorithm, algorithms) do
# Ready to call main verification function
Expand All @@ -169,10 +175,40 @@ defmodule PlugSignature do
end
end

defp get_authorization_header(conn) do
defp get_signature(conn, "authorization") do
case get_req_header(conn, "authorization") do
[authorization | _] -> {:ok, authorization}
_otherwise -> {:error, "no authorization header"}
[] ->
{:error, "no authorization header"}

authorizations ->
case Enum.find_value(authorizations, &authorization_signature/1) do
nil ->
{:error, "no signature in authorization header"}

authorization ->
{:ok, String.trim(authorization)}
end
end
end

defp get_signature(conn, header_name) do
case get_req_header(conn, header_name) do
[signature_string | _] -> {:ok, String.trim(signature_string)}
_otherwise -> {:error, "no #{header_name} header"}
end
end

defp authorization_signature(authorization) do
case String.split(authorization, " ", parts: 2) do
[scheme, value] ->
if String.downcase(scheme) == "signature" do
value
else
nil
end

_ ->
nil
end
end

Expand Down
4 changes: 4 additions & 0 deletions lib/plug_signature/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule PlugSignature.Config do
# can help catch configuration issues early
@spec new(Keyword.t()) :: [
{:callback_module, Module.t()}
| {:header_name, String.t()}
| {:default_algorithm, String.t()}
| {:algorithms, map()}
| {:on_success, on_success()}
Expand All @@ -45,6 +46,8 @@ defmodule PlugSignature.Config do
Keyword.get(opts, :callback_module) ||
raise PlugSignature.ConfigError, "missing mandatory option `:callback_module`"

header_name = Keyword.get(opts, :header_name, "authorization") |> String.downcase()

algorithms = Keyword.get(opts, :algorithms, @default_algorithms)

if algorithms == [] do
Expand All @@ -61,6 +64,7 @@ defmodule PlugSignature.Config do

[
callback_module: callback_module,
header_name: header_name,
default_algorithm: hd(algorithms),
algorithms: algorithm_config,
on_success: on_success,
Expand Down
42 changes: 24 additions & 18 deletions lib/plug_signature/conn_test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,20 @@ defmodule PlugSignature.ConnTest do
parameter in the Authorization header
"""
def with_signature(conn, key, key_id, opts \\ []) do
conn = maybe_add_host_header(conn)
age = Keyword.get(opts, :age, 0)
created = Keyword.get_lazy(opts, :created, fn -> created(age) end)
date = Keyword.get_lazy(opts, :date, fn -> http_date(age) end)
expires_in = Keyword.get(opts, :expires_in, nil)
expires = Keyword.get_lazy(opts, :expires, fn -> expires(expires_in) end)
algorithms = Keyword.get(opts, :algorithms, ["hs2019"])
algorithm = hd(algorithms)
headers = Keyword.get(opts, :headers, default_headers(algorithm))
header_name = Keyword.get(opts, :header_name, "authorization")

conn =
conn
|> maybe_add_host_header()
|> put_req_header("date", date)

request_target =
Keyword.get_lazy(opts, :request_target, fn ->
Expand All @@ -85,27 +98,17 @@ defmodule PlugSignature.ConnTest do
end
end)

age = Keyword.get(opts, :age, 0)
created = Keyword.get_lazy(opts, :created, fn -> created(age) end)
date = Keyword.get_lazy(opts, :date, fn -> http_date(age) end)
expires_in = Keyword.get(opts, :expires_in, nil)
expires = Keyword.get_lazy(opts, :expires, fn -> expires(expires_in) end)
algorithms = Keyword.get(opts, :algorithms, ["hs2019"])
algorithm = hd(algorithms)
headers = Keyword.get(opts, :headers, default_headers(algorithm))

to_be_signed =
Keyword.get_lazy(opts, :to_be_signed, fn ->
headers
|> String.split(" ")
|> Enum.map(fn
|> Enum.map_join("\n", fn
"(request-target)" -> "(request-target): #{request_target}"
"(created)" -> "(created): #{created}"
"(expires)" -> "(expires): #{expires}"
"date" -> "date: #{date}"
header -> "#{header}: #{get_req_header(conn, header) |> Enum.join(",")}"
end)
|> Enum.join("\n")
end)

signature =
Expand All @@ -114,7 +117,7 @@ defmodule PlugSignature.ConnTest do
Base.encode64(signature)
end)

authorization =
signature_string =
[
~s(keyId="#{Keyword.get(opts, :key_id_override, key_id)}"),
~s(signature="#{Keyword.get(opts, :signature_override, signature)}"),
Expand All @@ -126,9 +129,13 @@ defmodule PlugSignature.ConnTest do
|> Enum.reject(&Regex.match?(~r/^[^=]+=("")?$/, &1))
|> Enum.join(",")

conn
|> put_req_header("date", date)
|> put_req_header("authorization", "Signature #{authorization}")
case header_name do
"authorization" ->
put_req_header(conn, "authorization", "Signature #{signature_string}")

_ ->
put_req_header(conn, header_name, signature_string)
end
end

defp default_headers("hs2019"), do: "(created)"
Expand All @@ -147,8 +154,7 @@ defmodule PlugSignature.ConnTest do
def with_digest(conn, digests) when is_map(digests) do
digest_header =
digests
|> Enum.map(fn {alg, value} -> "#{alg}=#{value}" end)
|> Enum.join(",")
|> Enum.map_join(",", fn {alg, value} -> "#{alg}=#{value}" end)

put_req_header(conn, "digest", digest_header)
end
Expand Down
Loading

0 comments on commit 2a3d82b

Please sign in to comment.