Skip to content

Commit a1c6020

Browse files
Merge pull request #5 from joken-elixir/feat/override_algorithm
feat: override "alg" claim
2 parents 8a0ccf8 + f0d5385 commit a1c6020

File tree

12 files changed

+239
-76
lines changed

12 files changed

+239
-76
lines changed

.travis.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,29 @@ language: elixir
44
elixir:
55
- 1.6.6
66
- 1.7.4
7+
- 1.8.1
78

89
otp_release:
910
- 19.3
1011
- 20.3
11-
- 21.0
12+
- 21.2
13+
14+
matrix:
15+
exclude:
16+
- otp_release: 19.3
17+
elixir: 1.8.1
1218

1319
script:
14-
- mix credo
20+
- mix credo --strict
1521
- mix format --check-formatted
1622
- MIX_ENV=test mix coveralls.travis
23+
- mix test --only external
24+
25+
cache:
26+
directories:
27+
- ~/.mix
28+
- ~/.hex
29+
- deps
30+
- _build
31+
32+

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [1.1.0] - 2019-03-05
10+
11+
### Added
12+
13+
- Options for explicitly telling which algorithm will use for the parsed signers;
14+
- HTTP options for retry and adapter;
15+
- Integration tests for Google and Microsoft JWKS endpoints.
16+
17+
### Fixed
18+
19+
- Fixed docs about how to use DefaultStrategyTemplate and Fixed spelling (#4 thanks to @bforchhammer)
20+
921
## [1.0.0] - 2019-01-02
1022

1123
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Please see our guides and documentation for usage.
1515
```elixir
1616
def deps do
1717
[
18-
{:joken_jwks, "~> 1.0.0"}
18+
{:joken_jwks, "~> 1.1.0"}
1919
]
2020
end
2121
```

guides/introduction.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,14 @@ This could potentially open an attack vector for massively hitting the authentic
3737

3838
`JokenJwks` comes with a smart enough implementation that uses a time window approach for re-fetching signers. By default, it polls the cache state every minute to see if a bad kid was attempted. If so, it refetches the cache. So, it will fetch JWKS once every minute tops.
3939

40+
## Interpretation of the JWKS RFC
41+
42+
Since the JWKS specification is just that, a specification, many servers might disagree on how to implement this. For example, Google specifies the "alg" claim on every key instance. Microsoft does not. Therefore we assume some interpretations:
43+
44+
- Every key must have a "kid" (even if there is only one key);
45+
- We don't currently check for the "use" claim and so we might hit an encryption key (which will be parsed as well);
46+
- If no "alg" claim is provided, then the user must pass the option "explicit_alg".
47+
48+
That's it for now :)
49+
50+

lib/joken_jwks/default_strategy_template.ex

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@ defmodule JokenJwks.DefaultStrategyTemplate do
4444
events in the strategy like HTTP errors and so on. It is advised not to turn off logging in production;
4545
- `should_start` (`boolean()` - default true): whether to start the supervised polling task. For tests, this
4646
should be false;
47-
- `first_fetch_sync` (`boolean()` - default false): whether to fetch the first time synchronously or async.
47+
- `first_fetch_sync` (`boolean()` - default false): whether to fetch the first time synchronously or async;
48+
- `explicit_alg` (`String.t()`): the JWS algorithm for use with the key. Overrides the one in the JWK;
49+
- `http_max_retries_per_fetch` (`pos_integer()` - default 10): passed to `Tesla.Middleware.Retry`;
50+
- `http_delay_per_retry` (`pos_integer()` - default 500): passed to `Tesla.Middleware.Retry`.
4851
4952
### Example usage:
5053
51-
defmodule MyStrategy do
54+
defmodule JokenExample.MyStrategy do
5255
use JokenJwks.DefaultMatchStrategy
5356
5457
def init_opts(opts) do
@@ -158,26 +161,32 @@ defmodule JokenJwks.DefaultStrategyTemplate do
158161
url = opts[:jwks_url] || raise "No url set for fetching JWKS!"
159162
EtsCache.new()
160163

161-
do_init(start?, first_fetch_sync, time_interval, log_level, url)
164+
opts =
165+
opts
166+
|> Keyword.put(:time_interval, time_interval)
167+
|> Keyword.put(:log_level, log_level)
168+
|> Keyword.put(:jwks_url, url)
169+
170+
do_init(start?, first_fetch_sync, opts)
162171
end
163172

164-
defp do_init(start?, first_fetch_sync, time_interval, log_level, url) do
173+
defp do_init(should_start, first_fetch_sync, opts) do
165174
cond do
166-
start? and first_fetch_sync ->
167-
fetch_signers(url, log_level)
168-
Task.start_link(__MODULE__, :poll, [time_interval, log_level, url])
175+
should_start and first_fetch_sync ->
176+
fetch_signers(opts[:jwks_url], opts)
177+
Task.start_link(__MODULE__, :poll, [opts])
169178

170-
start? ->
171-
start_fetch_signers(url, log_level)
172-
Task.start_link(__MODULE__, :poll, [time_interval, log_level, url])
179+
should_start ->
180+
start_fetch_signers(opts[:jwks_url], opts)
181+
Task.start_link(__MODULE__, :poll, [opts])
173182

174183
true ->
175184
{:ok, spawn_link(fn -> "Normal shutdown" end)}
176185
end
177186
end
178187

179188
@impl SignerMatchStrategy
180-
def match_signer_for_kid(kid, _opts) do
189+
def match_signer_for_kid(kid, opts) do
181190
with {:cache, [{:signers, signers}]} <- {:cache, EtsCache.get_signers()},
182191
{:signer, signer} when not is_nil(signer) <- {:signer, signers[kid]} do
183192
{:ok, signer}
@@ -195,37 +204,41 @@ defmodule JokenJwks.DefaultStrategyTemplate do
195204
end
196205

197206
@doc false
198-
def poll(time_interval, log_level, url) when is_integer(time_interval) do
207+
def poll(opts) do
208+
interval = opts[:time_interval]
209+
199210
receive do
200211
after
201-
time_interval ->
202-
check_fetch(url, log_level)
203-
poll(time_interval, log_level, url)
212+
interval ->
213+
check_fetch(opts)
214+
poll(opts)
204215
end
205216
end
206217

207-
defp check_fetch(url, log_level) do
218+
defp check_fetch(opts) do
208219
case EtsCache.check_state() do
209220
# no need to re-fetch
210221
0 ->
211-
JokenJwks.log(:debug, log_level, "Re-fetching cache is not needed.")
222+
JokenJwks.log(:debug, opts[:log_level], "Re-fetching cache is not needed.")
212223
:ok
213224

214225
# start re-fetching
215226
_counter ->
216-
JokenJwks.log(:debug, log_level, "Re-fetching cache is needed and will start.")
217-
start_fetch_signers(url, log_level)
227+
JokenJwks.log(:debug, opts[:log_level], "Re-fetching cache is needed and will start.")
228+
start_fetch_signers(opts[:jwks_url], opts)
218229
end
219230
end
220231

221-
defp start_fetch_signers(url, log_level) do
222-
Task.start(fn -> fetch_signers(url, log_level) end)
232+
defp start_fetch_signers(url, opts) do
233+
Task.start(fn -> fetch_signers(url, opts) end)
223234
end
224235

225236
@doc "Fetch signers with `JokenJwks.HttpFetcher`"
226-
def fetch_signers(url, log_level) do
227-
with {:ok, keys} <- HttpFetcher.fetch_signers(url, log_level),
228-
{:ok, signers} <- validate_and_parse_keys(keys, log_level) do
237+
def fetch_signers(url, opts) do
238+
log_level = opts[:log_level]
239+
240+
with {:ok, keys} <- HttpFetcher.fetch_signers(url, opts),
241+
{:ok, signers} <- validate_and_parse_keys(keys, opts) do
229242
JokenJwks.log(:debug, log_level, "Fetched signers. #{inspect(signers)}")
230243
EtsCache.put_signers(signers)
231244
EtsCache.set_status(:ok)
@@ -245,28 +258,28 @@ defmodule JokenJwks.DefaultStrategyTemplate do
245258
end
246259
end
247260

248-
defp validate_and_parse_keys(keys, log_level) when is_list(keys) do
261+
defp validate_and_parse_keys(keys, opts) when is_list(keys) do
249262
Enum.reduce_while(keys, {:ok, %{}}, fn key, {:ok, acc} ->
250-
with {:ok, signer} <- parse_signer(key, log_level) do
263+
with {:ok, signer} <- parse_signer(key, opts) do
251264
{:cont, {:ok, Map.put(acc, key["kid"], signer)}}
252265
else
253266
e -> {:halt, e}
254267
end
255268
end)
256269
end
257270

258-
defp parse_signer(key, log_level) do
271+
defp parse_signer(key, opts) do
259272
with {:kid, kid} when is_binary(kid) <- {:kid, key["kid"]},
260-
{:alg, alg} when is_binary(alg) <- {:alg, key["alg"]},
273+
{:ok, alg} <- get_algorithm(key["alg"], opts[:explicit_alg]),
261274
{:ok, _signer} = res <- {:ok, Signer.create(alg, key)} do
262275
res
263276
else
264277
{:kid, _} -> {:error, :kid_not_binary}
265-
{:alg, _} -> {:error, :algorithm_not_binary}
278+
err -> err
266279
end
267280
rescue
268281
e ->
269-
JokenJwks.log(:error, log_level, """
282+
JokenJwks.log(:error, opts[:log_level], """
270283
Error while parsing a key entry fetched from the network.
271284
272285
This should be investigated by a human.
@@ -278,6 +291,15 @@ defmodule JokenJwks.DefaultStrategyTemplate do
278291

279292
{:error, :invalid_key_params}
280293
end
294+
295+
# According to JWKS spec (https://tools.ietf.org/html/rfc7517#section-4.4) the "alg"" claim
296+
# is not mandatory. This is why we allow this to be passed as a hook option.
297+
#
298+
# We give preference to the one provided as option
299+
defp get_algorithm(nil, nil), do: {:error, :no_algorithm_supplied}
300+
defp get_algorithm(_, alg) when is_binary(alg), do: {:ok, alg}
301+
defp get_algorithm(alg, _) when is_binary(alg), do: {:ok, alg}
302+
defp get_algorithm(_, _), do: {:error, :bad_algorithm}
281303
end
282304
end
283305
end

lib/joken_jwks/http_fetcher.ex

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ defmodule JokenJwks.HttpFetcher do
1212
"""
1313
alias Tesla.Middleware, as: M
1414

15-
@middleware [{M.Retry, delay: 500, max_retries: 10}, M.JSON, M.Logger]
16-
1715
@doc """
1816
Fetches the JWKS signers from the given url.
1917
@@ -23,8 +21,10 @@ defmodule JokenJwks.HttpFetcher do
2321
We use `:hackney` as it validates certificates automatically.
2422
"""
2523
@spec fetch_signers(binary, boolean) :: {:ok, list} | {:error, atom} | no_return()
26-
def fetch_signers(url, log_level) do
27-
with {:ok, resp} <- Tesla.get(new(), url),
24+
def fetch_signers(url, opts) do
25+
log_level = opts[:log_level]
26+
27+
with {:ok, resp} <- Tesla.get(new(opts), url),
2828
{:status, 200} <- {:status, resp.status},
2929
{:keys, keys} when not is_nil(keys) <- {:keys, resp.body["keys"]} do
3030
JokenJwks.log(:debug, log_level, "JWKS fetching: fetched keys -> #{inspect(keys)}")
@@ -56,11 +56,21 @@ defmodule JokenJwks.HttpFetcher do
5656

5757
@default_adapter Tesla.Adapter.Hackney
5858

59-
defp new do
59+
defp new(opts) do
6060
adapter =
6161
Application.get_env(:tesla, __MODULE__)[:adapter] ||
6262
Application.get_env(:tesla, :adapter, @default_adapter)
6363

64-
Tesla.client(@middleware, adapter)
64+
adapter = opts[:http_adapter] || adapter
65+
66+
middleware = [
67+
M.JSON,
68+
M.Logger,
69+
{M.Retry,
70+
delay: opts[:http_delay_per_retry] || 500,
71+
max_retries: opts[:http_max_retries_per_fetch] || 10}
72+
]
73+
74+
Tesla.client(middleware, adapter)
6575
end
6676
end

mix.exs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule JokenJwks.MixProject do
22
use Mix.Project
33

4-
@version "1.0.0"
4+
@version "1.1.0"
55

66
def project do
77
[
@@ -39,7 +39,7 @@ defmodule JokenJwks.MixProject do
3939

4040
defp deps do
4141
[
42-
{:joken, "~> 2.0.0"},
42+
{:joken, "~> 2.0"},
4343
{:jason, "~> 1.1"},
4444
{:tesla, "~> 1.2"},
4545
{:hackney, "~> 1.14"},
@@ -52,7 +52,7 @@ defmodule JokenJwks.MixProject do
5252
{:excoveralls, "~> 0.10", only: :test},
5353

5454
# tests
55-
{:mox, "~> 0.4", only: :test}
55+
{:mox, "~> 0.5", only: :test}
5656
]
5757
end
5858

mix.lock

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,26 @@
22
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
33
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
44
"cachex": {:hex, :cachex, "3.1.1", "588bcf48d20eddad7bff5172f5453090a071eba3191a03f51f779f88e3ac1900", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
5-
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
6-
"credo": {:hex, :credo, "1.0.0", "aaa40fdd0543a0cf8080e8c5949d8c25f0a24e4fc8c1d83d06c388f5e5e0ea42", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
7-
"earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"},
5+
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
6+
"credo": {:hex, :credo, "1.0.2", "88bc918f215168bf6ce7070610a6173c45c82f32baa08bdfc80bf58df2d103b6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
7+
"earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"},
88
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
9-
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
10-
"excoveralls": {:hex, :excoveralls, "0.10.3", "b090a3fbcb3cfa136f0427d038c92a9051f840953ec11b40ee74d9d4eac04d1e", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
9+
"ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
10+
"excoveralls": {:hex, :excoveralls, "0.10.6", "e2b9718c9d8e3ef90bc22278c3f76c850a9f9116faf4ebe9678063310742edc2", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
1111
"fastglobal": {:hex, :fastglobal, "1.0.0", "f3133a0cda8e9408aac7281ec579c4b4a8386ce0e99ca55f746b9f58192f455b", [:mix], [], "hexpm"},
12-
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
12+
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
1313
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
1414
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
15-
"joken": {:hex, :joken, "2.0.0", "ff10fca10ec539d7a73874da303f4a7a975fea53fcd59b1b89dda2a71ecb4c6b", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
15+
"joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
1616
"jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
1717
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm"},
18-
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
19-
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
18+
"makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
19+
"makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
2020
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
2121
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
22-
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
23-
"mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"},
24-
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
22+
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
23+
"mox": {:hex, :mox, "0.5.0", "c519b48407017a85f03407a9a4c4ceb7cc6dec5fe886b2241869fb2f08476f9e", [:mix], [], "hexpm"},
24+
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
2525
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
2626
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
2727
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},

0 commit comments

Comments
 (0)