From 239f4a236a4907700a1b79aaeff511ad48688c61 Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Thu, 8 Aug 2019 15:54:25 +0200 Subject: [PATCH] Remove duplicate JWT related code in favor of `RIG.JWT` --- CHANGELOG.md | 2 + apps/rig/lib/rig/jwt.ex | 19 +++++ .../utils_test.exs => rig/test/jwt_test.exs} | 55 +++++------- apps/rig/test/rig/subscriptions_test.exs | 12 +-- .../controllers/channels_controller.ex | 5 +- .../rig_auth/authorization_check/header.ex | 5 +- apps/rig_auth/lib/rig_auth/jwt/utils.ex | 83 ------------------- apps/rig_auth/test/support/conn_case.ex | 20 ----- .../rig_inbound_gateway/api_proxy/auth/jwt.ex | 60 ++++++++------ .../test/rig/api_proxy/proxy_metrics_test.exs | 3 +- .../test/rig/api_proxy/router_test.exs | 5 +- .../test/support/conn_case.ex | 14 ---- mix.exs | 2 +- mix.lock | 3 +- 14 files changed, 91 insertions(+), 197 deletions(-) rename apps/{rig_auth/test/jwt/utils_test.exs => rig/test/jwt_test.exs} (66%) delete mode 100644 apps/rig_auth/lib/rig_auth/jwt/utils.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index c045adb4..2d6dd85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Automated UI-tests using Cypress make sure that all examples work and that code changes do not introduce any unintended API changes. [#227](https://github.com/Accenture/reactive-interaction-gateway/issues/227) +- Refactor JWT related code in favor of `RIG.JWT`. + [#244](https://github.com/Accenture/reactive-interaction-gateway/pull/244) ## [2.2.1] - 2019-06-21 diff --git a/apps/rig/lib/rig/jwt.ex b/apps/rig/lib/rig/jwt.ex index c5889958..2995fdd3 100644 --- a/apps/rig/lib/rig/jwt.ex +++ b/apps/rig/lib/rig/jwt.ex @@ -82,6 +82,25 @@ defmodule RIG.JWT do # --- + @doc "Checks an encoded JWT for validity." + @callback valid?(token, jwt_conf, ensure_not_blacklisted) :: boolean() + def valid?( + token, + jwt_conf \\ config().jwt_conf, + ensure_not_blacklisted \\ &ensure_not_blacklisted/1 + ) + + def valid?(token, jwt_conf, ensure_not_blacklisted) do + token + |> parse_token(jwt_conf, ensure_not_blacklisted) + |> case do + {:ok, _} -> true + _ -> false + end + end + + # --- + defp ensure_not_blacklisted(%{"jti" => jti} = claims) do if Blacklist.contains_jti?(Blacklist, jti) do {:error, "Ignoring blacklisted JWT with ID #{jti}."} diff --git a/apps/rig_auth/test/jwt/utils_test.exs b/apps/rig/test/jwt_test.exs similarity index 66% rename from apps/rig_auth/test/jwt/utils_test.exs rename to apps/rig/test/jwt_test.exs index fc38df08..ad663a91 100644 --- a/apps/rig_auth/test/jwt/utils_test.exs +++ b/apps/rig/test/jwt_test.exs @@ -1,63 +1,46 @@ -defmodule RigAuth.Jwt.UtilsTest do +defmodule RIG.JWTTest do @moduledoc false - # Cannot be async because JWT related environment variables are modified: - use ExUnit.Case, async: false + use ExUnit.Case, async: true use RigAuth.ConnCase - alias RigAuth.Jwt.Utils - - setup do - env_jwt_key = Confex.fetch_env!(:rig, RigAuth.Jwt.Utils)[:secret_key] - env_jwt_alg = Confex.fetch_env!(:rig, RigAuth.Jwt.Utils)[:alg] - - on_exit(fn -> - System.put_env("JWT_SECRET_KEY", env_jwt_key) - System.put_env("JWT_ALG", env_jwt_alg) - end) - - System.put_env("JWT_SECRET_KEY", "mysecret") - System.put_env("JWT_ALG", "HS256") - end + alias RIG.JWT describe "decode/1" do test "should return decoded jwt payload with valid jwt" do - jwt = generate_jwt() - assert {:ok, _decoded_payload} = Utils.decode(jwt) + conf = %{alg: "HS256", key: "mysecret"} + jwt = JWT.encode(%{"foo" => "bar"}, conf) + assert {:ok, %{"foo" => "bar"}} = JWT.parse_token(jwt, conf) end test "should return error with invalid jwt" do - jwt = "badtoken" - assert {:error, "Invalid signature"} = Utils.decode(jwt) + illegal_jwt = "badtoken" + assert {:error, %JWT.DecodeError{}} = JWT.parse_token(illegal_jwt) end end describe "valid?/1" do test "should return true with valid jwt using RS256" do - System.put_env( - "JWT_SECRET_KEY", + pubkey = "-----BEGIN CERTIFICATE-----\nMIICVzCCAcACCQC6Bxn5zZYgBzANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJx\ndzEMMAoGA1UECAwDcXdlMQwwCgYDVQQHDANxd2UxDDAKBgNVBAoMA3F3ZTEMMAoG\nA1UECwwDcXdlMQwwCgYDVQQDDANxd2UxGzAZBgkqhkiG9w0BCQEWDHF3ZUBtYWls\nLmNvbTAeFw0xODA3MjMxMjA1MTFaFw0yMzA3MjIxMjA1MTFaMHAxCzAJBgNVBAYT\nAnF3MQwwCgYDVQQIDANxd2UxDDAKBgNVBAcMA3F3ZTEMMAoGA1UECgwDcXdlMQww\nCgYDVQQLDANxd2UxDDAKBgNVBAMMA3F3ZTEbMBkGCSqGSIb3DQEJARYMcXdlQG1h\naWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQChEGtKvkROJocIBNxT\n0inEGEdQPxqvbFol2cMNiQikg7DCx91qc2FQ2hfZSuJiVcQSfVfJvyVO66pM7+bZ\nV/845LhDaUUhzp18pv0PGrIIxNMVD+A25vwq9ay6qlOZz1LNW1OSWpUlqK0Cw/LD\nlO6qFQLVNfudC0hRpVKLYC3hvQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAJNtoxWp\n0gC1437hF09a8MkCxh2JrtZTJLUozYedwtmkBFhHi1vvpMgFRkPPaGnSI14H4nyJ\nSBjUBRWLkf0+NKMP8OR8VW2qQ2F/o0fkcW+3CHBt+8b3basDxVb2ooFq599P1qB4\n3R0T687G1c8pQ98CBN5gaBvtNldenM0QxXhn\n-----END CERTIFICATE-----" - ) - System.put_env("JWT_ALG", "RS256") - - jwt = + privkey = "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQChEGtKvkROJocIBNxT0inEGEdQPxqvbFol2cMNiQikg7DCx91q\nc2FQ2hfZSuJiVcQSfVfJvyVO66pM7+bZV/845LhDaUUhzp18pv0PGrIIxNMVD+A2\n5vwq9ay6qlOZz1LNW1OSWpUlqK0Cw/LDlO6qFQLVNfudC0hRpVKLYC3hvQIDAQAB\nAoGAKAWpc4g19ulx8lcq3JVDlZum1NTpb5/QAsnKwylDAYZLvQrnBRWon+uhs3f9\nKww+zY1h7BrYTXUX+0g9p9JK89Ysto2HncjEC9vMm8Gb0feFcBDOJlYrom1SA47N\n+MqJeg1LiDfHIVBXs4W7h5u1kFZhN6MNtYyzOsKkilL7PmECQQDV3Tpi/i3kyMSw\nGCYdsDUHE/sMGIW9VaetgYdvHDd+tPO3mwMe0lrItUyNtzDrJR4K/MmI6sWP4raN\nd4mfnf0lAkEAwMwW/urkQBLNgNRpPojiWvy2+ZsXmtW15qaWlaEffzjfFQ2IoEzs\nLZe6Rsj4BZ7p53JXILJS3JzevQFFEpyKuQJAVwFHjZpmxVrAWfuZFh7nk9eXHJal\nYh+EtduqY5ORKCUpuZqArHtbn6fSWx0Z87AIBuRMgT0x3pWXOvpUrPEzWQJAccuE\nfy3xTwhKF5JIFEsDH6Ut8qHiCte9J8iH9QVG6/aLZYe5brQ4aqi1n/YavmaPtLY+\nSuQ2GFTW+0P2mweesQJBAKic+GSd/eNTBnJUJsTJMuZjGaHyfvqd0LWVGnQx7xPM\n4I6jG6djGZI1+ImIxIkd+KPuTeEYjFzTEN+rzcoUtLw=\n-----END RSA PRIVATE KEY-----" - |> generate_jwt - assert Utils.valid?("Bearer " <> jwt) + # Encode with the private key: + jwt = JWT.encode(%{}, %{alg: "RS256", key: privkey}) + + # Verify with the public key: + assert JWT.valid?(jwt, %{alg: "RS256", key: pubkey}) end test "should return true with valid jwt using HS256" do - jwt = generate_jwt() - assert Utils.valid?("Bearer " <> jwt) + conf = %{alg: "HS256", key: "mysecret"} + assert %{} |> JWT.encode(conf) |> JWT.valid?(conf) end test "should return false with invalid jwt" do - jwt = "badtoken" - - assert Utils.valid?(jwt) == %{ - error: "JWT=badtoken is missing token type. Required format is: \"Bearer token\"" - } + conf = %{alg: "HS256", key: "mysecret"} + refute "badtoken" |> JWT.valid?(conf) end end end diff --git a/apps/rig/test/rig/subscriptions_test.exs b/apps/rig/test/rig/subscriptions_test.exs index b99876d9..f3ea38d6 100644 --- a/apps/rig/test/rig/subscriptions_test.exs +++ b/apps/rig/test/rig/subscriptions_test.exs @@ -2,8 +2,6 @@ defmodule RIG.SubscriptionsTest do @moduledoc false use ExUnit.Case, async: false - import Joken - alias RIG.JWT alias RIG.Subscriptions alias RigInboundGateway.ExtractorConfig @@ -79,13 +77,5 @@ defmodule RIG.SubscriptionsTest do Subscriptions.from_token("Bearer #{jwt}", @jwt_conf) end - defp generate_jwt do - token() - |> with_exp - |> with_signer(@jwt_secret_key |> hs256) - |> with_claim("username", "john") - |> with_claim("fullname", "John Doe") - |> sign - |> get_compact - end + defp generate_jwt, do: JWT.encode(%{"username" => "john", "fullname" => "John Doe"}, @jwt_conf) end diff --git a/apps/rig_api/lib/rig_api/controllers/channels_controller.ex b/apps/rig_api/lib/rig_api/controllers/channels_controller.ex index c45cb957..014efa2a 100644 --- a/apps/rig_api/lib/rig_api/controllers/channels_controller.ex +++ b/apps/rig_api/lib/rig_api/controllers/channels_controller.ex @@ -5,8 +5,9 @@ defmodule RigApi.ChannelsController do """ use RigApi, :controller require Logger + + alias RIG.JWT alias RigAuth.Blacklist - alias RigAuth.Jwt alias RigInboundGatewayWeb.Endpoint def list_channels(conn, _params) do @@ -44,7 +45,7 @@ defmodule RigApi.ChannelsController do defp jwt_expiry_from_tokens([]), do: nil defp jwt_expiry_from_tokens([token]) do - {:ok, %{"exp" => expiry}} = Jwt.Utils.decode(token) + {:ok, %{"exp" => expiry}} = JWT.parse_token(token) expiry # Comes as int, convert to string diff --git a/apps/rig_auth/lib/rig_auth/authorization_check/header.ex b/apps/rig_auth/lib/rig_auth/authorization_check/header.ex index 31b2710e..a80e224a 100644 --- a/apps/rig_auth/lib/rig_auth/authorization_check/header.ex +++ b/apps/rig_auth/lib/rig_auth/authorization_check/header.ex @@ -2,8 +2,9 @@ defmodule RigAuth.AuthorizationCheck.Header do @moduledoc false alias Plug + + alias RIG.JWT alias RigAuth.AuthorizationCheck.Request - alias RigAuth.Jwt.Utils, as: Jwt # --- @@ -11,7 +12,7 @@ defmodule RigAuth.AuthorizationCheck.Header do def any_valid_bearer_token?(request) def any_valid_bearer_token?(%{auth_info: %{auth_tokens: tokens}}) do - for({"bearer", token} <- tokens, do: Jwt.valid?(token)) + for({"bearer", token} <- tokens, do: JWT.valid?(token)) |> Enum.any?() end diff --git a/apps/rig_auth/lib/rig_auth/jwt/utils.ex b/apps/rig_auth/lib/rig_auth/jwt/utils.ex deleted file mode 100644 index 1cb02fa4..00000000 --- a/apps/rig_auth/lib/rig_auth/jwt/utils.ex +++ /dev/null @@ -1,83 +0,0 @@ -defmodule RigAuth.Jwt.Utils do - @moduledoc """ - Provides utility functions over JWT using Joken - """ - use Rig.Config, [:secret_key] - - import Joken - - alias Plug - alias RigAuth.Blacklist - - @type claims :: %{required(String.t()) => String.t()} - - @spec valid?(String.t()) :: boolean - def valid?("Bearer " <> jwt) do - jwt - |> validate - |> get_error == nil - end - - def valid?(invalid_access_token) do - %{ - error: - "JWT=#{invalid_access_token} is missing token type. Required format is: \"Bearer token\"" - } - end - - # --- - - @spec decode(String.t()) :: {:ok, claims} | {:error, String.t()} - def decode(jwt) do - jwt - |> validate - |> get_data - end - - # --- - - @spec generate(map) :: String.t() - def generate(claims) do - token() - |> with_exp() - |> with_signer(signer()) - |> with_claims(claims) - |> sign() - |> get_compact() - end - - # --- - - defp signer do - conf = config() - - case conf.alg do - "HS" <> _ = alg -> Joken.Signer.hs(alg, conf.secret_key) - "RS" <> _ = alg -> Joken.Signer.rs(alg, JOSE.JWK.from_pem(conf.secret_key)) - end - end - - # --- - - @spec validate(String.t()) :: Joken.Token.t() - defp validate(jwt) do - jwt - |> token() - |> with_validation("exp", &(&1 > current_time()), "token expired") - |> with_signer(signer()) - |> verify() - |> check_blacklist() - end - - # --- - - @spec check_blacklist(token :: Joken.Token.t()) :: Joken.Token.t() - defp check_blacklist(%{error: nil, claims: %{"jti" => jti}} = token) do - case Blacklist.contains_jti?(Blacklist, jti) do - true -> %{token | error: "JWT with JTI=#{jti} is blacklisted"} - false -> token - end - end - - defp check_blacklist(token), do: token -end diff --git a/apps/rig_auth/test/support/conn_case.ex b/apps/rig_auth/test/support/conn_case.ex index 59e0ab1c..38ca14d5 100644 --- a/apps/rig_auth/test/support/conn_case.ex +++ b/apps/rig_auth/test/support/conn_case.ex @@ -19,26 +19,6 @@ defmodule RigAuth.ConnCase do quote do # Import conveniences for testing with connections use Phoenix.ConnTest - - import Joken - - # Generation of JWT - def generate_jwt(priv_key \\ nil) do - jwt_secret_key = System.get_env("JWT_SECRET_KEY") - jwt_alg = System.get_env("JWT_ALG") - - signer = - case jwt_alg do - "HS" <> _ = alg -> Joken.Signer.hs(alg, jwt_secret_key) - "RS" <> _ = alg -> Joken.Signer.rs(alg, JOSE.JWK.from_pem(priv_key)) - end - - token() - |> with_exp - |> with_signer(signer) - |> sign - |> get_compact - end end end end diff --git a/apps/rig_inbound_gateway/lib/rig_inbound_gateway/api_proxy/auth/jwt.ex b/apps/rig_inbound_gateway/lib/rig_inbound_gateway/api_proxy/auth/jwt.ex index 16c02306..8f9143f9 100644 --- a/apps/rig_inbound_gateway/lib/rig_inbound_gateway/api_proxy/auth/jwt.ex +++ b/apps/rig_inbound_gateway/lib/rig_inbound_gateway/api_proxy/auth/jwt.ex @@ -5,47 +5,59 @@ defmodule RigInboundGateway.ApiProxy.Auth.Jwt do import Plug.Conn, only: [get_req_header: 2] - alias RigAuth.Jwt + alias RIG.JWT alias RigInboundGateway.ApiProxy.Api # --- def check(conn, api) do - tokens = - pick_query_token(conn, api) - |> Enum.concat(pick_header_token(conn, api)) - |> Enum.reject(&(&1 == "")) - - if Enum.any?(tokens, &Jwt.Utils.valid?/1) do - :ok - else - {:error, :authentication_failed} + valid_tokens = + tokens_from_query_params(conn, api) ++ + tokens_from_req_header(conn, api) + + case valid_tokens do + [] -> {:error, :authentication_failed} + _ -> :ok end end # --- - # Pick JWT from query parameters - @spec pick_query_token(Plug.Conn.t(), Api.t()) :: [String.t()] + # Find and parse JWTs in request query params. + @spec tokens_from_query_params(Plug.Conn.t(), Api.t()) :: [JWT.claims()] + + defp tokens_from_query_params(conn, %{"auth" => %{"use_query" => true}} = api) do + selected_param = get_in(api, ["auth", "query_name"]) + tokens_string = Map.get(conn.query_params, selected_param, "") - defp pick_query_token(conn, %{"auth" => %{"use_query" => true}} = api) do - conn - |> Map.get(:query_params) - |> Map.get(Kernel.get_in(api, ["auth", "query_name"]), "") - |> String.split() + tokens_string + |> String.split(",", trim: true) + |> Enum.map(&JWT.parse_token/1) + |> Result.filter_and_unwrap() end - defp pick_query_token(_conn, %{"auth" => %{"use_query" => false}}), do: [""] + defp tokens_from_query_params(_conn, _), do: [] # -- - # Pick JWT from headers - @spec pick_header_token(Plug.Conn.t(), Api.t()) :: [String.t()] + # Find and parse JWTs in request headers. + @spec tokens_from_req_header(Plug.Conn.t(), Api.t()) :: [JWT.claims()] + + defp tokens_from_req_header(conn, %{"auth" => %{"use_header" => true}} = api) do + selected_auth_header = get_in(api, ["auth", "header_name"]) |> String.downcase() - defp pick_header_token(conn, %{"auth" => %{"use_header" => true}} = api) do - header_key = Kernel.get_in(api, ["auth", "header_name"]) |> String.downcase() - get_req_header(conn, header_key) + case selected_auth_header do + "authorization" -> + JWT.parse_http_header(conn.req_headers) + |> Result.filter_and_unwrap() + + "x-" <> _ = custom_header -> + conn + |> get_req_header(custom_header) + |> Enum.map(&JWT.parse_token/1) + |> Result.filter_and_unwrap() + end end - defp pick_header_token(_conn, %{"auth" => %{"use_header" => false}}), do: [""] + defp tokens_from_req_header(_conn, _), do: [] end diff --git a/apps/rig_inbound_gateway/test/rig/api_proxy/proxy_metrics_test.exs b/apps/rig_inbound_gateway/test/rig/api_proxy/proxy_metrics_test.exs index 67b48f14..b887a11e 100644 --- a/apps/rig_inbound_gateway/test/rig/api_proxy/proxy_metrics_test.exs +++ b/apps/rig_inbound_gateway/test/rig/api_proxy/proxy_metrics_test.exs @@ -7,6 +7,7 @@ defmodule RigInboundGateway.ApiProxy.ProxyMetricsTest do import FakeServer alias FakeServer.Response + alias RIG.JWT alias RigMetrics.ProxyMetrics alias RigInboundGatewayWeb.Router @@ -93,7 +94,7 @@ defmodule RigInboundGateway.ApiProxy.ProxyMetricsTest do # --- defp construct_request_with_jwt(method, url, query \\ %{}) do - jwt = generate_jwt() + jwt = JWT.encode(%{}) build_conn(method, url, query) |> put_req_header("authorization", "Bearer #{jwt}") diff --git a/apps/rig_inbound_gateway/test/rig/api_proxy/router_test.exs b/apps/rig_inbound_gateway/test/rig/api_proxy/router_test.exs index 3adb3790..153173e7 100644 --- a/apps/rig_inbound_gateway/test/rig/api_proxy/router_test.exs +++ b/apps/rig_inbound_gateway/test/rig/api_proxy/router_test.exs @@ -9,6 +9,7 @@ defmodule RigInboundGateway.ApiProxy.RouterTest do import FakeServer alias FakeServer.Response + alias RIG.JWT alias RigInboundGatewayWeb.Router @env [port: 7070] @@ -166,7 +167,7 @@ defmodule RigInboundGateway.ApiProxy.RouterTest do filepath = "#{__DIR__}/#{filename}" file_part = {:file, filepath, displayname, [{"content-type", "plain/text"}]} body = {:multipart, [file_part]} - headers = [{"authorization", "Bearer #{generate_jwt()}"}] + headers = [{"authorization", "Bearer #{JWT.encode(%{})}"}] url = "http://localhost:#{@env[:port]}/myapi/books" assert {:ok, @@ -359,7 +360,7 @@ defmodule RigInboundGateway.ApiProxy.RouterTest do end defp construct_request_with_jwt(method, url, query \\ %{}) do - jwt = generate_jwt() + jwt = JWT.encode(%{}) build_conn(method, url, query) |> put_req_header("authorization", "Bearer #{jwt}") diff --git a/apps/rig_inbound_gateway/test/support/conn_case.ex b/apps/rig_inbound_gateway/test/support/conn_case.ex index 25619cad..afd07b5a 100644 --- a/apps/rig_inbound_gateway/test/support/conn_case.ex +++ b/apps/rig_inbound_gateway/test/support/conn_case.ex @@ -21,7 +21,6 @@ defmodule RigInboundGatewayWeb.ConnCase do use Phoenix.ConnTest import RigInboundGatewayWeb.Router.Helpers - import Joken # The default endpoint for testing @endpoint RigInboundGatewayWeb.Endpoint @@ -57,19 +56,6 @@ defmodule RigInboundGatewayWeb.ConnCase do "versioned" => false, "active" => true } - - # The key for signing JWTs: - @jwt_secret_key Confex.fetch_env!(:rig, RigInboundGatewayWeb.ConnCase) - |> Keyword.fetch!(:jwt_secret_key) - - # Generation of JWT - def generate_jwt do - token() - |> with_exp - |> with_signer(@jwt_secret_key |> hs256) - |> sign - |> get_compact - end end end diff --git a/mix.exs b/mix.exs index 716c7f9c..ec80cb7d 100644 --- a/mix.exs +++ b/mix.exs @@ -44,7 +44,7 @@ defmodule Rig.Umbrella.Mixfile do [ {:excoveralls, "~> 0.10", only: :test}, {:credo, "~> 1.0", only: [:dev, :test]}, - {:dialyxir, "~> 0.5", only: [:dev, :test]}, + {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false}, {:distillery, "~> 2.0"}, {:ex_doc, "~> 0.16", only: :dev, runtime: false}, {:mix_test_watch, "~> 0.5", only: :dev, runtime: false} diff --git a/mix.lock b/mix.lock index cb627ac3..294f5b01 100644 --- a/mix.lock +++ b/mix.lock @@ -12,11 +12,12 @@ "crc32cer": {:hex, :crc32cer, "0.1.3", "8984906c4b4fae6aa292c48f286a1c83b19ad44bd102287acb94d696015967ce", [:make, :rebar, :rebar3], [], "hexpm"}, "credo": {:hex, :credo, "1.0.4", "d2214d4cc88c07f54004ffd5a2a27408208841be5eca9f5a72ce9e8e835f7ede", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "deferred_config": {:hex, :deferred_config, "0.1.1", "ec912e9ee3c99b90a8d4bdec8fbd15309f4bd6729f30789e0ff6f595d06bbce5", [:mix], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, "distillery": {:hex, :distillery, "2.0.12", "6e78fe042df82610ac3fa50bd7d2d8190ad287d120d3cd1682d83a44e8b34dfb", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"}, "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.5.2", "96a28c79f5b8d34879cd95ebc04d2a0d678cfbbd3e74c43cb63a76adf0ee8054", [:mix], [], "hexpm"}, "erlavro": {:hex, :erlavro, "2.6.5", "b144ed22bcca64f3a811569d8687e3445b2c2ca7add41c0eac1aefbd2a52c771", [:make, :rebar, :rebar3], [{:jsone, "1.4.6", [hex: :jsone, repo: "hexpm", optional: false]}], "hexpm"}, + "erlex": {:hex, :erlex, "0.2.4", "23791959df45fe8f01f388c6f7eb733cc361668cbeedd801bf491c55a029917b", [:mix], [], "hexpm"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws_kinesis": {:hex, :ex_aws_kinesis, "2.0.1", "7f8746e89904ccc6da2c9575e8fe08584e189b2739d964968bba2b9ec1f57fd6", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm"},