Skip to content

Commit

Permalink
Chore/dev updates (#46)
Browse files Browse the repository at this point in the history
* πŸ”–β¬†οΈ  Bump elixir version to 1.14.0 and package version to 0.4.0

* πŸ”§ Update credo config

* βž• Add mix_audit to check for security vulnerabilities

* βž•βž– Remove secure_compare in favor of plug_crypto

* πŸ”§ Switch from CircleCI to Github Actions

* πŸ“ Update README to include github actions status and gitmoji badge

* 🚨 Automatic formatting fixes
  • Loading branch information
mollyretter authored Oct 23, 2023
1 parent 8f6c72d commit c0b7185
Show file tree
Hide file tree
Showing 24 changed files with 465 additions and 303 deletions.
12 changes: 0 additions & 12 deletions .circleci/config.yml

This file was deleted.

11 changes: 1 addition & 10 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
{Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.IExPry},
{Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.LazyLogging},
{Credo.Check.Warning.LazyLogging, false},
{Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.OperationWithConstantResult},
{Credo.Check.Warning.UnusedEnumOperation},
Expand All @@ -133,15 +133,6 @@
{Credo.Check.Refactor.VariableRebinding, false},
{Credo.Check.Warning.MapGetUnsafePass, false},
{Credo.Check.Consistency.MultiAliasImportRequireUse, false},

# Deprecated checks (these will be deleted after a grace period)
#
{Credo.Check.Readability.Specs, false},
{Credo.Check.Warning.NameRedeclarationByAssignment, false},
{Credo.Check.Warning.NameRedeclarationByCase, false},
{Credo.Check.Warning.NameRedeclarationByDef, false},
{Credo.Check.Warning.NameRedeclarationByFn, false},

# Custom checks can be created using `mix credo.gen.check`.
#
]
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Elixir CI
on:
push:
branches: [ "**" ]
permissions:
contents: read
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: 1.14.0
otp-version: 24
- name: Restore dependencies cache
uses: actions/cache@v3
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install Mix Dependencies
run: mix deps.get
- name: Check package compiles without warnings
run: mix compile --warnings-as-errors
- name: Check Formatting
run: mix format "lib/**/*.{ex,exs}" "test/**/*.{ex,exs}" --check-formatted
- name: Run credo linting
run: mix credo
- name: Check for security vulnerabilities in deps
run: mix deps.audit
- name: Run Test Suite
run: mix test
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# ApiAuth
[![Elixir CI](https://github.com/TheGnarCo/api_auth_ex/actions/workflows/elixir.yml/badge.svg?branch=master)](https://github.com/TheGnarCo/api_auth_ex/actions/workflows/elixir.yml)
<a href="https://gitmoji.dev">
<img src="https://img.shields.io/badge/gitmoji-%20😜%20😍-FFDD67.svg?style=flat-square"
alt="Gitmoji">
</a>

HMAC API authentication.

Expand All @@ -12,7 +17,7 @@ by adding `api_auth` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:api_auth, "~> 0.2.0"}
{:api_auth, "~> 0.4.0"}
]
end
```
Expand Down
22 changes: 16 additions & 6 deletions lib/api_auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ defmodule ApiAuth do
|> DateHeader.headers()
|> UriHeader.headers(uri)
|> ContentHashHeader.headers(parsed.method, parsed.content, parsed.content_algorithm)
|> AuthorizationHeader.override(parsed.method, client_id, secret_key, parsed.signature_algorithm)
|> AuthorizationHeader.override(
parsed.method,
client_id,
secret_key,
parsed.signature_algorithm
)
|> HeaderValues.unwrap()
end

Expand Down Expand Up @@ -105,16 +110,21 @@ defmodule ApiAuth do
|> DateHeader.headers()
|> UriHeader.override(uri)
|> ContentHashHeader.override(parsed.method, parsed.content, parsed.content_algorithm)
|> AuthorizationHeader.override(parsed.method, client_id, secret_key, parsed.signature_algorithm)
|> AuthorizationHeader.override(
parsed.method,
client_id,
secret_key,
parsed.signature_algorithm
)
|> HeaderValues.unwrap()
end

defp parse(opts) do
%{
method: opts |> Keyword.get(:method, "GET") |> String.upcase(),
content: opts |> Keyword.get(:content, ""),
content_algorithm: opts |> Keyword.get(:content_algorithm, :sha256),
signature_algorithm: opts |> Keyword.get(:signature_algorithm, :sha256),
method: opts |> Keyword.get(:method, "GET") |> String.upcase(),
content: opts |> Keyword.get(:content, ""),
content_algorithm: opts |> Keyword.get(:content_algorithm, :sha256),
signature_algorithm: opts |> Keyword.get(:signature_algorithm, :sha256)
}
end
end
35 changes: 18 additions & 17 deletions lib/api_auth/authorization_header.ex
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
defmodule ApiAuth.AuthorizationHeader do
@moduledoc false

@keys [:Authorization, :AUTHORIZATION]
@keys [:Authorization, :AUTHORIZATION]
@header_key :Authorization
@value_key :authorization
@pattern ~r{\AAPIAuth(?:-HMAC-(?:MD5|SHA(?:1|224|256|384|512)?))? (?<client_id>[^:]+):(?<signature>.+)\z}
@value_key :authorization
@pattern ~r{\AAPIAuth(?:-HMAC-(?:MD5|SHA(?:1|224|256|384|512)?))? (?<client_id>[^:]+):(?<signature>.+)\z}

alias ApiAuth.HeaderValues
alias ApiAuth.HeaderCompare
alias ApiAuth.Utils

def override(hv, method, client_id, secret_key, algorithm) do
canonical = canonical_string(
method,
hv |> HeaderValues.get(:content_type),
hv |> HeaderValues.get(:content_hash),
hv |> HeaderValues.get(:uri, "/"),
hv |> HeaderValues.get(:timestamp)
)

authorization = canonical
|> signature(secret_key, algorithm)
|> header_string(client_id, algorithm)
canonical =
canonical_string(
method,
hv |> HeaderValues.get(:content_type),
hv |> HeaderValues.get(:content_hash),
hv |> HeaderValues.get(:uri, "/"),
hv |> HeaderValues.get(:timestamp)
)

authorization =
canonical
|> signature(secret_key, algorithm)
|> header_string(client_id, algorithm)

hv |> HeaderValues.put(@keys, @header_key, @value_key, authorization)
end

def extract_client_id(headers) do
with {:ok, header} <- Utils.find(headers, @keys),
%{"client_id" => client_id} <- Regex.named_captures(@pattern, header)
do
with {:ok, header} <- Utils.find(headers, @keys),
%{"client_id" => client_id} <- Regex.named_captures(@pattern, header) do
{:ok, client_id}
else
_ -> :error
Expand Down
16 changes: 11 additions & 5 deletions lib/api_auth/content_hash_header.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
defmodule ApiAuth.ContentHashHeader do
@moduledoc false

@methods ["PUT", "POST"]
@keys [:"X-APIAuth-Content-Hash", :"X-APIAUTH-CONTENT-HASH", :X_APIAUTH_CONTENT_HASH,
:"Content-MD5", :"CONTENT-MD5", :CONTENT_MD5]
@header_key :"X-APIAuth-Content-Hash"
@methods ["PUT", "POST"]
@keys [
:"X-APIAuth-Content-Hash",
:"X-APIAUTH-CONTENT-HASH",
:X_APIAUTH_CONTENT_HASH,
:"Content-MD5",
:"CONTENT-MD5",
:CONTENT_MD5
]
@header_key :"X-APIAuth-Content-Hash"
@md5_header_key :"Content-MD5"
@value_key :content_hash
@value_key :content_hash

alias ApiAuth.HeaderValues
alias ApiAuth.HeaderCompare
Expand Down
2 changes: 1 addition & 1 deletion lib/api_auth/content_type_header.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule ApiAuth.ContentTypeHeader do
@moduledoc false

@keys [:"Content-Type", :"CONTENT-TYPE", :"CONTENT_TYPE", :"HTTP_CONTENT_TYPE"]
@keys [:"Content-Type", :"CONTENT-TYPE", :CONTENT_TYPE, :HTTP_CONTENT_TYPE]
@value_key :content_type

alias ApiAuth.HeaderValues
Expand Down
12 changes: 6 additions & 6 deletions lib/api_auth/date_header.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule ApiAuth.DateHeader do
@moduledoc false

@keys [:DATE, :HTTP_DATE]
@header_key :DATE
@value_key :timestamp
@keys [:DATE, :HTTP_DATE]
@header_key :DATE
@value_key :timestamp
@allowed_skew 900

alias ApiAuth.HeaderValues
Expand All @@ -23,7 +23,7 @@ defmodule ApiAuth.DateHeader do

defp timestamp do
DateTime.now_utc()
|> Format.httpdate
|> Format.httpdate()
end

defp timestamp_compare(t1, t2) do
Expand All @@ -34,9 +34,9 @@ defmodule ApiAuth.DateHeader do
now = DateTime.now_utc()

case DateTime.diff(now, time) do
{:ok, _, _, :same_time} -> true
{:ok, _, _, :same_time} -> true
{:ok, seconds, _, :after} -> seconds < @allowed_skew
_ -> false
_ -> false
end
end

Expand Down
8 changes: 4 additions & 4 deletions lib/api_auth/header_compare.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ defmodule ApiAuth.HeaderCompare do
@moduledoc false

alias ApiAuth.Utils
alias Plug.Crypto

def wrap(valid, request) do
{:ok, valid, request}
end

def compare(hc, keys) do
compare(hc, keys, &SecureCompare.compare/2)
compare(hc, keys, &Crypto.secure_compare/2)
end

def compare({:ok, valid, request} = hc, keys, fun) do
with {:ok, valid_value} <- Utils.find(valid, keys),
with {:ok, valid_value} <- Utils.find(valid, keys),
{:ok, request_value} <- Utils.find(request, keys),
true <- fun.(valid_value, request_value)
do
true <- fun.(valid_value, request_value) do
hc
else
_ -> :error
Expand Down
33 changes: 18 additions & 15 deletions lib/api_auth/header_values.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ defmodule ApiAuth.HeaderValues do
def copy({headers, assigns}, keys, value_key, default \\ "") do
header = Utils.find(headers, keys)

new_assigns = case header do
{:ok, v} -> assigns |> Map.put(value_key, v)
_ -> assigns |> Map.put(value_key, default)
end
new_assigns =
case header do
{:ok, v} -> assigns |> Map.put(value_key, v)
_ -> assigns |> Map.put(value_key, default)
end

{headers, new_assigns}
end
Expand All @@ -34,23 +35,25 @@ defmodule ApiAuth.HeaderValues do
clean_headers = Utils.reject(headers, keys)

{
clean_headers |> Keyword.put(header_key, default),
assigns |> Map.put(value_key, default)
clean_headers |> Keyword.put(header_key, default),
assigns |> Map.put(value_key, default)
}
end

def put_new({headers, assigns}, keys, header_key, value_key, default) do
header = Utils.find(headers, keys)

new_headers = case header do
{:ok, _v} -> headers
_ -> headers |> Keyword.put(header_key, default)
end

new_assigns = case header do
{:ok, v} -> assigns |> Map.put(value_key, v)
_ -> assigns |> Map.put(value_key, default)
end
new_headers =
case header do
{:ok, _v} -> headers
_ -> headers |> Keyword.put(header_key, default)
end

new_assigns =
case header do
{:ok, v} -> assigns |> Map.put(value_key, v)
_ -> assigns |> Map.put(value_key, default)
end

{new_headers, new_assigns}
end
Expand Down
8 changes: 4 additions & 4 deletions lib/api_auth/uri_header.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule ApiAuth.UriHeader do
@moduledoc false

@keys [:"X-Original-URI", :"X-ORIGINAL-URI", :"X_ORIGINAL_URI", :"HTTP_X_ORIGINAL_URI"]
@keys [:"X-Original-URI", :"X-ORIGINAL-URI", :X_ORIGINAL_URI, :HTTP_X_ORIGINAL_URI]
@header_key :"X-Original-URI"
@value_key :uri
@value_key :uri

alias ApiAuth.HeaderValues

Expand All @@ -24,8 +24,8 @@ defmodule ApiAuth.UriHeader do

case query do
nil -> value_for(path)
"" -> value_for(path)
_ -> "#{path}?#{query}"
"" -> value_for(path)
_ -> "#{path}?#{query}"
end
end

Expand Down
9 changes: 5 additions & 4 deletions lib/api_auth/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule ApiAuth.Utils do

case pair do
{_k, v} -> {:ok, v}
_ -> :error
_ -> :error
end
end

Expand All @@ -23,9 +23,10 @@ defmodule ApiAuth.Utils do
end

defp convert_key({key, value}) when is_bitstring(key) do
new_key = key
|> String.upcase()
|> String.to_atom()
new_key =
key
|> String.upcase()
|> String.to_atom()

{new_key, value}
end
Expand Down
Loading

0 comments on commit c0b7185

Please sign in to comment.